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

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

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

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

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

  • איך האפליקציה מקצה זיכרון לאורך זמן. ב-Memory Profiler מוצג תרשים בזמן אמת של נפח הזיכרון שבו האפליקציה משתמשת, מספר אובייקטי Java שהוקצו ומועד האיסוף של האשפה.
  • הפעלת אירועי איסוף אשפה וצילום קובץ snapshot של אשכול Java בזמן שהאפליקציה פועלת.
  • תיעוד הקצאות הזיכרון של האפליקציה, בדיקת כל האובייקטים שהוקצו, צפייה בדוח הקריסות של על כל הקצאה, ומעבר לקוד המתאים בעורך Android Studio.

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

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

תוכלו להטמיע את הקריאה החוזרת (callback) של 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 מגדירה מגבלה קבועה על גודל האוסף (heap) שהוקצה לכל אפליקציה. מגבלת הגודל המדויקת של האוסף משתנה בין מכשירים בהתאם לכמות ה-RAM הזמינה במכשיר באופן כללי. אם האפליקציה מגיעה לקיבולת של אשכול ומנסה להקצות יותר זיכרון, המערכת תשליך OutOfMemoryError.

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

קטע הקוד לדוגמה הבא מראה איך להשתמש ב-method 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;
}

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

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

שימוש מצומצם בשירותים

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

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

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

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

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

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

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

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

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

שימוש ב-lite protobufs לנתונים סריאליים

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

מידע נוסף זמין במאמר פרוטובוף Readme.

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

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

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

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

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

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

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

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

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

הסרת משאבים וספריות שצורכים הרבה זיכרון

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

הקטנת הגודל הכולל של ה-APK

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

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

שימוש ב-Hilt או ב-Dagger 2 להזרקת יחסי תלות

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

אם אתם מתכננים להשתמש באפליקציה שלכם ב-Dependency Injection Framework, כדאי לכם להשתמש ב-Hilt או ב-Dagger. Hilt היא ספרייה להזרקת תלויות ל-Android שפועלת על גבי Dagger. Dagger לא משתמש בהשתקפות (reflection) כדי לסרוק את הקוד של האפליקציה. ניתן להשתמש בהטמעה הסטטית של Dagger בזמן הידור (compile-time) באפליקציות ל-Android ללא צורך עלות זמן ריצה או שימוש בזיכרון.

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

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

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

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

למרות ש-ProGuard יכול לעזור בהסרה של ממשקי API ומשאבים עם הדגלים הנכונים לא יכולה להסיר יחסי תלות פנימיים גדולים של ספרייה. התכונות הרצויות יכול להיות שהספריות האלה ידרשו יחסי תלות ברמה נמוכה יותר. הבעיה הזו חמורה במיוחד כשמשתמשים בתת-סוג של Activity מתוך ספרייה – שיכולה לכלול מגוון רחב של יחסי תלות – כשהספריות משתמשות בהשתקפות (reflection), דבר נפוץ שדורש שינוי ידני של ProGuard כדי שהוא יפעל.

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