אובייקטים של מפת סיביות הם בדרך כלל הגורם הכי גדול לשימוש בזיכרון של אפליקציה. בין אם מדובר בסמלי אפליקציות, בתמונות של התראות או בתוכן מדיה, טיפול לא יעיל במפת סיביות עלול להוביל במהירות לשגיאות של חוסר זיכרון (OOM) ולעומס על הזיכרון בכל המערכת.
הגדרות של מפת סיביות ונתוני פיקסלים
נפח הזיכרון שמפת סיביות צורכת נקבע בעיקר לפי המידות שלה (רוחב × גובה) והתצורה שלה (Bitmap.Config).
ההגדרה קובעת כמה בייטים משמשים לייצוג כל פיקסל:
| הגדרות אישיות | בייט לכל פיקסל | תיאור |
|---|---|---|
ALPHA_8 |
1 | רק ערוץ אלפא (שקיפות). שימושי למסכות. |
RGB_565 |
2 | אדום (5 ביטים), ירוק (6 ביטים), כחול (5 ביטים). אין אלפא. מתאים לתמונות אטומות שבהן נאמנות גבוהה לצבעים היא לא קריטית. |
ARGB_8888 |
4 | אלפא, אדום, ירוק, כחול (8 ביטים כל אחד). ברירת המחדל והאפשרות הכי נפוצה. |
RGBA_F16 |
8 | נקודה צפה בחצי דיוק. משמש לתוכן עם טווח צבעים רחב ולתוכן HDR. |
HARDWARE |
לא רלוונטי | מאוחסן בזיכרון ה-GPU (gralloc/DMABuf). מידע נוסף על מפות סיביות של חומרה |
נוסחת הזיכרון: Memory (Bytes) = Width × Height × Bytes Per Pixel
לדוגמה, תמונה במסך מלא במכשיר 1080p (1920x1080) בפורמט ARGB_8888
תופסת: 1920 × 1080 × 4 bytes ≈ 8.3 MB.
מפות סיביות של ערימה לעומת מפות סיביות משותפות
מפות סיביות של ערימות (ערימה מקורית)
ב-Android מודרני (גרסה 8.0 ואילך), נתוני פיקסלים של מפת סיביות מאוחסנים ב-Native Heap, בעוד שרק אובייקט wrapper קטן נמצא ב-Java heap.
כשאפליקציה צריכה להציג תמונה, היא בדרך כלל מפוענחת מקובץ תמונה דחוס ל-Bitmap ונשמרת ב-Heap.
מפות סיביות משותפות (ashmem/memfd)
כשמפת סיביות מועברת בין תהליכים (למשל, דרך Binder אל SystemUI להתראה), מערכת Android נמנעת מהעתקת נתוני הפיקסלים באמצעות זיכרון משותף (ashmem או memfd).
אפשר להעתיק מכונה של Bitmap לזיכרון משותף באופן מפורש על ידי קריאה ל-Bitmap.asShared(), או באופן מרומז אם מכניסים Bitmap לתוך Parcel (בדרך כלל על ידי הוספת Bitmap ל-Parcelable כמו Bundle) ושולחים אותו באמצעות Binder IPC.
כששולחים Bitmap משותף באמצעות Binder IPC, נתוני הפיקסלים עצמם לא מועתקים, אלא מתבצע שכפול של מתאר קובץ שמפנה לאזור זיכרון משותף לתהליך המקבל. יכול להיות שאזור הזיכרון הבסיסי משותף בין כמה תהליכים, והוא לא משוחרר עד שכל מתארי הקבצים שמפנים אליו נסגרים.
מפות סיביות שניתנות לשינוי לעומת מפות סיביות שלא ניתן לשנות
- מפות סיביות שניתנות לשינוי: אפשר לשנות אותן אחרי היצירה (למשל, באמצעות
Canvas). תמיד נדרשת הקצאת זיכרון פרטית משלהן. אם מעתיקים Bitmap שניתן לשינוי, צריך ליצור עותק עמוק (עותק שני של כל נתוני הפיקסלים). - מפות סיביות שלא ניתן לשנות: אי אפשר לשנות אותן. כך אפשר לבצע אופטימיזציות כמו שיתוף אותו מאגר זיכרון בסיסי בין מופעים שונים של
Bitmap. מפות סיביות שנטענו ממשאבי APK (BitmapFactory) הן בדרך כלל בלתי ניתנות לשינוי.
טיפול יעיל במפת סיביות
איגום ושימוש חוזר של מפות סיביות
הקצאה וביטול הקצאה של מפות סיביות לעיתים קרובות גורמים לתחלופת הקצאה, שמאלצת את ה-GC לפעול כל הזמן. ספריות נפוצות לטעינת תמונות משתמשות במאגר מפות סיביות.
Google ממליצה על Glide כפתרון לאפליקציות מבוססות Java, ועל Coil לאפליקציות מבוססות Kotlin (במיוחד כשמשתמשים ב-Jetpack Compose).
כשאין יותר צורך במפת סיביות, במקום לאפשר ל-GC לטפל בה, האפליקציה קוראת ל-bitmap.recycle() או מחזירה אותה למאגר. בפעם הבאה שיידרש מפת סיביות באותם ממדים והגדרות, המאגר יספק את המאגר הקיים, וכך יימנע הקצאה חדשה.
מפות סיביות של חומרה
Bitmap.Config.HARDWARE מאפשרת לכם לאחסן נתוני פיקסלים ישירות בזיכרון הגרפיקה (DMABuf).
- יתרונות:
- חיסכון בזיכרון: לא נעשה שימוש בערימה של האפליקציה או בערימה מקומית, אלא בזיכרון ה-GPU. לרוב, צריך להעתיק את מפות הביטים שמוצגות בממשק המשתמש של האפליקציה לזיכרון של ה-GPU, כך שהשימוש ב-API הזה חוסך את פעולת ההעתקה ואת העלות הנוספת של הזיכרון.
- ביצועים: מהירות הציור גבוהה מאוד כי הנתונים כבר נמצאים במעבד ה-GPU.
- חסרונות:
- בלתי ניתן לשינוי: אי אפשר לשנות מפות סיביות של חומרה.
- קריאה חוזרת איטית: הגישה לפיקסלים מהמעבד (לדוגמה,
getPixel()) יקרה מאוד. - שיוך (Attribution): קשה יותר לעקוב אחריו בכלים רגילים כמו AHAT (ראו בהמשך).
תרגיל מעשי: ניתוח מפת סיביות
נשתמש באפליקציית הדוגמה BitmapLab כדי להסביר את המושגים האלה.
1. מדידה באמצעות dumpsys meminfo
מפעילים את BitmapLab ומקישים על ALLOCATE 10MB ARGB_8888. מריצים את הפקודה:
adb shell dumpsys meminfo -s com.android.bitmaplab
בגרסאות מודרניות של Android, מחפשים את הקטע Native Allocations (הקצאות מקומיות). הם מספקים שיוך טוב יותר של מפות סיביות בהשוואה לסיכום האפליקציה הכללי:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240 # <--- 10MB Bitmap data!
Bitmap (nonmalloced): 0 0
- Bitmap (malloced): מפות סיביות שהוקצו בזיכרון המקומי של התהליך. כאן נמצאים רוב מפות הביטים הסטנדרטיות ב-Android מגרסה 8.0 ואילך.
- Bitmap (nonmalloced): מפות סיביות שמשתמשות בזיכרון מיוחד כמו Hardware Bitmaps או Shared Bitmaps (דרך
ashmemאוmemfd).
אם מקצים Shared Bitmap ב-BitmapLab, הוא יופיע ב-Bitmap (nonmalloced):
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240
Bitmap (nonmalloced): 1 10240 # <--- Shared Bitmap!
מעקב אחר מפות סיביות משותפות
בגרסאות מסוימות של Android ובהגדרות ליבה מסוימות, dumpsys meminfo מספק גם מעקב ברזולוציה גבוהה אחר מפות סיביות שממופות למרחב הכתובות של התהליך באמצעות מתארים של קבצים.
כברירת מחדל, מפות ביטמשפ שמשותפות מקבלות שם כללי ("bitmap"). כדי להפעיל שיוך מפורט ומעקב ייחודי אחרי מפות סיביות (זיהוי מפות סיביות משותפות בתהליכים שונים), צריך להפעיל את מאפיין המערכת הבא:
adb shell setprop debug.hwui.bitmap_ashmem_long_name true
כשמפעילים את האפשרות הזו, לאזורי ה-ashmem ב-/proc/<pid>/smaps יש שמות יותר תיאוריים. meminfo ינצל את היתרון הזה, והתוצאות ייראו כך:
Shared Bitmaps
Count Size(KB)
------ ------
Mapped: 1 10240
Unique: 1 10240
- ממופה: הגודל הכולל של כל מיפויי הזיכרון שקשורים למפת הסיביות.
- ייחודי: הגודל של מפות סיביות, רק אם לוקחים בחשבון את הערכים הייחודיים (כלומר, שני מיפויים או יותר של אותם נתוני פיקסלים משותפים של מפת סיביות נספרים רק פעם אחת).
2. מפות סיביות ב-AHAT
AHAT מספקת ויזואליזציה מצוינת למפות סיביות.
- ב-BitmapLab, מקצים כמה מפות סיביות.
לכידת תמונת מצב של הזיכרון באמצעות הדגל
-b(כדי לכלול נתוני מפת סיביות מקוריים):adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof adb pull /data/local/tmp/bitmaps.hprof . ahat bitmaps.hprofפותחים את
localhost:7100ומחפשים את הקישור Bitmaps בסרגל הצד או מחפשים את הכיתהBitmap.הכלי AHAT יציג את מפות הביטים בדפדפן, וכך יהיה קל לזהות אילו תמונות תופסות הרבה זיכרון.

