ניהול הזיכרון של האפליקציה

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

זיכרון גישה אקראית (RAM) הוא משאב חשוב לכל סביבת פיתוח תוכנה, והוא חשוב עוד יותר למערכת הפעלה לנייד שבה הזיכרון הפיזי מוגבל לעיתים קרובות. למרות שגם סביבת זמן ריצה ל-Android‏ (ART) וגם מכונת Dalvik הווירטואלית מבצעות מנגנון איסוף זבל שגרתי, זה לא אומר שאפשר להתעלם מהמקרים שבהם האפליקציה מקצה ומשחררת זיכרון. עדיין צריך להימנע מיצירת דליפות זיכרון – בדרך כלל הן נגרמות כתוצאה משמירה של הפניות לאובייקטים במשתני חברים סטטיים – ולשחרר אובייקטים מסוג Reference בזמן המתאים, כפי שמוגדר בפונקציות הקריאה החוזרות של מחזור החיים.

הקטנת טביעת הרגל של הקוד והמשאבים של האפליקציה

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

הקטנת הגודל הכולל של האפליקציה באמצעות הפעלת R8

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

אפשר להשתמש ב-R8 כדי לצמצם את הזיכרון שבשימוש של האפליקציה. למרות ש-R8 ידוע בעיקר בהקטנת הגודל של קובץ ה-APK, יש לו השפעה ישירה וחיובית על זיכרון זמן הריצה (RAM). ‫R8 מנתח את קוד הבייט של האפליקציה כדי להסיר קוד מת, למזג מחלקות מיותרות, להטמיע שיטות ולהקטין מזהים. טעינה של פחות בייטקוד שעבר קומפילציה מ-APK ל-RAM, מפחיתה את הזיכרון שבשימוש הכולל של האפליקציה. בנוסף, צמצום שמות של מחלקות, שיטות ושדות למזהים קצרים יותר מפחית ישירות את התקורה של ה-RAM. אופטימיזציות כמו מיזוג מחלקות והטמעה נרחבת של שיטות מחליפות גם חיפושים יקרים בזמן ריצה ודפוסי הקצאה, וכתוצאה מכך מתקבלת אופטימיזציה של זיכרון הערימה והמחסנית.

הסבר על כללי השמירה

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

כללי שמירה שנכתבו בצורה לא טובה מונעים מ-R8 לבצע אופטימיזציה של חלקים גדולים בבסיס הקוד. מומלץ להימנע מכללי שמירה רחבים מדי ולפעול לפי השיטות המומלצות הבאות:

  • כללים גלובליים שכדאי להימנע מהם:
    • -dontoptimize: משבית לחלוטין את האופטימיזציה של כל האפליקציה, וכתוצאה מכך נוצרים קבצי הפעלה גדולים יותר ואיטיים יותר.
    • -dontshrink: מונעת את ההסרה של קוד ומשאבים שלא נעשה בהם שימוש.
    • -dontobfuscate: מונעת מזעור של שמות, וכך מונעת חיסכון משמעותי בזיכרון (במיוחד באפליקציות גדולות).
  • אל תשתמשו בתווים כלליים לחיפוש בכל החבילה: כללים רחבים כמו -keep class com.example.package.** { *; } מאלצים את R8 לשמור כל מחלקה, שדה ושיטה בחבילה הזו. הפעולה הזו מפסיקה לחלוטין את היכולת של R8 להסיר, לבצע אופטימיזציה או לכווץ קוד בחבילה הזו.

  • שימוש בקובץ התצורה של R8 שמוגדר כברירת מחדל: תמיד משתמשים ב-proguard-android-optimize.txt.

מידע נוסף על כתיבת כללי שמירה מופיע במאמר סקירה כללית על כללי שמירה. במאמר שיטות מומלצות לשימוש בכללי שמירה מפורטים דפוסים ספציפיים שכדאי להשתמש בהם ודפוסים שכדאי להימנע מהם.

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

צריך להיזהר כשמשתמשים בספריות חיצוניות

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

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

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

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

שימוש ב-Hilt או ב-Dagger 2 להחדרת תלות

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

אם אתם מתכוונים להשתמש ב-framework להזרקת תלות באפליקציה, כדאי להשתמש ב-Hilt או ב-Dagger. ‫Hilt היא ספרייה להזרקת תלות (dependency injection) ל-Android שפועלת על Dagger. ‫Dagger לא משתמש בהשתקפות כדי לסרוק את הקוד של האפליקציה. אתם יכולים להשתמש בהטמעה סטטית של Dagger בזמן קומפילציה באפליקציות ל-Android בלי עלויות מיותרות בזמן ריצה או שימוש בזיכרון.

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

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

