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

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

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

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

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

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

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

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

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

כללי שמירה הם הוראות הגדרה שמציינות ל-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 להחדרת תלות

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

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

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

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

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

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

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

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

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

כדי לפתור בעיות שקשורות לשימוש בזיכרון באפליקציה, צריך קודם למצוא אותן. כלי פרופיל הזיכרון ב-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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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