3. טראקים של מפות סיביות ב-Perfetto
בעזרת Perfetto אפשר לעקוב אחרי הקצאות של מפות סיביות וספירות לאורך זמן. המונים האלה מופקים על ידי מסגרת Android כשקטגוריית gfx של atrace מופעלת עבור אפליקציה ספציפית.
מתחילים מעקב. צריך לכלול את הקטגוריה
gfxולטרגט את חבילת האפליקציה הספציפית באמצעות הדגל-a:external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \ -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplabב-BitmapLab, מקישים שוב ושוב על הלחצנים הקצאה וניקוי.
מקישים גם על Parcel/Unparcel Bitmap (חלוקה/ביטול חלוקה של מפת סיביות).
מנתחים את נתוני המעקב בכתובת ui.perfetto.dev.
בקטע התהליך של com.android.bitmaplab, יוצגו הנתונים הבאים:
* Bitmap Count: מונה שמציג את מספר מפות הביטים הפעילות.
* Bitmap Memory: מונה שמציג את מספר הבייטים הכולל שמשמשים למפות סיביות.
פרוסות ברמה גבוהה (Perfetto SDK)
בנוסף, BitmapLab משתמש ב-Perfetto SDK כדי להפיק נתונים ברמה גבוהה לגבי פעולות bitmap. מחפשים את BitmapLab_ בנתוני ה-trace כדי למצוא:
* BitmapLab_parcelUnparcel: Slices covering the parceling and unparceling
logic.
* BitmapLab_postNotification: רכיבי Slice שכוללים את תהליך שליחת ההתראה.
מעקב אחרי תהליכי התראות
כשמקישים על שליחת התראה, האפליקציה יוצרת התראה שמכילה את מפת הסיביות הנוכחית ושולחת אותה למערכת. קוד המסגרת שאחראי לכך פולט פרוסות של Perfetto עם אירועי זרימה שמקשרים בין חלוקת החבילה (כתיבת מפת הסיביות לחבילה לשליחה באמצעות Binder IPC) לבין ביטול החלוקה (קריאת מפת הסיביות מחבילה בצד המקבל).
בצילום המסך שבהמשך אפשר לראות את האפליקציה מחלקת את מפת הסיביות הגדולה כדי להשתמש בה בעסקת Binder כדי לפרסם את ההתראה, ואת הפעולה ההפוכה של החלוקה שמתבצעת בתהליך system_server.

