Skip to main content
Select a menu in the customizer

Android 爛裝置想跑人臉辨識1-評估第一

這一篇原本只佔我本來規劃的系列文章小小的篇幅,可是越寫越深,但不想一次就拉到那麼深,來回修改沒有想法,分成基礎跟進階就好。

還沒評估,別談優化

我們不是在做手機 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)

只有搞清楚這幾件事,你才知道該砍誰、該讓誰留下來。

沒搞清楚誰在吃記憶體,做再多優化也只是盲修盲改。