點陣圖物件通常是應用程式記憶體用量中,單一貢獻度最高的項目。無論是應用程式圖示、通知圖片或媒體內容,如果處理點陣圖的效率不彰,很快就會導致記憶體不足 (OOM) 錯誤,以及全系統的記憶體壓力。
點陣圖設定和像素資料
點陣圖消耗的記憶體量主要取決於尺寸 (寬度 × 高度) 和設定 (Bitmap.Config)。
設定會定義用於表示每個像素的位元組數:
| 設定 | 每像素位元組數 | 說明 |
|---|---|---|
ALPHA_8 |
1 | 僅限 Alpha (透明度) 管道。適用於遮罩。 |
RGB_565 |
2 | 紅色 (5 位元)、綠色 (6 位元)、藍色 (5 位元)。沒有 Alpha 版。適合不透明圖片,且不要求高色彩保真度。 |
ARGB_8888 |
4 | Alpha、Red、Green、Blue (各 8 位元)。預設選項,也是最常用的選項。 |
RGBA_F16 |
8 | 半精度浮點數。用於廣色域和 HDR 內容。 |
HARDWARE |
N/A | 儲存在圖形記憶體 (gralloc/DMABuf)。請參閱「硬體點陣圖」。 |
記憶體公式: Memory (Bytes) = Width × Height × Bytes Per Pixel
舉例來說,在 1080p 裝置上,全螢幕圖片 (1920x1080) 在 ARGB_8888 中會佔用:1920 × 1080 × 4 個位元組 ≈ 8.3 MB。
堆積點陣圖與共用點陣圖
堆積點陣圖 (原生堆積)
在 Android 8.0 以上版本中,點陣圖像素資料會儲存在原生堆積中,而只有小型封裝物件會駐留在 Java 堆積中。
應用程式需要顯示圖片時,通常會將壓縮圖片檔案解碼為點陣圖,並儲存在堆積中。
共用點陣圖 (ashmem/memfd)
在程序之間傳輸點陣圖時 (例如透過 Binder 傳輸至 SystemUI 以顯示通知),Android 會使用共用記憶體 (ashmem 或 memfd),避免複製像素資料。
您可以呼叫 Bitmap.asShared(),將 Bitmap 例項明確複製到共用記憶體,也可以將 Bitmap 放入 Parcel (通常是將 Bitmap 新增至 Parcelable,例如 Bundle),然後透過 Binder IPC 傳送,隱含地複製到共用記憶體。
透過 Binder IPC 傳送共用的點陣圖時,系統不會複製像素資料本身,而是將參照共用記憶體區域的檔案描述元複製到接收端程序。多個程序可能會共用基礎記憶體區域,且只有在參照該區域的所有檔案描述元都已關閉時,才會釋出該區域。
可變動與不可變動的點陣圖
- 可變動點陣圖:建立後可修改 (例如透過
Canvas)。這類點陣圖一律需要專屬的私有記憶體配置。如果複製可變動的 Bitmap,就必須進行深層複製 (所有像素資料的第二個副本)。 - 不可變動的點陣圖:無法變更。這項功能可讓您進行最佳化,例如在不同
Bitmap執行個體之間共用相同的基礎記憶體緩衝區。從 APK 資源載入的點陣圖 (BitmapFactory) 通常是不可變動的。
有效處理點陣圖
點陣圖集區和重複使用
頻繁配置及取消配置點陣圖會導致配置流失,進而迫使 GC 持續執行。常見的圖片載入程式庫會使用 Bitmap Pool。
Google 建議 Java 應用程式使用 Glide,Kotlin 應用程式則使用 Coil (特別是使用 Jetpack Compose 時)。
不再需要點陣圖時,應用程式會呼叫 bitmap.recycle() 或將點陣圖傳回集區,而不是讓 GC 處理。下次需要相同維度和設定的點陣圖時,集區會提供現有緩衝區,避免重新分配。
硬體點陣圖
Bitmap.Config.HARDWARE 可讓您直接在圖形記憶體 (DMABuf) 中儲存像素資料。
- 優點:
- 節省記憶體:不使用應用程式或原生堆積,而是使用 GPU 記憶體。應用程式 UI 中顯示的點陣圖通常還是需要複製到 GPU 記憶體,因此這項作業可節省複製作業和額外的記憶體成本。
- 效能:由於資料已在 GPU 上,因此繪製速度極快。
- 缺點:
- 不可變更:無法修改硬體點陣圖。
- 回讀速度緩慢:從 CPU 存取像素 (例如
getPixel()) 的成本非常高。 - 歸因:在 AHAT 等標準工具中較難追蹤 (請參閱下文)。
實作練習:探索點陣圖
我們將使用 BitmapLab 範例應用程式來探索這些概念。
1. 使用 dumpsys meminfo 評估
啟動 BitmapLab,然後輕觸「ALLOCATE 10MB ARGB_8888」(配置 10 MB ARGB_8888)。然後執行:
adb shell dumpsys meminfo -s com.android.bitmaplab
在較新的 Android 版本中,請尋找「原生分配」部分。相較於一般應用程式摘要,這些屬性可提供更準確的點陣圖歸因:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240 # <--- 10MB Bitmap data!
Bitmap (nonmalloced): 0 0
- 點陣圖 (malloced):在程序原生堆積中分配的點陣圖。在 Android 8.0 以上版本中,大多數標準點陣圖都位於這個位置。
- 點陣圖 (非 malloced):使用專用記憶體的點陣圖,例如硬體點陣圖或共用點陣圖 (透過
ashmem或memfd)。
如果在 BitmapLab 中配置 Shared Bitmap,您會在 Bitmap (nonmalloced) 中看到相關資訊:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240
Bitmap (nonmalloced): 1 10240 # <--- Shared Bitmap!
追蹤共用點陣圖
在某些 Android 版本和核心設定中,dumpsys meminfo 也會為透過檔案描述元對應至程序位址空間的點陣圖提供高解析度追蹤功能。
預設情況下,共用點陣圖會使用一般名稱 (「點陣圖」)。如要啟用詳細歸因和專屬點陣圖追蹤功能 (識別不同程序中的共用點陣圖),您必須啟用下列系統屬性:
adb shell setprop debug.hwui.bitmap_ashmem_long_name true
啟用後,/proc/<pid>/smaps 中的 ashmem 區域會使用更具描述性的名稱。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 可以追蹤點陣圖分配量和計數。針對特定應用程式啟用 gfx atrace 類別時,Android 架構會發出這些計數器。
開始追蹤。您必須加入
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 中,重複輕觸「Allocate」和「Clear」按鈕。
同時輕觸「Parcel/Unparcel Bitmap」。
在 ui.perfetto.dev 中分析追蹤記錄。
在 com.android.bitmaplab 的程序部分中,您會看到:
* 「點陣圖計數」:顯示有效點陣圖數量的計數器。
* 點陣圖記憶體:顯示點陣圖使用的總位元組數。
高階切片 (Perfetto SDK)
BitmapLab 也會使用 Perfetto SDK,針對點陣圖作業發出高階切片。在追蹤記錄中搜尋 BitmapLab_,找出:
* BitmapLab_parcelUnparcel:涵蓋封送和取消封送邏輯的切片。
* BitmapLab_postNotification:涵蓋通知發布流程的切片。
追蹤通知流程
輕觸「Post Notification」(發布通知) 後,應用程式會建立包含目前點陣圖的通知,並傳送至系統。負責這項作業的框架程式碼會發出 Perfetto 切片,並透過流程事件連結封裝 (將點陣圖寫入要透過 Binder 處理序間通訊 (IPC) 傳送的 Parcel) 和解除封裝 (在接收端從 Parcel 讀取點陣圖)。
在下方的螢幕截圖中,您可以看到應用程式封裝大型點陣圖,以便在 Binder 交易中使用,發布通知,以及 system_server 程序中對應的解除封裝作業。

