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

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

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

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

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

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

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

לדוגמה, ברוב מפות הביטים נעשה שימוש בהגדרה ARGB_8888, כלומר כל פיקסל דורש 4 בייטים של זיכרון – בייט אחד לכל אחד מהצבעים אדום, ירוק וכחול, ובייט אחד לערך אלפא (שקיפות). אם יש לכם תמונה בפורמט JPEG בגודל 100KB ואתם מציגים אותה בתצוגה של 1, 000‎×1,000 פיקסלים,מפת הסיביות תדרוש 4 בייטים לכל אחד ממיליון הפיקסלים האלה, כלומר עד 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: האות הזה מציין שממשק המשתמש של האפליקציה כבר לא מוצג למשתמש. המעבר הזה מספק הזדמנות לשחרר הקצאות משמעותיות של זיכרון שקשורות באופן הדוק לממשק המשתמש, כמו מפות סיביות (bitmap), מאגרי נתונים זמניים (buffer) של הפעלת סרטונים או משאבי אנימציה מורכבים.

  • 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 מגדירה מגבלה קשיחה על גודל ה-heap שמוקצה לכל אפליקציה. מגבלת הגודל המדויקת של ה-heap משתנה בין מכשירים שונים, בהתאם לכמות ה-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 (הדמון של LMK) מסיים תהליכים על סמך 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 (מאגרי פרוטוקולים) (protobufs) הם מנגנון מורחב שפותח על ידי Google לסדר נתונים מובְנים, בדומה ל-XML, אבל קטן יותר, מהיר יותר ופשוט יותר. המנגנון הזה לא תלוי בשפה או בפלטפורמה. אם אתם משתמשים ב-protobufs לנתונים שלכם, תמיד צריך להשתמש ב-lite protobufs בקוד בצד הלקוח. פרוטוקולי protobuf רגילים יוצרים קוד מפורט מאוד, מה שמגדיל את טביעת הרגל של קוד האפליקציה ב-RAM (ראו ניהול ואופטימיזציה של טביעת הרגל של קוד האפליקציה) ותורם להגדלת גודל ה-APK.

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

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

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

מידע נוסף מופיע במאמר בנושא דליפות זיכרון.

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

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

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

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

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

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

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

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

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