הקפידו על טעינת תמונות

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

לדוגמה, ברוב מפות הביטים נעשה שימוש בהגדרה ARGB_8888, כלומר כל פיקסל דורש 4 בייטים של זיכרון – בייט אחד לכל אחד מהצבעים אדום, ירוק וכחול, ובייט אחד לאלפא (שקיפות). אם יש לכם תמונה בפורמט JPEG בגודל 100KB ואתם מציגים אותה בתצוגה של 1, 000x1,000 פיקסלים,מפת הביטים תדרוש 4 בייטים לכל אחד ממיליון הפיקסלים האלה, כלומר 4MB של זיכרון.

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

מעקב אחרי הזיכרון הזמין והשימוש בזיכרון

כדי לפתור את הבעיות בשימוש בזיכרון של האפליקציה, קודם צריך למצוא אותן. כלי פרופיל הזיכרון ב-Android Studio עוזר לכם למצוא ולאבחן בעיות בזיכרון בדרכים הבאות:

כלי פרופיל הזיכרון משולב גם עם ספריית זיהוי הדליפות LeakCanary. בעזרת LeakCanary, אפשר להעביר את ניתוח דליפת הזיכרון ממכשיר הבדיקה למחשב הפיתוח, וכך להאיץ משמעותית את תהליך העבודה. מידע נוסף זמין בנתוני הגרסה של Android Studio.

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

שחרור זיכרון בתגובה לאירועים

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

ההטמעה של onTrimMemory() צריכה להתמקד אך ורק באירועים TRIM_MEMORY_UI_HIDDEN ו-TRIM_MEMORY_BACKGROUND. (החל מ-Android 14, המערכת לא שולחת יותר התראות לגבי הקבועים האחרים מדור קודם. הוצאנו משימוש את הקבועים האלה באופן רשמי ב-Android 15).

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

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

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

Kotlin

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

Java

import android.content.ComponentCallbacks2;
// Other import statements.

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    public void onTrimMemory(int level) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

בדיקה של נפח הזיכרון שצריך

כדי לאפשר הפעלה של כמה תהליכים, מערכת Android מגדירה מגבלה קשיחה על גודל הערימה שמוקצה לכל אפליקציה. מגבלת הגודל המדויקת של הערימה משתנה בין מכשירים, בהתאם לכמות ה-RAM שזמינה במכשיר באופן כללי. אם האפליקציה מגיעה לקיבולת של ה-heap ומנסה להקצות עוד זיכרון, המערכת זורקת OutOfMemoryError.

כדי להימנע ממצב של אין זיכרון פנוי (OOM), אפשר לשלוח שאילתה למערכת כדי לקבוע כמה שטח Heap זמין במכשיר הנוכחי. כדי להריץ שאילתה במערכת לגבי הנתון הזה, מפעילים את השיטה getMemoryInfo(). הפונקציה מחזירה אובייקט ActivityManager.MemoryInfo שמספק מידע על סטטוס הזיכרון הנוכחי של המכשיר, כולל הזיכרון הזמין, הזיכרון הכולל וסף הזיכרון – רמת הזיכרון שבה המערכת מתחילה להפסיק תהליכים. האובייקט ActivityManager.MemoryInfo חושף גם את lowMemory, שהוא ערך בוליאני פשוט שמציין אם הזיכרון במכשיר עומד להיגמר.

קטע הקוד הבא מראה איך להשתמש בשיטה getMemoryInfo() באפליקציה.

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

ניטור של תהליכים שנסגרים בגלל זיכרון נמוך

אירועי LMK (הפסקת תהליכים בגלל מחסור בזיכרון) שגלויים למשתמשים מתרחשים כשהזיכרון של המערכת נמוך באופן קריטי. כשזיכרון המכשיר נמוך, lmkd (הדמון low memory killer) מסיים תהליכים על סמך oom_adj_score שלהם. אפליקציות שנשמרות במטמון או שמריצות שירות ללא ממשק משתמש משויך (כמו עבודה), מקבלות את הניקוד הכי גבוה ומסתיימות ראשונות. אם הזיכרון נשאר נמוך מאוד, הדמון נאלץ להקצות מחדש זיכרון מתהליכים עם oom_adj_score של 0. הניקוד הזה שמור לאפליקציות גלויות, ולכן סיום הפעולה שלהן מוביל ליציאה מיידית ולא מסודרת מהתהליך. למשתמש הקצה, נראה שהאפליקציה קרסה, ולרוב היא עוקפת את המנגנונים הרגילים לשמירת מצב מחזור החיים וגורמת לאובדן ההתקדמות של המשתמש.

