תקציבי זיכרון לאפליקציות מאפשרים לאפליקציות להצהיר על תקציב זיכרון משלהן, מה שאומר למערכת לצמצם את השימוש בזיכרון כשהאפליקציה משתמשת ביותר מהתקציב שהוגדר לה. האפשרות הזו שימושית במיוחד לאפליקציות מערכת ואפליקציות בחבילה, או לאפליקציות שמטרגטות מכשירים עם מגבלות זיכרון, שבהן המפתח יודע מהי קבוצת העבודה הצפויה בזיכרון (הזיכרון שהאפליקציה צריכה באופן פעיל לפעולה שהמשתמש מבצע כרגע) ורוצה לוודא שהאפליקציה לא משתמשת ביותר מדי ממשאבי ה-RAM המשותפים של המערכת.
כדי לשמור על איזון בתקציב, המערכת משתמשת בפינוי זיכרון ובהחלפה כדי להסיר דפי זיכרון שלא היו בשימוש לאחרונה, וכך מתמקדת בזיכרון שבשימוש של האפליקציה בקבוצת העבודה הנוכחית שלה. כשאפליקציה חורגת מהתקציב שהוקצה לה, מערכת ההפעלה מנסה להחזיר את הזיכרון שהוקצה לאפליקציה הזו:
- דפים נקיים שמגובים בקבצים (כמו קוד לא פעיל ונכסים ממופים) מפונים קודם, כי אפשר לקרוא אותם מחדש מהאחסון במקרה הצורך.
- דפים מלוכלכים שמגובים בקבצים נכתבים בחזרה לאחסון ומוסרים.
- דפי זיכרון אנונימיים (כמו הקצאות זיכרון בערימה) נדחסים ומוחלפים ב-zRAM.
כל עוד קבוצת העבודה לא חורגת מהתקציב, האפליקציה תפעל היטב בלי להשתמש ביותר זיכרון מהתקציב שהוגדר לה. מערכת ההפעלה מפנה זיכרון שלא בשימוש ודוחסת דפי ערימה לא פעילים להחלפה, כדי שהקצאות הזיכרון יישארו מוגבלות בלי לסיים את התהליך.
מה קורה כשאפליקציה חורגת מהתקציב שלה
חריגה מתקציב הזיכרון לא גורמת למערכת לסגור את האפליקציה או לקרוס. במקום זאת, כשאפליקציה משתמשת ביותר זיכרון ממה שהתקציב שלה מאפשר, מערכת ההפעלה מוצאת זיכרון שהאפליקציה לא השתמשה בו במשך זמן מה ומאחסנת אותו באופן בטוח עד שיהיה בו צורך שוב. כך אפשר להקצות זיכרון חדש לאפליקציה ולשמור על השימוש הכולל בזיכרון של האפליקציה במסגרת התקציב.
כל עוד יש מרווח בין מה שהאפליקציה צריכה בכל זמן נתון (קבוצת העבודה שלה) לבין התקציב שהוגדר, האפליקציה ממשיכה לפעול כרגיל. אם אפליקציה מגדירה תקציב קטן יותר מקבוצת העבודה שלה, הביצועים של האפליקציה בזמן הריצה עלולים להיות איטיים יותר כתוצאה מכך.
במדריך לארכיטקטורת הזיכרון מוסבר איך מערכת ההפעלה עוקבת אחרי הזיכרון ומחזירה אותו:
- החזרת זיכרון והחלפה: הסבר על האופן שבו מערכת ההפעלה מחייבת את האפליקציה על דפים אנונימיים פרטיים של ערימה ודפים מגובים בקבצים, ומחזירה דפים קרים כשהתקציב חורג.
- מושגים בסיסיים: הסבר למה זיכרון Zygote משותף לא נכלל בתקציב של האפליקציה, ואיך תקציבי הזיכרון הפיזי קשורים להחלפה + RSS אנונימי בטלמטריה בשטח.
- מפות סיביות וזיכרון: הסבר על האופן שבו מפות סיביות של חומרה והקצאות DMA-BUF שמגובות על ידי GPU נספרות מחוץ לערימה ולתקציב הזיכרון של האפליקציה.
הצהרה על תקציבים במניפסט של Android
הצהרה על תקציבי הזיכרון ב-AndroidManifest.xml היא השיטה העיקרית והמומלצת להגדרת תקציבים. הוא לא דורש קוד זמן ריצה, נכנס לתוקף מיד עם הפעלת התהליך ומספק חוזה ברור למערכת ההפעלה.
הצהרות <memory-budget> נכנסות לתוקף במכשירים עם Android 17 QPR2 (רמת API 37.2) ומעלה. בגרסאות קודמות של Android, מנתח המניפסט של הפלטפורמה מתעלם באופן בטוח מרכיבי XML לא מזוהים, כך שאתם יכולים להשתמש ב-<memory-budget> בלי להשפיע על התאימות לדורות קודמים.
הצהרה על תקציב בסיסי
ברוב האפליקציות, כל מה שצריך לעשות הוא להגדיר תקציב אחד לאפליקציה. מצהירים על רכיב <memory-budget> ישירות בתוך התג <application>:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.simpleapp">
<application
android:label="@string/app_name">
<!-- Baseline budget for the application -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
ההגדרה הזו קובעת תקציב זיכרון תושב של 256MB לכל התהליכים והמצבים של החבילה. כשהזיכרון שבשימוש של האפליקציה חורג מ-256MB, מערכת ההפעלה מצמצמת את דפי הזיכרון הלא פעילים באמצעות פינוי והחלפה.
שינוי התקציבים בהתאם למצב התהליך
אפליקציה דורשת כמויות שונות של זיכרון בהתאם לנראות שלה למשתמש:
- חזית: התהליך מארח פעילות גלויה שיוצרת אינטראקציה עם המשתמש. במצב הזה בדרך כלל יש את טביעת הרגל הגדולה ביותר בגלל ממשק המשתמש והגרפיקה הפעילים.
- ניתן לזיהוי: המשתמש יכול לזהות את התהליך, אבל הוא לא מארח חלון גלוי (לדוגמה, אירוח של שירות שפועל בחזית להפעלת מדיה, הורדה פעילה ברקע, מסלול מפורט או שיטת קלט פעילה).
- ברקע: התהליך מריץ משימות, מקלטים או סנכרוני נתונים ברקע. הוא צפוי לשמור על טביעת רגל מינימלית.
אפשר להצהיר על כמה משפטי <memory-budget> כדי להתאים למצבים האלה:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.simpleapp">
<application
android:label="@string/app_name">
<!-- Default budget for visible foreground UI -->
<memory-budget android:maxMb="200" />
<!-- Tighter budget when playing audio in background -->
<memory-budget
android:maxMb="120"
android:state="perceptible" />
<!-- Minimal budget when fully in background -->
<memory-budget
android:maxMb="48"
android:state="background" />
</application>
</manifest>
לא חובה להוסיף סעיף בסיס בלי android:state. אם מציינים רק סעיפים ספציפיים למדינה (לדוגמה, android:state="background"), מדינות אחרות לא יהיו מוגבלות על ידי תקציב האפליקציה. אם מוסיפים סעיף בלי android:state, הוא משמש כברירת מחדל למצבים לא מוגדרים (כמו חזית), וסעיפים מגבילים יותר שמופיעים אחריו מבטלים אותו כשהאפליקציה עוברת למצבים perceptible או background.
כשמגדירים את גודל התקציבים לכל מצב, צריך לטרגט את קבוצת העבודה הפעילה של אותו מצב, ולא את הזיכרון התושב המקסימלי (סימן המים הגבוה של RSS):
- תקציבי
foregroundלעיבוד חלק של פריימים: צריך להקצות מספיק מקום לממשק המשתמש שפועל בחזית כדי שפינוי הזיכרון הסינכרוני לא יעכב את עיבוד הפריימים. אם האפליקציה כוללת תהליך עבודה עצמאי שדורש הרבה זיכרון (כמו עורך תמונות או סרטונים ברזולוציה גבוהה), כדאי להעביר את הפעילות הזו לתהליך ייעודי (ראו אפליקציות עם כמה תהליכים) כדי שההקצאות שלו יתוקצבו בנפרד ויוסרו כשהמשתמש ייצא. - הקצאת תקציבי
backgroundקפדניים לעבודה ללא ממשק משתמש: ערכי RSS גבוהים ברקע בנתוני הטלמטריה בשטח משקפים לעיתים קרובות הקצאות של ממשק משתמש שנשמרו אחרי היציאה מהחזית (ראו מיקום משותף של שירות מאוגד עם ממשק משתמש שדורש הרבה זיכרון ושחרור זיכרון בתגובה לאירועים) או קריאות חד-פעמיות של קבצים במהלך הוספה לאינדקס וסנכרון ברקע. מכיוון שלעבודה ברקע ללא ממשק משתמש אין מועדים אחרונים למסגרות, פינוי דפים של מטמון קבצים חד-פעמיים והחלפת דפים לא פעילים של ערימה ל-zRAM כשסנכרון ברקע מקצה לזמן קצר מעבר לתקציב שלו לא גורם לבעיות שגלויות למשתמש, ומונע מעליות חדות ברקע לפנות את האפליקציה שפועלת בחזית של המשתמש.
איך מצבי התקציב ממפים למצבי התהליך
הפלטפורמה ממפה את מצבי התקציב במניפסט למצבי תהליך בזמן ריצה על סמך RunningAppProcessInfo.importance, שמנהל הפעילות מחשב גם מהרכיבים הפעילים של התהליך עצמו וגם מכל כבילות השירות הנכנסות או שאילתות של ספקים לניהול גישה מאפליקציות אחרות:
foreground: אינטראקציות פעילות וגלויות של משתמשים, כמו אירוח של Activity שהתחדשה, שמירה על מצב האפליקציה העליונה כשהמסך נכבה, או אירוח של שירות או ספק לניהול גישה שקשורים באופן פעיל לאפליקציה שפועלת בחזית או נשלחת אליהם שאילתה מהאפליקציה הזו.-
perceptible: עומסי עבודה שהמשתמש יכול להבחין בהם בלי חלון גלוי, כמו הפעלת מדיה פעילה, ניווט מפורט, צילום במצלמה או הקלטה במיקרופון, הורדות פעילות ברקע, שירותים שפועלים בחזית לסנכרון נתונים או שירותים שקשורים ללקוח שפועל בחזית באמצעות דגלים כמוBIND_NOT_FOREGROUND(ראו שליטה בירושה באמצעות דגלי BIND). יכול להיות שבמדדים פנימיים של הפלטפורמה חלק מעומסי העבודה האלה יסומנו כ-PROCESS_STATE_IMPORTANT_FOREGROUND, אבל הקבוע הפנימי הזה לא מציין חלון גלוי והוא נשלט על ידי התקציב שלperceptible. -
background: עבודה שהמשתמש לא מבחין בה באופן מיידי, כמו משימות ברקע, התראות, מקלטי שידור או הפעילות הקודמת אחרי שהמשתמש עובר למקום אחר.
בטבלה הבאה מוצג המיפוי של רמות החשיבות בזמן הריצה למצבי המניפסט:
מניפסט android:state |
חשיבות זמן הריצה (RunningAppProcessInfo) |
רכיבים אופייניים |
|---|---|---|
foreground |
IMPORTANCE_FOREGROUNDIMPORTANCE_TOP_SLEEPING |
פעילות גלויה שהופעלה מחדש, אפליקציה מובילה עם מסך נעול, שירות או ספק לניהול גישה שקשורים לאפליקציה המובילה |
perceptible |
IMPORTANCE_FOREGROUND_SERVICEIMPORTANCE_VISIBLE |
הפעלה פעילה של מדיה, ניווט, הורדות או סנכרון של שירותים שפועלים בחזית; שירות שקשור לשירות שפועל בחזית או לדגלים גלויים |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICE |
משימות ברקע, מקלטים, סנכרון ברקע, Activity קודמת או אפליקציית Home ברקע |
במאמר כבילות שירות ומצבי תהליכים מוסבר איך כבילות לקוח מעלות את העדיפות של תהליך מארח של שירות או ספק, ואיך התהליך חוזר למצב background או cached אחרי שהלקוח מבטל את הכבילה או עובר למסך אחר.
כשתהליך נכנס למצב שמור במטמון (IMPORTANCE_CACHED) בלי רכיבים פעילים, הפלטפורמה משחררת את תקציב הזיכרון הפעיל שלו ומנהלת את הזיכרון שלו באמצעות החזרה רגילה של תהליכים שנשמרו במטמון.
בדיקת מצב התהליך והתקציב של האפליקציה
אחת הדרכים לבדוק את המצב והחשיבות של התהליך הפעיל של האפליקציה במהלך הפיתוח היא לשלוח שאילתה ל-Activity Manager באמצעות ADB:
adb shell dumpsys activity processes <package-name>
בדוגמה המקוצרת הבאה מוצגות רשומות של תיעוד התהליך ושל בקרת ה-OOM של אפליקציה שמפעילה שירות בחזית:
ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)
All known processes:
*APP* UID 10123 ProcessRecord{edf056c 3919:com.example.app/u0a123}
pid=3919
oom adj: max=1001 curRaw=200 setRaw=200 cur=200 set=200
curProcState=4 mRepProcState=4 setProcState=4 lastStateTime=-42s360ms
hasStartedServices=true
mHasForegroundServices=true forcingToImportant=null
...
Process OOM control (48 total):
Proc #22: prcp F/S/FGS ---NFU-TI t: 0 3919:com.example.app/u0a123 (fg-service)
oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
state: cur=FGS set=FGS lastRss=0.00 lastCachedRss=0.00
בפלט הזה:
-
curProcState=4ו-state: cur=FGSמשקפים שהתהליך נמצא במצב של שירות שפועל בחזית. אם האפליקציה מארחת פעילות גלויה ופעילה, היא תופיע בתורTOP. אם פועל בה רכיב חשוב בחזית (כמו הורדה או סנכרון), היא מופיעה בתורIMPF. - בטבלת הבקרה של Process OOM, הערך
prcpמציין שהתהליך נבדק ברמת העדיפות של perceptible, שתואמת לתקציבperceptible.
כדי לבדוק את מגבלת הזיכרון שנאכפת כרגע ואת השימוש בזיכרון הפיזי:
adb shell dumpsys meminfo <package-name>
החל מ-Android 17 QPR2, הפלט הזה כולל את הקטע Memory Budget, שבו מוצגים התקרה של המגבלה הפעילה, מקור המגבלה (כמו AndroidManifest או MemoryBudgetManager) והזיכרון הנוכחי של התושב.
אפליקציות עם כמה תהליכים
אם האפליקציה מחלקת את העבודה שלה בין כמה תהליכים, צריך להגדיר תקציבי תהליכים ייעודיים באמצעות התג <process> בתוך <processes>.
לדוגמה, נניח שיש לכם אפליקציה לסטרימינג של מוזיקה (com.example.radio):
- התהליך הראשי: מארח את ממשק המשתמש הגלוי ואת מנוע הפעלת האודיו (
MediaSessionServiceעם שירותmediaPlaybackשפועל בחזית). כשהתהליך גלוי, הוא פועל במסגרת תקציב של 180MB בחזית. כשהמשתמש יוצא מהאפליקציה בזמן שהמוזיקה ממשיכה לפעול, התהליך נכנס למצבperceptible, שבו תקציב של 64MB מספיק למנוע ההפעלה ולמאגר הנתונים הזמני של האודיו. - תהליך סנכרון (
:sync): תהליך ייעודי שמפעיל סנכרון מטא-נתונים ברקע והוספה לאינדקס של הורדות. התהליך הזה פועל רק ברקע, ולכן לא צריך להצהיר במפורש עלstate="background", וחל תקציב אחד.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.radio">
<application
android:label="@string/app_name">
<!-- Package baseline: main process with UI and audio playback -->
<memory-budget android:maxMb="180" />
<!-- Tighter budget when audio plays in the background -->
<memory-budget
android:maxMb="64"
android:state="perceptible" />
<processes>
<!-- Dedicated background sync process -->
<process android:process=":sync">
<memory-budget android:maxMb="32" />
</process>
</processes>
<service
android:name=".playback.AudioPlayerService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
<service
android:name=".sync.PlaylistSyncService"
android:process=":sync"
android:exported="false" />
</application>
</manifest>
כשמצהירים על בלוק <processes> במניפסט, צריך לציין רשומות <process> רק לתהליכים שמצהירים על <memory-budget>. אם רוצים להקצות תקציב רק לתהליכי משנה ספציפיים ולהשאיר את שאר האפליקציה במגבלת המערכת של המכשיר, אפשר להשמיט את הרכיבים <memory-budget> ישירות מתחת ל-<application>.
השימוש בזיכרון בכל תהליך משנה נספר גם בתקציב התהליך וגם בתקציב החבילה שמכילה אותו. תהליך חווה עומס על הזיכרון בזמן ריצה אם הוא חורג מתקציב התהליך או מתקציב החבילה, לפי הסף שאליו מגיעים קודם.
התאמת התקציבים למסכים עם דחיסות גבוהה של פיקסלים
באפליקציות שבהן הזיכרון שבשימוש משתנה באופן משמעותי בהתאם למספר הפיקסלים שצריך לצייר בתצוגה בבת אחת – כמו אפליקציית גלריית תמונות ששומרת במטמון מפות סיביות בגודל המסך – Android מספקת שני מנגנונים חלופיים לשינוי דינמי של התקציבים בהתאם למפרט התצוגה:
התאמה לפי קטגוריית צפיפות התצוגה (
android:additionalMbPerDensity): הוספת מגה-בייטים באופן יחסי לריבוע של יחס הצפיפות של התצוגה בהשוואה ל-mdpi(1.0x / 160 dpi), כי נכסי ממשק המשתמש ורכיבי ה-drawable של רסטר דו-ממדי מותאמים גם לרוחב וגם לגובה:<!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 --> <memory-budget android:maxMb="180" android:additionalMbPerDensity="16" />במסך
mdpi(1.0x), התקציב הוא 180 + 16 × 12 = 196MB. במסךxxhdpi(3.0x), התקציב גדל ל-180 + 16 × 32 = 324MB.שינוי קנה מידה לפי רזולוציית התצוגה הפיזית (
android:additionalBytesPerDisplayPixel): הוספת בייטים ישירות לכל פיקסל בתצוגה הפיזית (רוחב × גובה). האפשרות הזו מתאימה לאפליקציות שמקצות משטחים גרפיים במסך מלא, מאגרי נתונים זמניים לעיבוד או מטמונים של תמונות ברזולוציה מלאה, שבהם צריכת הזיכרון משתנה ישירות בהתאם למספר הפיקסלים הגולמיים בתצוגה ולא בהתאם לקטגוריות של צפיפות ממשק המשתמש:<!-- Baseline 128MB + 16 bytes per physical display pixel --> <!-- For example, a 4-byte RGBA full-screen buffer with double or quadruple buffering --> <memory-budget android:maxMb="128" android:additionalBytesPerDisplayPixel="16" />במסך 1080p (1080 × 2400 ≈ 2.59M פיקסלים), זה מוסיף ≈ 41.4MB לתקציב הבסיסי. במסך 1440p (1440 × 3120, בערך 4.49 מיליון פיקסלים), זה מוסיף בערך 71.8MB.
שני המאפיינים האלה הם חלופות. בחרו את המאפיין שתואם לגורם העיקרי לקביעת קנה המידה של האפליקציה, ואל תשלבו את שניהם באותו סעיף.
התאמה לגורמי צורה של מכשירים
כשמפיצים קובץ APK לטלפונים, לטאבלטים ול-Wear OS, משתמשים במאפיין android:feature כדי להתאים את התקציבים ליעדי חומרה שונים.
בשעוני Wear OS, זיכרון ה-RAM מוגבל, וממשק המשתמש וקבוצת התכונות של האפליקציה פשוטים בהרבה. אתם יכולים להגדיר תקציב מצומצם יותר שמתאים לתכונה watch:
<!-- General phone and tablet baseline -->
<memory-budget android:maxMb="180" />
<!-- Wear OS override: simpler UI and constrained hardware -->
<memory-budget
android:maxMb="48"
android:feature="watch" />
כלל פתרון: הסעיף הרלוונטי האחרון נכנס לתוקף
כשמגדירים כמה רכיבי <memory-budget> לאפליקציה או לתהליך, המערכת מעריכה אותם לפי הסדר שבו הם מוצהרים במניפסט. סעיף התקציב הרלוונטי האחרון הוא הסעיף שנאכף.
התקציב הרלוונטי האחרון הוא זה שקובע, ולכן הסדר חשוב. תמיד צריך להציב את התקציב הבסיסי הכללי ביותר בהתחלה, ואחריו את ההחרגות הספציפיות יותר (כמו סעיפים ספציפיים למדינה או לחומרה).
מסמך עזר בנושא מאפייני XML
כל המאפיינים של גודל הזיכרון מבוטאים במגה-בייט (MB) וממופים ל-Linux cgroup memory.current charge (לא כולל זיכרון משותף כמו Zygote).
| מאפיין | פורמט | ברירת מחדל | תיאור |
|---|---|---|---|
android:maxMb |
מספר שלם (> 0) | חובה | מגבלת התקציב הבסיסית של הזיכרון התושב ב-MB. |
android:state |
טיפוסים בני מנייה (enum) | כל צבע | מצב התהליך שהתקציב הזה חל עליו: foreground, perceptible או background. אם משמיטים את הסעיף, הוא משמש כברירת מחדל לכל מצב שלא צוין. |
android:additionalMbPerDensity |
מספר שלם (≥ 0) | 0 |
מגה-בייטים נוספים כפול הריבוע של יחס צפיפות התצוגה ביחס ל-mdpi (1.0x). |
android:additionalBytesPerDisplayPixel |
מספר שלם (≥ 0) | 0 |
בייטים נוספים שהוקצו לכל פיקסל במסך הפיזי (רוחב × גובה), שימושי למאגרי נתונים זמניים (buffers) של משטחים ולמפות סיביות (bitmaps). |
android:feature |
מחרוזת | כל צבע | הגבלת הסעיף למכשירים שמצהירים על תכונות חומרה ספציפיות: watch, automotive או leanback. |
ממשקי API של זמן ריצה (אפשרות דינמית משנית)
הצהרה על תקציבים באופן סטטי ב-AndroidManifest.xml היא הפתרון המועדף כמעט לכל האפליקציות. עם זאת, באפליקציות עם עומסי עבודה דינמיים או לניסויים בזמן ריצה, Android מספקת ממשקי API של SDK ו-NDK בזמן ריצה כאפשרות משנית.
באמצעות ה-API של זמן הריצה אפשר:
- שאילתה לגבי השימוש הנוכחי בזיכרון והתקציבים בפועל.
- התאמה דינמית של תקציב התהליך כלפי מטה.
- האזינו לאירועים של חריגה מהתקציב כדי לצמצם באופן יזום את המטמונים לפני שמערכת ההפעלה מפעילה החזרה ישירה.
Android SDK API (MemoryBudgetManager)
שירות המערכת MemoryBudgetManager זמין לאפליקציות שכתובות ב-Kotlin וב-Java החל מ-Android 17 QPR2 (גרסת SDK משנית, רמת API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).
אחזור השירות
לפני שאתם ניגשים אל MemoryBudgetManager, ודאו שבמכשיר פועלת גרסת Android 17 QPR2 ואילך. כדי לעשות את זה, צריך לבדוק את SDK_INT_FULL. אם minSdk של האפליקציה נמוך מ-36, קודם צריך לבדוק את SDK_INT כדי שמכשירים עם Android בגרסה 15 (רמת API 35) או גרסאות קודמות לא יזרקו את NoSuchFieldError בזמן הקריאה של SDK_INT_FULL:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2
) {
val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}
שימוש בשאילתות ותקציבים
// Query current memory charged to this process and the package UID
val processUsageBytes = budgetManager.processCurrentUsageBytes
val packageUsageBytes = budgetManager.packageCurrentUsageBytes
// Query effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes
הגדרה או ניקוי של תקציבים באופן דינמי
אתם יכולים להגדיר תקציב מצומצם יותר בזמן הריצה כדי להגביל את הזיכרון במהלך משימות קלות, או לנקות אותו כשהמשימה מסתיימת:
// Set a tighter dynamic budget on the current process (e.g., 96 MB)
try {
budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
// Thrown if the budget is <= 0 or exceeds the manifest ceiling or system limit
Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}
// Clear the dynamic process budget to restore the manifest limit (or unconstrained baseline)
budgetManager.clearProcessBudget()
בניגוד לסעיפים במניפסט של <memory-budget>, שמעבירים תקציבים באופן אוטומטי בכל פעם שהתהליך עובר בין המצבים foreground, perceptible ו-background, תקציב דינמי שמוגדר באמצעות MemoryBudgetManager מחיל תקרה אחת עד שהקוד מעדכן או מנקה אותו. אם אתם משתמשים ב-API של זמן הריצה כדי להריץ ניסויים בשטח או לנהל מגבלות ספציפיות למצב, אתם צריכים לעדכן או לנקות את התקציב הדינמי במעברים של מחזור החיים (לדוגמה, באמצעות ProcessLifecycleOwner או קריאות חוזרות במחזור חיים של פעילות, כמו שמוצג בדוגמה של עורך התמונות הדינמי).
ביצועים ואידמפוטנטיות
כשעובדים עם MemoryBudgetManager בזמן ריצה, חשוב לזכור את המאפיינים הבאים:
- אידמפוטנטיות: כל ממשקי ה-API לשינוי תקציבים הם אידמפוטנטיים. קריאה חוזרת ל-
clearProcessBudget()או ל-clearPackageBudget(), או קריאה להם כשאין תקציב דינמי פעיל, היא פעולה בטוחה שלא עושה כלום. באופן דומה, הגדרתsetProcessBudgetBytes()אוsetPackageBudgetBytes()לאותו ערך ברצף לא מפעילה הגדרות מערכת מיותרות. - תדירות הקריאות: הגדרה, עדכון או ניקוי של תקציב יוצרים קריאה חוצת-תהליכים לשירות המערכת כדי לעדכן את בקר הזיכרון של מערכת ההפעלה בשביל התהליך. הוא קל, אבל לא לגמרי חינמי. צריך להפעיל את ממשקי ה-API האלה במהלך מעברים נפרדים במחזור החיים ובאבני דרך של משימות (כמו כניסה לפעילות או יציאה ממנה, התחלה או השלמה של עיבוד ברקע או טיפול בבקשות דיבור), ולהימנע מהפעלתם בלולאות הדוקות, בשרשורים של עיבוד אודיו או בשגרות עיבוד לכל פריים.
- תדירות השאילתות: מאפייני השאילתות
processCurrentUsageBytesו-packageCurrentUsageBytesמאחזרים את השימוש בזיכרון בזמן אמת משירות המערכת. למרות שהן מהירות, צריך להפעיל שאילתות לפי דרישה (למשל במהלך מעברים בין משימות או בתוך קריאות חוזרות (callbacks) שלOnOverBudgetListener) במקום לדגום אותן בלולאות רציפות.
האזנה לקריאות חוזרות (callbacks) של לחץ על התקציב
אפליקציות יכולות לרשום מאזין כדי לקבל התראה כשהשימוש בזיכרון חורג מסף התקציב. כך האפליקציה יכולה לבצע ניקוי יזום ברמת האפליקציה (כמו ניקוי מטמונים של מפת סיביות בזיכרון) בזמן שמערכת ההפעלה מפנה זיכרון כדי לשמור על התהליך או החבילה במסגרת התקציב:
val cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
// Proactively evict thread-safe caches on the background Looper
imageTileCache.evictAll()
}
// Register on a background Looper before tightening a dynamic budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, listener)
// When done (for example, in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)
שיטות מומלצות לטיפול בבקשות חוזרות שחורגות מהתקציב:
- שימוש ברקע
Looper: מעבירים רקעHandlerThreadLooperאלregisterProcessOverBudgetListenerאוregisterPackageOverBudgetListenerבכל פעם שאפשר. הפעלת פינוי מטמון או ניקוי קבצים בשרשור ברקע מונעת תחרות עם רינדור פריימים בשרשור UI כשהתהליך נמצא תחת לחץ זיכרון. שמרו אתLooper.getMainLooperלמאזינים שנוגעים רק במצב של ממשק המשתמש שקשור ל-thread הראשי. - התראות חוזרות: התראות על חריגה מהתקציב הן לא התראות חד-פעמיות. כשהשימוש חורג מהתקציב, מערכת ההפעלה מחזירה קודם דפים קרים. אם הלחץ נמשך, המערכת ממשיכה להחזיר זיכרון ולהפעיל את ה-listener לכל היותר פעם בשנייה. חשוב לוודא שהקריאה החוזרת היא אידמפוטנטית. אם האפליקציה צריכה רק התראה אחת או אם רוצים תקופת צינון ארוכה יותר בין סבבי הניקוי, צריך להפעיל את
unregisterProcessOverBudgetListener(או אתunregisterPackageOverBudgetListener) בתוך פונקציית ה-callback, או להגן על הניקוי באמצעות חותמת זמן של תקופת צינון אוAtomicBoolean. - רישום לפני הקטנת התקציב: אם השימוש הנוכחי בזיכרון כבר חורג מהתקציב החדש כשמפעילים את
setProcessBudgetBytesאו אתsetPackageBudgetBytes, אירוע חריגה מהתקציב מופעל ברגע שהתקציב החדש מוחל. כדי שהאפליקציה לא תפספס את ההתראה הראשונית על חריגה מהתקציב, צריך לרשום את המאזין לפני שמגדירים את התקציב. - מהירות: פעולות ההחזרה צריכות להציע הקלה מיידית. חישובים מורכבים בזמן עומס פוגעים בביצועים.
- הימנעו מהקצאות: אל תקצו אובייקטים חדשים ואל תתחילו שרשורים חדשים בתוך הקריאה החוזרת, כי זה עלול להפעיל מיד החזרה ישירה של מערכת ההפעלה.
- התמקדות ביעדים עם תשואה גבוהה: פינוי של מפות סיביות (bitmap) גדולות, מאגרי נתונים זמניים (buffer) לעיבוד או סגירת קבצים שממופים בזיכרון, יעילים הרבה יותר משחרור של הרבה אובייקטים קטנים.
Native NDK API (<android/memory_budget_manager.h>)
אפליקציות נייטיב יכולות להשתמש ב-C NDK API שחשוף ל-libandroid.so החל מ-Android 17 QPR2 (רמת API 37.2).
הגדרת CMake
find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})
הכללת השימוש בכותרות ובשאילתות
#include <android/memory_budget_manager.h>
// Query current memory usage
int64_t process_usage = AMemoryBudgetManager_getProcessCurrentUsageBytes();
int64_t package_usage = AMemoryBudgetManager_getPackageCurrentUsageBytes();
// Query current budget
int64_t process_budget = 0;
AMemoryBudgetResult result = AMemoryBudgetManager_getProcessBudget(&process_budget);
if (result == AMEMORY_BUDGET_RESULT_SUCCESS) {
// Current budget available in process_budget
} else if (result == AMEMORY_BUDGET_RESULT_LIMIT_IS_DISABLED) {
// No budget is currently active
}
הגדרה דינמית של תקציב מקורי
// Set a tighter process budget (e.g. 160MB)
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(160LL * 1024 * 1024);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
const char* error_msg = AMemoryBudgetManager_resultToString(result);
// Handle error (e.g. AMEMORY_BUDGET_RESULT_ERROR_EXCEEDS_MANIFEST_LIMIT)
}
// Clear the dynamic budget to resume manifest limits (or unconstrained baseline)
AMemoryBudgetManager_clearProcessBudget();
מעקב אחרי אירועים של עומס על הזיכרון
ה-NDK מספק שתי דרכים למעקב אחרי אירועי זיכרון:
- High-Level Watcher (
AMemoryBudgetManager_Watcher_create): מעקב אחרי אירועים ב-ALooperעם ביטול אוטומטי של הקפצות. - תיאור קובץ ברמה נמוכה: הפונקציה
AMemoryBudgetManager_getProcessMemoryPressureFdמחזירה תיאור קובץ מקורי שאפשר לשלב ישירות בלולאת מנועepollבהתאמה אישית.
void onMemoryPressure(int32_t event_mask, const AMemoryBudgetEvents* events, void* userdata) {
// High-yield eviction of unused native textures or geometry caches
purgeNativeTextureCaches();
}
// Register watcher on a background worker ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
background_looper,
AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
1000 /* debounce_ms */,
&onMemoryPressure,
NULL /* userdata */
);
// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);
מקשרים את הצופה ל-ALooper של Thread עובד ברקע, במקום לממשק המשתמש הראשי או ל-Thread הרינדור, כדי שפינוי המטמון לא יתחרה ברינדור הפריימים. כדי שהאפליקציה לא תפספס את האירוע הראשוני של חריגה מהתקציב, צריך ליצור את הצופה לפני שמצמצמים את התקציב באמצעות AMemoryBudgetManager_setProcessBudget או AMemoryBudgetManager_setPackageBudget.
כל עוד התהליך או החבילה חורגים מהתקציב ומערכת ההפעלה ממשיכה לפנות זיכרון, הצופה מפעיל את הקריאה החוזרת שוב ושוב (לכל היותר פעם אחת בכל מרווח זמן של debounce_ms). הפכו את הקריאה החוזרת לאידמפוטנטית, או קראו ל-AMemoryBudgetManager_Watcher_destroy (שאפשר לקרוא לה בבטחה מתוך הקריאה החוזרת) אם האפליקציה שלכם צריכה רק התראה חד-פעמית.
דוגמאות ל-Runtime API
הדוגמאות הבאות מראות איך להטמיע את ממשקי ה-API של זמן הריצה באמצעות Android SDK API (כתוב ב-Kotlin) ו-Native NDK API (כתוב ב-C++).
דוגמה ל-Android SDK: עורך תמונות מותאם אישית
בדוגמה הזו מוצגת אפליקציה לעריכת תמונות (com.example.imageeditor) שהמניפסט שלה מצהיר על תקרה של 256MB כדי להתאים לקנבס מרובה השכבות שלה:
<manifest ... >
<application ... >
<!-- Manifest ceiling accommodates the heaviest editing workload -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
כשהמשתמש מדפדף בגלריית התמונות הממוזערות הקלה, האפליקציה משתמשת ב-Android SDK API ב-Kotlin כדי להקטין באופן דינמי את תקציב התהליך ל-96MB.
כשהמשתמש פותח את אזור העריכה של כמה שכבות, האפליקציה מנקה את התקציב הדינמי כדי לשחזר את התקרה המלאה של 256MB במניפסט. הוא גם רושם OnOverBudgetListener ב-HandlerThread ברקע, Looper לפני החלת תקציב הגלריה כדי להסיר מפות סיביות (bitmap) של תצוגה מקדימה שנשמרו במטמון, בלי לחסום את שרשור ה-UI.
package com.example.imageeditor.ui
import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.os.HandlerThread
import android.util.Log
import android.util.LruCache
class ImageEditorActivity : Activity() {
private lateinit var budgetManager: MemoryBudgetManager
private lateinit var cleanupThread: HandlerThread
// Thread-safe in-memory cache for rendered preview tiles (32MB limit)
private val previewCache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
}
private val overBudgetListener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
Log.w(TAG, "Process memory pressure detected (budget: ${budgetBytes / 1048576}MB). Evicting preview cache.")
previewCache.evictAll()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
budgetManager = getSystemService(MemoryBudgetManager::class.java)
cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
}
override fun onStart() {
super.onStart()
// Register listener on a background Looper before tightening the budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, overBudgetListener)
// Constrain memory during lightweight gallery browsing
applyGalleryBudget()
}
override fun onStop() {
super.onStop()
budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
}
override fun onDestroy() {
cleanupThread.quitSafely()
super.onDestroy()
}
/**
* Called when the user enters the high-resolution editing canvas.
* Clears the dynamic budget, restoring the full 256MB manifest ceiling.
*/
fun enterEditingCanvas() {
// Clear the tighter dynamic budget to restore the full manifest ceiling (256MB)
budgetManager.clearProcessBudget()
Log.i(TAG, "Restored manifest budget ceiling (256MB) for editing canvas")
}
/**
* Called when the user exits the editor back to the thumbnail gallery.
* Re-applies the tighter dynamic budget.
*/
fun exitToGallery() {
previewCache.trimToSize(8 * 1024 * 1024)
applyGalleryBudget()
}
private fun applyGalleryBudget() {
try {
// Dynamically tighten budget to 96MB for the lightweight gallery view
budgetManager.processBudgetBytes = 96L * 1024L * 1024L
Log.i(TAG, "Tighter dynamic budget applied for gallery: 96MB")
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Could not apply dynamic budget", e)
}
}
companion object {
private const val TAG = "ImageEditor"
}
}
דוגמה ל-NDK C++: מנוע תלת-ממדי מקורי
בדוגמה הזו מוצג מנוע משחק מקורי ב-C++ שמנהל את תקציבי הזיכרון על סמך רמת האיכות הפעילה של הגרפיקה, בהנחה שבמניפסט של האפליקציה מוגדרת תקרה של 512MB כדי להתאים לגרפיקה באיכות גבוהה (android:maxMb="512"). המנוע מפעיל את AMemoryBudgetManager_Watcher_create ב-ALooper של Thread עובד ברקע לפני שהוא מצמצם את התקציב להגדרות קבועות מראש באיכות נמוכה יותר, ופורק את ה-mipmap של הטקסטורה כשהוא חורג מהתקציב.
#include <android/memory_budget_manager.h>
#include <android/looper.h>
#include <android/log.h>
#define LOG_TAG "Native3DEngineMemory"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
class MemoryGovernor {
public:
MemoryGovernor() : mWatcher(nullptr) {}
~MemoryGovernor() {
stopMonitoring();
}
// Configures process budget based on user graphics quality settings
bool setQualityBudget(int qualityLevel) {
int64_t targetBytes = 0;
switch (qualityLevel) {
case 0: // Low (budget: 128MB)
targetBytes = 128LL * 1024 * 1024;
break;
case 1: // Medium (budget: 256MB)
targetBytes = 256LL * 1024 * 1024;
break;
case 2: // High (budget: 512MB)
targetBytes = 512LL * 1024 * 1024;
break;
default:
// Clear dynamic override and restore manifest limit
AMemoryBudgetManager_clearProcessBudget();
return true;
}
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(targetBytes);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
LOGW("Could not set quality budget: %s", AMemoryBudgetManager_resultToString(result));
return false;
}
return true;
}
// Call on a background worker ALooper before tightening the budget with setQualityBudget
bool startMonitoring(ALooper* backgroundLooper) {
if (!backgroundLooper) return false;
// Monitor process budget events, debounced to at most once every 1000ms
mWatcher = AMemoryBudgetManager_Watcher_create(
backgroundLooper,
AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
1000,
&MemoryGovernor::onPressureEvent,
this
);
return mWatcher != nullptr;
}
void stopMonitoring() {
if (mWatcher) {
AMemoryBudgetManager_Watcher_destroy(mWatcher);
mWatcher = nullptr;
}
}
void unloadUnusedTextures() {
LOGW("Memory pressure callback triggered. Purging cached texture mipmaps...");
// Fast, idempotent, high-yield eviction without allocating memory
}
private:
static void onPressureEvent(
int32_t event_mask,
const AMemoryBudgetEvents* events,
void* userdata
) {
auto* governor = static_cast<MemoryGovernor*>(userdata);
governor->unloadUnusedTextures();
}
AMemoryBudgetManagerWatcher* mWatcher;
};