使用 Perfetto 時,您甚至可以追蹤同一個通知點陣圖,瞭解該點陣圖如何在執行緒和程序之間進一步傳播,例如從 system_server 中的繫結執行緒 (實作 INotificationManager Binder 伺服器) 傳播至 system_server 工作執行緒,然後工作執行緒可能會將同一個點陣圖轉送至 com.android.systemui,以便在通知匣中顯示。
系統應用程式面臨的挑戰
SystemUI (通知) 和 Launcher 等系統應用程式面臨獨特的挑戰:
- 無界限內容:通知和小工具數量可能很多。如果每個檢視區塊都包含大型點陣圖,系統很快就會記憶體不足。
- 重複:啟動器的快取、SystemUI 的通知區域和「設定」應用程式可能都保留相同的應用程式圖示。
- 透過硬體緩衝區分享:為減輕這類問題,系統元件正朝集中式「映像檔卸載」服務發展,以便在程序間分享
HardwareBuffer執行個體。 DMABuf 歸因:硬體點陣圖可節省堆積空間,但會使用 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:目前持有這個緩衝區參照的程序數量。
- 匯出器:分配緩衝區的驅動程式 (例如 Cuttlefish 上的
virtio_gpu,或硬體上供應商專用的 Ion/DMA-BUF 堆積)。
您也可以使用
adb shell dmabuf_dump -b取得所有緩衝區的摘要,以及整個系統的 DMA-BUF 總用量。