這一篇原本只佔我本來規劃的系列文章小小的篇幅,可是越寫越深,但不想一次就拉到那麼深,來回修改沒有想法,分成基礎跟進階就好。
還沒評估,別談優化
我們不是在做手機 App,不需要切 app、滑畫面、體驗順順的。 我們做的是一台人臉辨識機,開機就跑模型,模型就要活。
而你手上是一台 2GB RAM 的 Android 10 裝置,系統一開機就吃掉 1.3GB,剩下不到 700MB 能用。 你放個雙鏡頭 stream、TFLite 模型、再加活體檢測,鐵定爆炸。 但爆不爆,不是看心情,是看數據。
全機記憶體現況:該先看的就是這個
你打開機器,第一個該跑的就不是什麼 profiler,也不是你 app 的記憶體分佈,而是:整台機器還剩多少記憶體可活?
直接來:
adb shell dumpsys meminfo
裡面最上面那區,就告訴你整體狀況,像這樣:
Total RAM: 1,909,104K (status normal)
Free RAM: 456,339K ( 140,995K cached pss + 251,112K cached kernel + 64,232K free)
Used RAM: 1,503,496K (1,301,496K used pss + 202,000K kernel)
Lost RAM: 125,250K
ZRAM: 44,240K physical used for 225,168K in swap ( 524,284K total swap)
KSM: 26,640K saved from shared 828K 20,964K unshared; 6,096K
- Total RAM:這機器就是標準 2GB,能動的大概就 1.9GB
- Free RAM:其實只有 64MB 真空,其他都是 cached,一換 context 就沒了
- Used RAM:1.5GB,幾乎炸滿
- ZRAM:用 44MB 壓縮出 225MB swap,壓縮比超過 5 倍,是現在唯一能靠的彈性區
- KSM:救了 26MB,雖然小,但能省就省
- Lost RAM:125MB 被保留、碎片化或被 ION/GPU 吃掉,回不來了
你如果看到 Free RAM 小於 100MB,那就知道,一開 TFLite 模型馬上 OOM 是常態。
這個指令,請你在每次改完系統、改完服務、開機後第一件事就跑它。
Android Studio Profiler:看趨勢用的
- 記憶體使用圖(Memory Timeline):展示 Java、Native、Graphics、Stack、Code、Others 的即時分佈與變化。為能看出 camera stream 開啟後 Graphics 是否不斷累積、TFLite 推理時 Native memory 是否爆衝。
- GC 活動標記(Garbage Collection Events):顯示 GC 發生時間點,協助你判斷是否因為記憶體吃緊導致系統頻繁回收。當我們在模型推理或多次辨識時出現大量 GC,就要考慮是否有 Bitmap 沒釋放。
- Heap Dump:允許你在任意時間點產生 heap dump 並檢查有哪些類別、物件實例與記憶體佔用。這個是我非常常用的功能,實戰上我用hprof 找出非常多可以優化的部分,很多部分都是ByteArray 累積:camera callback 或 image preprocess 不斷 new buffer 沒回收,這部分如果未來有空我會專門寫一篇怎麼玩的文章
- Allocation Tracker(配置追蹤):可以觀察 Java Object 何時被建立、誰建立的,以及是否有釋放。在使用 camera callback 或 Tensor 輸入前處理流程時,這功能可以追蹤是否一直在 new ByteBuffer 卻沒被釋放。
- 實體配置與釋放事件追蹤(Allocation Timeline):在 Allocation Tracking 開啟時可分析記憶體碎片與重複配置行為。我們常用來觀察 各種圖片的前處理是否都重建輸入結構尤其是 MAT,造成記憶體反覆震盪。
這些功能搭配使用,可以有效協助你:
- 初步辨識記憶體成長趨勢(例如 buffer 沒釋放、模型 init 後未清理)
- 找出記憶體爆衝的區段(推理瞬間爆掉)
- 分析重複配置與過度 GC 的程式邏輯錯誤
- 捕捉 bitmap、texture、native library 沒釋放導致的 native memory 積壓
適合初期觀察,也方便畫面 Demo 給老闆看真的吃很兇

