הגדרת תקציבי זיכרון לאפליקציות

תקציבי זיכרון לאפליקציות מאפשרים לאפליקציות להצהיר על תקציב זיכרון משלהן, מה שאומר למערכת לצמצם את השימוש בזיכרון כשהאפליקציה משתמשת ביותר מהתקציב שהוגדר לה. האפשרות הזו שימושית במיוחד לאפליקציות מערכת ואפליקציות בחבילה, או לאפליקציות שמטרגטות מכשירים עם מגבלות זיכרון, שבהן המפתח יודע מהי קבוצת העבודה הצפויה בזיכרון (הזיכרון שהאפליקציה צריכה באופן פעיל לפעולה שהמשתמש מבצע כרגע) ורוצה לוודא שהאפליקציה לא משתמשת ביותר מדי ממשאבי ה-RAM המשותפים של המערכת.

כדי לשמור על איזון בתקציב, המערכת משתמשת בפינוי זיכרון ובהחלפה כדי להסיר דפי זיכרון שלא היו בשימוש לאחרונה, וכך מתמקדת בזיכרון שבשימוש של האפליקציה בקבוצת העבודה הנוכחית שלה. כשאפליקציה חורגת מהתקציב שהוקצה לה, מערכת ההפעלה מנסה להחזיר את הזיכרון שהוקצה לאפליקציה הזו:

  1. דפים נקיים שמגובים בקבצים (כמו קוד לא פעיל ונכסים ממופים) מפונים קודם, כי אפשר לקרוא אותם מחדש מהאחסון במקרה הצורך.
  2. דפים מלוכלכים שמגובים בקבצים נכתבים בחזרה לאחסון ומוסרים.
  3. דפי זיכרון אנונימיים (כמו הקצאות זיכרון בערימה) נדחסים ומוחלפים ב-zRAM.

כל עוד קבוצת העבודה לא חורגת מהתקציב, האפליקציה תפעל היטב בלי להשתמש ביותר זיכרון מהתקציב שהוגדר לה. מערכת ההפעלה מפנה זיכרון שלא בשימוש ודוחסת דפי ערימה לא פעילים להחלפה, כדי שהקצאות הזיכרון יישארו מוגבלות בלי לסיים את התהליך.

מה קורה כשאפליקציה חורגת מהתקציב שלה

חריגה מתקציב הזיכרון לא גורמת למערכת לסגור את האפליקציה או לקרוס. במקום זאת, כשאפליקציה משתמשת ביותר זיכרון ממה שהתקציב שלה מאפשר, מערכת ההפעלה מוצאת זיכרון שהאפליקציה לא השתמשה בו במשך זמן מה ומאחסנת אותו באופן בטוח עד שיהיה בו צורך שוב. כך אפשר להקצות זיכרון חדש לאפליקציה ולשמור על השימוש הכולל בזיכרון של האפליקציה במסגרת התקציב.

כל עוד יש מרווח בין מה שהאפליקציה צריכה בכל זמן נתון (קבוצת העבודה שלה) לבין התקציב שהוגדר, האפליקציה ממשיכה לפעול כרגיל. אם אפליקציה מגדירה תקציב קטן יותר מקבוצת העבודה שלה, הביצועים של האפליקציה בזמן הריצה עלולים להיות איטיים יותר כתוצאה מכך.

במדריך לארכיטקטורת הזיכרון מוסבר איך מערכת ההפעלה עוקבת אחרי הזיכרון ומחזירה אותו:

הצהרה על תקציבים במניפסט של 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_FOREGROUND
IMPORTANCE_TOP_SLEEPING
פעילות גלויה שהופעלה מחדש, אפליקציה מובילה עם מסך נעול, שירות או ספק לניהול גישה שקשורים לאפליקציה המובילה
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
הפעלה פעילה של מדיה, ניווט, הורדות או סנכרון של שירותים שפועלים בחזית; שירות שקשור לשירות שפועל בחזית או לדגלים גלויים
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_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):

  1. התהליך הראשי: מארח את ממשק המשתמש הגלוי ואת מנוע הפעלת האודיו (MediaSessionService עם שירות mediaPlayback שפועל בחזית). כשהתהליך גלוי, הוא פועל במסגרת תקציב של 180MB בחזית. כשהמשתמש יוצא מהאפליקציה בזמן שהמוזיקה ממשיכה לפעול, התהליך נכנס למצב perceptible, שבו תקציב של 64MB מספיק למנוע ההפעלה ולמאגר הנתונים הזמני של האודיו.
  2. תהליך סנכרון (: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 &approx; 2.59M פיקסלים), זה מוסיף &approx; 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: מעבירים רקע HandlerThread Looper אל 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 מספק שתי דרכים למעקב אחרי אירועי זיכרון:

  1. ‫High-Level Watcher (AMemoryBudgetManager_Watcher_create): מעקב אחרי אירועים ב-ALooper עם ביטול אוטומטי של הקפצות.
  2. תיאור קובץ ברמה נמוכה: הפונקציה 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;
};