באמצעות Perfetto אפשר לעקוב אחרי אותה מפת סיביות של התראה כשהיא מועברת בין השרשורים והתהליכים, למשל משרשור של Binder ב-system_server (שמטמיע את INotificationManager שרת Binder) לשרשורים של עובדים ב-system_server, שיכולים להעביר את אותה מפת סיביות ל-com.android.systemui כדי להציג אותה במרכז ההתראות.
אתגרים באפליקציות מערכת
אפליקציות מערכת כמו SystemUI (התראות) ו-Launcher מתמודדות עם אתגרים ייחודיים:
- תוכן ללא גבולות: יכול להיות שיהיו הרבה התראות ווידג'טים. אם כל אחד מהם מכיל מפת סיביות גדולה, יכול להיות שהמערכת תישאר אין זיכרון פנוי (OOM) במהירות.
- שכפול: יכול להיות שאותו סמל האפליקציה יישמר במטמון של מרכז האפליקציות, באזור ההתראות של SystemUI ובאפליקציית ההגדרות.
- שיתוף באמצעות מאגרי חומרה: כדי לצמצם את הבעיה, רכיבי המערכת עוברים לשירות מרכזי של 'העברת תמונות' שמשתף מופעי
HardwareBufferבין תהליכים. שיוך DMABuf: מפות סיביות של חומרה חוסכות מקום ב-heap, אבל משתמשות בזיכרון DMABuf, שקשה יותר לשייך לתהליך ספציפי בכלי זיכרון רגילים.
אפשר להשתמש ב-
adb shell dmabuf_dumpכדי לראות הקצאות של DMABuf בכל המערכת. הכלי הזה מספק פירוט של המאגרים לפי תהליך:droid.bitmaplab:19562 Name Rss Pss nr_procs Inode Exporter <unknown> 3840 kB 1280 kB 3 3397 virtio_gpu system 12 kB 4 kB 3 3398 system <unknown> 3840 kB 1920 kB 2 3399 virtio_gpu system 12 kB 6 kB 2 3400 system PROCESS TOTAL 11556 kB 5136 kB- RSS: הגודל הכולל של המאגר אם הוא ממופה בתהליך.
- Pss: הגודל הפרופורציונלי (RSS חלקי מספר התהליכים שמשתפים את המאגר). זהו המדד הטוב ביותר לראיית חשבון.
- nr_procs: מספר התהליכים שמחזיקים כרגע הפניה למאגר הזה.
- היצואן: מנהל ההתקן שהקצה את המאגר (לדוגמה,
virtio_gpuב-Cuttlefish, או ערימת Ion/DMA-BUF ספציפית לספק בחומרה).
אפשר גם להשתמש ב-
adb shell dmabuf_dump -bכדי לקבל סיכום של כל המאגרים ושל השימוש הכולל ב-DMA-BUF בכל המערכת.