可以看到:
- 🟫 Graphics(棕色區塊)佔用超過一半,持續上升
- 🔵 Native(深藍)穩定成長
- 🟦 Java Heap(淺藍)只有少量佔用
- 有幾段記憶體「斷崖式下降」,可能是 GC 或 LMKD 觸發清除
dumpsys meminfo:精算記憶體結構(不要只看總量)
這是我每次開機後都會跑的指令,對應實際 app 的記憶體佔用結構。
adb shell dumpsys meminfo com.your.app
範例輸出:
Applications Memory Usage (in Kilobytes):
Uptime: 3758295 Realtime: 3758295
** MEMINFO in pid 12345 [com.your.app] **
Pss Private Private Swapped Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 43264 41800 0 0 49152 45000 8000
Dalvik Heap 92160 90500 0 0 102400 98000 4300
Dalvik Other 10240 10000 0 0
Stack 2048 2048 0 0
Ashmem 3072 3072 0 0
Other dev 12 12 0 0
.so mmap 15360 1024 8192 0
.apk mmap 2048 0 2048 0
.ttf mmap 1024 0 1024 0
.dex mmap 8192 0 8192 0
Other mmap 4096 64 2048 0
EGL mtrack 2048 2048 0 0
GL mtrack 4096 4096 0 0
Unknown 20480 20480 0 0
TOTAL 205600 192096 20408 0
App Summary
Pss(KB)
Java Heap: 90500
Native Heap: 41800
Code: 21504
Stack: 2048
Graphics: 6144
Private Other: 30496
System: 12308
TOTAL PSS: 205600 TOTAL RSS: 194560
關注重點:
- Native Heap: 41800K Private Dirty (約 41MB):C/C++ buffer、模型、bitmap 等佔用。
Private Dirty
是判斷 Native 記憶體洩漏的關鍵指標,持續增長且沒有釋放很可能就是洩漏。 - Dalvik Heap: 90500K Private Dirty (約 88MB):Java 物件空間,如果這個區域持續增長並接近 Heap Size,很容易觸發 OOM (Out Of Memory) 錯誤。
- TOTAL PSS: 205600K (約 200MB):應用程式實際佔用的實體記憶體(包含與其他進程共享的部分的加權值)
上面的部分其實也蠻夠用的,如果你是一名senior以上的工程師,我相信也早已經知道,但基本功永遠是最重要的,下面的部分說實在真的是因為裝置實在太爛,然後我對這些細解也很喜歡才會進階去看
各位有沒有好奇 dumpsys meminfo 的數據怎麼出現的,組成上有很多來源,其中一個是 /proc/process-id/smaps
smaps:記憶體顯微鏡
adb shell cat /proc/$(pidof com.your.app)/smaps
7f3e0000-7f3f0000 rw-p 00000000 00:00 0
Size: 64 kB # 該記憶體區塊總大小
Rss: 60 kB # 實際佔用的實體記憶體(未被 swap 出去)
Pss: 30 kB # 共享加權後的記憶體佔用值
Shared_Dirty: 0 kB # 被共享、且有寫入過的記憶體量
Private_Dirty: 60 kB # 專屬該 process 且有寫入過的記憶體,記憶體洩漏的重要指標
Referenced: 60 kB # 最近被存取過的記憶體頁數
Anonymous: 60 kB # 匿名記憶體(通常是 malloc 或 new 分配的 buffer)
VmFlags: rd wr mr mw me ac # 該段區域的記憶體權限與特性 (rd: 讀, wr: 寫, ex: 執行, sh: 共享, pr: 私有, sw: swap)
這些資訊可以進一步確認,也能推論這種爛裝置上最常遇到的幾種狀況:。
- camera buffer / image pipeline 沒有釋放 → Anonymous 區塊暴衝,記憶體爆了還找不到來源。
- 模型 pre-load → .so、.apk、.dex 段持續佔用,但其實只用了其中幾個模型。
- AI 模型推理時用 malloc 分配 → Private_Dirty 區塊大但沒回收。
交叉驗證:如果你在 smaps 裡看到 100MB 的 buffer,但 profiler 上完全沒顯示,那基本上就是 native / GPU 偷吃
原本第一版在寫的時候,是有提供自己常用的指令,但我發現有個大神 Jianwu Gao 作的更好,這邊可以補上,我目前看到最好用的smap 整合 android 的記憶體分析腳本 Android-App-Memory-Analysis
https://github.com/Gracker/Android-App-Memory-Analysis/tree/master
為什麼這樣觀察比較靠譜?
這些方法不是單點看數據,而是讓你能回答這些關鍵問題:
- 是哪種記憶體吃最多?(Profiler / dumpsys)
- 是哪段程式沒釋放?(Heap dump / LeakCanary)
- 是哪個 buffer 吃光記憶體?(smaps / ion / kgsl)
- 系統到底會不會殺 process?(PSI / LMKD)
只有搞清楚這幾件事,你才知道該砍誰、該讓誰留下來。
沒搞清楚誰在吃記憶體,做再多優化也只是盲修盲改。