התמקדות מרכזית ב-Android Vitals היא בהפסקת פעולה של תהליכים בחזית, כי הם משמשים כנציג נאמן לניהול לא תקין של הזיכרון. שיעור LMK גבוה מ-1% מצביע על צורך קריטי בפעולה מיידית, אבל שיעור נמוך לא מצביע בהכרח על תקינות. שיעור נמוך של אירועי LMK שמשפיעים על המשתמשים יכול להיות סימן לכך שתהליך ה-daemon של LMK מפסיק תהליכים לעיתים קרובות כשהם פועלים ברקע. כתוצאה מכך, הביצועים של הפעלה מתוך הזיכרון (warm start) יורדים והמעבר בין משימות לא חלק. לכן, מומלץ לפעול לפי השיטות המומלצות לניהול הזיכרון, בלי קשר לציון ה-LMK הנוכחי, כדי להבטיח יציבות לטווח ארוך ותקינות המכשיר.

שימוש ב-ProfilingManager כדי לעקוב אחרי בעיות בזיכרון

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

שני טריגרים חדשים שהושקו עם Android 17 שימושיים במיוחד לזיהוי בעיות בזיכרון:

  • TRIGGER_TYPE_OOM מציין שהאפליקציה הציגה OutOfMemoryError. הוא מופעל בפעם הבאה שהאפליקציה מופעלת אחרי הקריסה, כשהאפליקציה נרשמת להפעלת פרופילים.
  • TRIGGER_TYPE_ANOMALY מופעל כשהמערכת מזהה התנהגות חריגה באפליקציה. בין היתר, הוא יכול להיות מופעל בגלל שימוש מוגזם בזיכרון. ההתראה מופעלת אחרי שהאפליקציה הציגה שימוש מוגזם בזיכרון, ולפני שהמערכת מבצעת פעולה כלשהי כדי לעצור את התהליך הבעייתי. לדוגמה, אם האפליקציה חורגת ממגבלות הזיכרון שהוצגו ב-Android 17,‏ TRIGGER_TYPE_ANOMALY מופעל לפני שהמערכת סוגרת את האפליקציה.

מידע נוסף על שימוש ב-ProfilingManager כדי לרשום ולאחזר טריגרים באופן פרוגרמטי מופיע במאמר בנושא יצירת פרופילים מבוססי-טריגרים. ‫Firebase Crashlytics תומך גם בטריגרים TRIGGER_TYPE_OOM ו-TRIGGER_TYPE_ANOMALY, ומשייך אותם למטא-נתונים אחרים של אבחון.

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

מעקב אחרי מצב התהליך והזיכרון

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

עם לוח בקרה של Python לניטור רציף

אפשר להשתמש בסקריפט Python של Android Memory Monitor בזמן שהאפליקציה פועלת כדי לקבל תצוגה של מעקב רציף אחרי התהליכים של האפליקציה והזיכרון שבשימוש שלה. זהו כלי של שורת פקודה שמשתמש בפקודות adb כדי להציג לוחות בקרה של מסוף ואינטרנט, שיעזרו לכם להבין את המצב ואת השימוש בזיכרון של התהליכים באפליקציה. כדי להפעיל את לוחות הבקרה, מריצים את הסקריפט עם שם החבילה של האפליקציה, למשל:

python3 android_mem_monitor.py your.package.name

הוראות מפורטות על התכונות של לוח הבקרה ועל השימוש בו מופיעות בקובץ ה-README או בתגובות בשורות של הסקריפט.

בזמן הריצה עם ActivityManager

כדי לאחזר מידע על התהליכים הפועלים בזמן הריצה, אפשר לשלוח שאילתה אל ActivityManager.getRunningAppProcesses(). הפעולה הזו מחזירה רשימה של אובייקטים שמכילים מידע בסיסי על כל אחד מהתהליכים של האפליקציה. השדה importance מציין את החשיבות היחסית שהמערכת מייחסת לתהליך, והוא ממופה לקבועים כמו:

  • IMPORTANCE_FOREGROUND: התהליך פועל באופן פעיל בממשק המשתמש של האפליקציה.
  • IMPORTANCE_FOREGROUND_SERVICE: התהליך מפעיל שירות בחזית (כמו מוזיקת רקע פעילה או הורדות גדולות), שזהו דוגמה לשירות שהמשתמש יכול לראות.
  • IMPORTANCE_CACHED: התהליך הזה מכיל קוד ששמור במטמון. התהליך מוקפא והזיכרון שלא בשימוש מפונה על ידי Android. במצב הזה, מערכת ההפעלה MemoryLimiter לא תפסיק את האפליקציה אם היא תחרוג מסף הזיכרון הספציפי שלה. עם זאת, אם הזיכרון של המערכת כולה עומד להיגמר, האפליקציה היא מועמדת מובילה לחיסול על ידי התהליך לחיסול אפליקציות שצורכות הרבה זיכרון.

