點陣圖物件通常是應用程式記憶體用量中,單一貢獻度最高的項目。無論是應用程式圖示、通知圖片或媒體內容,處理點陣圖的效率不彰都可能迅速導致記憶體不足 (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 |
不適用 | 儲存在圖形記憶體 (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 建議使用 Glide 做為 Java 應用程式的解決方案,並使用 Coil 做為 Kotlin 應用程式的解決方案 (特別是使用 Jetpack Compose 時)。
不再需要點陣圖時,應用程式會呼叫 bitmap.recycle() 或將點陣圖傳回集區,而不是讓 GC 處理。下次需要相同維度和設定的點陣圖時,集區會提供現有緩衝區,避免重新分配。
硬體點陣圖
Bitmap.Config.HARDWARE 可讓您直接在圖形記憶體 (DMABuf) 中儲存像素資料。
- 優點:
- 節省記憶體:不使用應用程式或原生堆積,而是使用 GPU 記憶體。應用程式 UI 中顯示的點陣圖通常還是需要複製到 GPU 記憶體,因此這項功能可節省複製作業和額外的記憶體成本。
- 效能:繪製速度極快,因為資料已在 GPU 上。
- 缺點:
- 不可變動:無法修改硬體點陣圖。
- 回讀速度緩慢:從 CPU 存取像素 (例如
getPixel()) 的成本非常高。 - 歸因:在 AHAT 等標準工具中較難追蹤 (請參閱下文)。
常見的點陣圖記憶體陷阱
即使使用新式點陣圖設定,解碼和排程點陣圖的方式中,仍有幾個重複模式可能會導致記憶體用量大幅增加。
解碼過度縮放的點陣圖
4000 × 3000 像素的全解析度相片在 ARGB_8888 中會佔用 48 MB。如果只為了在 200 × 150 像素的縮圖中算繪完整圖片,就會浪費超過 99% 的配置像素緩衝區。
直接使用 ImageDecoder 或 BitmapFactory 解碼圖片時,請在解碼傳遞期間降取樣,並使用 ImageDecoder.setTargetSize() 或 BitmapFactory.Options.inSampleSize 配合目標檢視區塊維度。當您提供有界限的目標檢視區塊大小時,Glide 和 Coil 等圖片載入程式庫會自動執行這項降取樣作業。
舉例來說,使用 ImageDecoder 解碼點陣圖時,請傳遞 OnHeaderDecodedListener,將輸出尺寸縮放至目標檢視區塊大小:
val source = ImageDecoder.createSource(resources, R.drawable.high_res_photo)
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
if (info.size.width > targetWidth || info.size.height > targetHeight) {
decoder.setTargetSize(targetWidth, targetHeight)
}
}
平行解碼可提高同時觀看續看率
即使個別點陣圖大小適中且存留時間短暫,平行解碼大量圖片仍可能導致記憶體用量大幅增加。舉例來說,如果主辦方或相簿畫面在無界線的執行緒集區中,同時分派 30 項工作來解碼圖示或縮圖,則所有 30 個未壓縮的像素緩衝區和解碼器暫存緩衝區會同時佔用 RAM。
這種高並行保留會導致原生堆積用量達到高峰,並在批次完成前觸發 lmkd 終止。使用有限的執行緒集區、信號或協同程式調度器 (例如 Dispatchers.IO.limitedParallelism(2)) 限制解碼並行,這樣一次只會解碼幾個點陣圖。
影格處理迴圈中未回收的暫時性點陣圖
在 Android 8.0 以上版本中,Java Bitmap 包裝函式物件在 Java 堆積中只會佔用約 56 個位元組,而像素緩衝區則位於原生堆積中,可能佔用數百萬位元組。您可以在 Android Studio 記憶體分析器或 AHAT 中驗證這項分割作業,其中每個 Bitmap 執行個體都會顯示約 56 位元組的淺層 Java 大小,以及數百萬位元組的原生大小,並顯示在 dumpsys meminfo 的 Native Allocations (Bitmap (malloced)) 下方。
相機影格分析、OCR 或 ML 推論迴圈等高頻率管道,通常會透過呼叫 ImageProxy.toBitmap() 和 Bitmap.createBitmap() 進行旋轉或裁剪,在每個影格上分配新的點陣圖。如果捨棄已取代影格的參照,但未回收這些影格,可能會導致原生記憶體膨脹。微小的 Java 包裝函式幾乎不會增加 Java 堆積佔用空間,因此不會快速觸發垃圾回收,導致數百 MB 的原生像素緩衝區在NativeAllocationRegistry回收前累積。
在緊密迴圈中處理影格時,請盡可能重複使用預先分配的緩衝區,或在每個影格處理完畢後,立即對暫時的中間點陣圖明確呼叫 bitmap.recycle()。
實作練習:探索點陣圖
我們將使用 BitmapLab 範例應用程式來探討這些概念。
1. 使用 dumpsys meminfo 評估
啟動 BitmapLab,然後輕觸「ALLOCATE 10MB 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
- 點陣圖 (已分配):在程序原生堆積中分配的點陣圖。在 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 使用量總計。