בדף הזה מוסבר איך לצמצם באופן יזום את השימוש בזיכרון באפליקציה. למידע על האופן שבו מערכת ההפעלה Android מנהלת את הזיכרון, אפשר לעיין בסקירה כללית על ניהול הזיכרון.
זיכרון גישה אקראית (RAM) הוא משאב חשוב לכל סביבת פיתוח תוכנה, והוא חשוב עוד יותר למערכת הפעלה לנייד שבה הזיכרון הפיזי מוגבל לעיתים קרובות. למרות שגם סביבת זמן ריצה ל-Android (ART) וגם מכונת Dalvik הווירטואלית מבצעות מנגנון איסוף זבל שגרתי, זה לא אומר שאפשר להתעלם מהמקרים שבהם האפליקציה מקצה ומשחררת זיכרון. עדיין צריך להימנע מהחדרת דליפות זיכרון – בדרך כלל הן נגרמות כתוצאה משמירה של הפניות לאובייקטים במשתני חברים סטטיים – ולשחרר אובייקטים מסוג Reference בזמן המתאים, כפי שמוגדר בפונקציות הקריאה החוזרות (callback) של מחזור החיים.
הקטנת טביעת הרגל של הקוד והמשאבים של האפליקציה
חלק מהמשאבים והספריות בקוד יכולים לצרוך זיכרון בלי שתשימו לב. הגודל הכולל של האפליקציה, כולל ספריות של צד שלישי או משאבים מוטמעים, יכול להשפיע על כמות הזיכרון שהאפליקציה צורכת. כדי לשפר את צריכת הזיכרון של האפליקציה, אפשר להסיר מהקוד רכיבים, משאבים וספריות מיותרים, כפולים או מנופחים.
הקטנת הגודל הכולל של האפליקציה באמצעות הפעלת R8
קוד האפליקציה שעבר קומפילציה הוא חלק פעיל בזיכרון של סביבת זמן הריצה. כל מחלקה, שיטה, תלות בספרייה וקבוע מחרוזת חייבים להיטען לזיכרון ה-RAM בזמן ההרצה. ככל שבסיס הקוד המהודר גדול יותר, כך האפליקציה צריכה יותר זיכרון RAM פיזי כדי לפעול.
אפשר להשתמש ב-R8 כדי לצמצם את הזיכרון שבשימוש של האפליקציה. R8 ידוע בעיקר בהקטנת הגודל של קובץ ה-APK, אבל יש לו השפעה ישירה וחיובית על זיכרון זמן הריצה (RAM). R8 מנתח את קוד הבייט של האפליקציה כדי להסיר קוד מת, למזג מחלקות מיותרות, להטמיע שיטות ולהקטין מזהים. הטעינה של פחות bytecode מקומפל מה-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 עוזר לכם למצוא ולאבחן בעיות בזיכרון בדרכים הבאות:
- איך האפליקציה מקצה זיכרון לאורך זמן פרופיל הזיכרון מציג תרשים בזמן אמת של כמות הזיכרון שבה נעשה שימוש באפליקציה, מספר אובייקטי Java שהוקצו ומתי מתבצע מנגנון איסוף זבל.
- מפעילים אירועים של מנגנון איסוף זבל ומצלמים תמונת מצב של ה-heap של Java בזמן שהאפליקציה פועלת.
- מתעדים את הקצאות הזיכרון של האפליקציה, בודקים את כל האובייקטים שהוקצו, רואים את דוח הקריסות של כל הקצאה ועוברים לקוד המתאים בעורך של Android Studio.
כלי פרופיל הזיכרון משולב גם עם ספריית זיהוי הדליפות LeakCanary. בעזרת LeakCanary, אפשר להעביר את ניתוח דליפת הזיכרון ממכשיר הבדיקה למחשב הפיתוח, וכך להאיץ משמעותית את תהליך העבודה. מידע נוסף זמין בנתוני הגרסה של Android Studio.
יש כלים נוספים שאפשר להשתמש בהם כדי לאבחן בעיות בזיכרון על סמך נתונים ממשתמשים שמריצים את אפליקציית הייצור שלכם:
- משתמשים ב'תפקוד האפליקציה' ב-Android כדי לעקוב אחרי אירועי LMK (הפסקת תהליכים בגלל מחסור בזיכרון).
- משתמשים ב-Profiling Manager כדי לעקוב אחרי שגיאות של חוסר זיכרון, וגם אחרי התנהגות חריגה של אפליקציות שיכולה להיגרם מדליפות זיכרון.
שחרור זיכרון בתגובה לאירועים
מערכת 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.
כדי להימנע ממצב של חוסר זיכרון, אפשר לשלוח שאילתה למערכת כדי לקבוע כמה שטח 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 (הדמון של תהליך ההרג של זיכרון נמוך) מפסיק תהליכים על סמך 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() של תצוגה. בשני המקרים, האפליקציה יוצרת הרבה אובייקטים במהירות ובנפח גבוה. הם יכולים לצרוך במהירות את כל הזיכרון שזמין בדור הצעיר, ולגרום לאירוע של מנגנון איסוף זבל.
כדי לזהות את המקומות בקוד שבהם יש העמסת זיכרון גבוהה לפני שמתקנים אותם, אפשר להשתמש בMemory Profiler.
אחרי שמזהים את האזורים הבעייתיים בקוד, מנסים לצמצם את מספר ההקצאות באזורים שבהם הביצועים קריטיים. כדאי להעביר דברים מחוץ ללולאות פנימיות או להעביר אותם למבנה הקצאה מבוסס-מפעל.
אפשר גם להעריך אם מאגרי אובייקטים מועילים לתרחיש לדוגמה. במאגר אובייקטים, במקום להפיל מופע של אובייקט על הרצפה, משחררים אותו למאגר אחרי שכבר לא צריך אותו. בפעם הבאה שיידרש מופע אובייקט מהסוג הזה, תוכלו לאחזר אותו מהמאגר במקום להקצות אותו.
חשוב להעריך את הביצועים בצורה יסודית כדי לקבוע אם מאגר אובייקטים מתאים למצב נתון. יש מקרים שבהם מאגרי אובייקטים עלולים להחמיר את הביצועים. למרות שהשימוש במאגרי זיכרון חוסך הקצאות, הוא מוסיף תקורה אחרת. לדוגמה, תחזוקת המאגר כוללת בדרך כלל סנכרון, שיוצר תקורה משמעותית. בנוסף, ניקוי מופע האובייקט המשותף כדי למנוע דליפות זיכרון במהלך השחרור, ואז האתחול שלו במהלך הרכישה, עלול לגרום לתקורה לא אפסית.
גם שמירה של יותר מופעים של אובייקטים במאגר ממה שצריך יוצרת עומס על מנגנון איסוף זבל. מאגרי אובייקטים מצמצמים את מספר הקריאות לאיסוף האשפה, אבל בסופו של דבר הם מגדילים את כמות העבודה שנדרשת לכל קריאה, כי היא פרופורציונלית למספר הבייטים הפעילים (שניתן להגיע אליהם).