לניפוי באגים באמצעות adb

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

  • מציאת מזהי תהליכים (PID) פעילים: קודם כול, מוצאים את כל התהליכים שהאפליקציה מריצה כרגע באמצעות הפקודה ps שסוננה לפי שם החבילה.
    shell adb shell ps -A | grep your.package.name
  • בדיקת הניקוד הנוכחי של OOM: משתמשים ב-PID של תהליך כדי לקרוא את הניקוד של OOM ממערכת הקבצים של התהליך. ציון של 0 עד 199 בדרך כלל מציין אפליקציה שפועלת בחזית, ציון של 200 עד 249 מציין שירות מורגש, ציון של 250 עד 899 מציין שירות שפועל ברקע, וציון של 900 ומעלה מציין שהתהליך נמצא במטמון וסביר להניח שהוא יופסק.
    shell adb shell cat /proc/pid/oom_score_adj
  • מעקב רציף אחרי ציוני OOM: כדי לעקוב באופן רציף אחרי שינויים בציוני OOM של אפליקציה (הציון הנמוך ביותר מבין כל התהליכים שלה), אפשר להשתמש בפקודה watch-uids.
    shell adb shell am watch-uids
  • מדידת טביעות רגל בזיכרון: כדי לקבל קריאה מהירה של טביעת הרגל בזיכרון של תהליך, כמו שימוש בזיכרון RSS אנונימי ובזיכרון החלפה, קוראים את הסטטוס שלו ישירות ממערכת הקבצים של תהליך לינוקס.
    shell adb shell cat /proc/pid/status | grep -E "VmRSS|RssAnon|VmSwap"
    כדי לקבל פירוט מפורט ומקיף יותר של הקצאות הזיכרון, משתמשים בפקודה dumpsys meminfo:
    shell adb shell dumpsys meminfo pid
  • בדיקת רכיבים פעילים: אם התהליך שומר על עדיפות גבוהה באופן לא צפוי (למשל, אם הוא נשאר בציון OOM של 200 במקום לרדת לציון במטמון), אפשר לבדוק בדיוק מה התהליך מריץ. הפקודה dumpsys activity processes חושפת את הפעילויות, השירותים, הספקים והמקבלים הספציפיים ששומרים על התהליך פעיל.
    shell adb shell dumpsys activity processes your.package.name

שימוש במבני קוד יעילים יותר מבחינת זיכרון

חלק מהתכונות של Android, מחלקות Java ומבני קוד משתמשים ביותר זיכרון מאחרים. כדי לצמצם את כמות הזיכרון שהאפליקציה צורכת, אפשר לבחור חלופות יעילות יותר בקוד.

שימוש בשירותים בצורה מוגבלת

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

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

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

שימוש במאגרי נתונים שעברו אופטימיזציה

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

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

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

חשוב להיזהר מהפשטות של קוד

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

שימוש ב-protobufs קלים לנתונים שעברו סריאליזציה

Protocol buffers (מאגרי פרוטוקולים) הם מנגנון שפותח על ידי Google לסריאליזציה של נתונים מובְנים. הם לא תלויים בשפה או בפלטפורמה, והם ניתנים להרחבה. הם דומים ל-XML, אבל קטנים יותר, מהירים יותר ופשוטים יותר. אם אתם משתמשים ב-protobufs לנתונים שלכם, תמיד תשתמשו ב-lite protobufs בקוד בצד הלקוח. פרוטוקולי protobuf רגילים יוצרים קוד מפורט מאוד, מה שמגדיל את טביעת הרגל של קוד האפליקציה ב-RAM (ראו הקטנת טביעת הרגל של קוד האפליקציה והמשאבים) ותורם להגדלת גודל ה-APK.

מידע נוסף זמין בקובץ ה-README של protobuf.

היזהרו מדליפות זיכרון

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

איך להימנע מהעמסת זיכרון

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

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

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

כדי לזהות את המקומות בקוד שבהם יש העמסת זיכרון גבוהה לפני שמתקנים אותם, אפשר להשתמש בMemory Profiler.

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

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

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

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