從 Android 17 開始,Google 官方開始對記憶體進行嚴格管控,**App 如果長期占用過多匿名記憶體 / swap,系統會依設備總 RAM 給它加上限制,超限後可能直接殺掉程序,而且沒有一般的崩潰堆疊**。
也就是說,你繼續占用太多,我就把你「暗殺」掉,然後你只看 crash 日誌還不知道自己被殺過。
以前 Android 系統的記憶體治理主要還是靠 LMK(Low Memory Killer),系統整體記憶體緊張時,會按程序優先級殺掉背景、快取程序,只有最嚴重時才會殺前台,而 Android 17 的新邏輯是:
不能讓一個 App 因為記憶體洩漏或占用過大,就把系統流暢度給掀了。
這突然讓我想起了之前那篇 《抖音“極客”適配 Android 5 ~ 9 等老機型技術解讀,都是騷操作》 ,基本結論就是,App 為了讓使用者用得更流暢,你不給的我自己想辦法拿,這麼一對比貌似還是反著來的。
而在 Android 17 這裡,官方給了一個例子:
如果一個 App 持有前台服務等較高優先級狀態,同時又不斷膨脹記憶體,傳統 LMK 一開始不會優先殺它,這就導致系統只能去殺很多「小而正常」的快取 App 來回收記憶體,結果就是使用者返回其他 App 時本來應該 warm resume,結果反而變成冷啟動,還增加 CPU 壓力和耗電。
所以 Android 17 這次引入新的限制:按設備總 RAM 給 App 加記憶體上限,有線打擊極端洩漏和離群占用。
這次調整最有意思的就是,它不一定表現為 Java/Kotlin OOM,也不一定有 crash stack,官方給出的判斷方式是用 ApplicationExitInfo.getDescription(),如果被 Android 17 的 memory limiter 影響,exit reason 會是 REASON_OTHER,description 字串裡會包含 MemoryLimiter:AnonSwap。
也就是說,你以後排查線上「無堆疊死亡」、「使用者說 App 突然沒了」、「前台服務 App 莫名被殺」時,不能只看 Crashlytics/Java crash 了,還需要把
ApplicationExitInfo也加入啟動時診斷鏈路。
這個適配真的很重要,因為你不做真的就直接抓瞎,比如 App 下次啟動時讀取歷史退出原因,如果發現 REASON_OTHER + MemoryLimiter:AnonSwap,就打點上報:
val activityManager = getSystemService(ActivityManager::class.java)
val exitReasons = activityManager.getHistoricalProcessExitReasons(
packageName,
0,
10
)
for (info in exitReasons) {
val desc = info.description ?: ""
if (info.reason == ApplicationExitInfo.REASON_OTHER &&
desc.contains("MemoryLimiter:AnonSwap")
) {
// 上報:疑似 Android 17 memory limiter 殺掉程序
}
}
因為這個不是一般 crash,屬於系統層面的強制結束,所以只靠例外攔截沒用。
這裡官方也給出了幾個適配方案,比如 R8 位元碼最佳化、App 不可見時主動 trim memory 等,在幾個適配建議裡,Google 把 R8 放在第一位,官方建議以下設定全開,同時使用全新的 proguard-android-optimize.txt:
isMinifyEnabled = true
isShrinkResources = true

這是一個大家都不太想動的東西,而官方明確說,舊的 proguard-android.txt 會阻止最佳化,並且在 Android Gradle Plugin 9 也不再支援,還要求移除 android.enableR8.fullMode = false。
不過這對很多老專案來說確實很要命,因為不少專案的 ProGuard/R8 設定裡有這種「祖傳保命三件套」:
-dontoptimize
-dontshrink
-dontobfuscate
不過 Google 也說了,這些全域開關會讓 R8 對整個 codebase 失去最佳化空間,官方還推出了 R8 Configuration Analyzer,它會給出 shrinking、optimization、obfuscation 三類分數,告訴你哪些 keep rule 阻止了多少類別、方法、欄位被最佳化。

從 AGP 9.3.0-alpha05 開始,R8 建置時會自動在
build/outputs/mapping/release/configanalyzer.html產生報告。
其次就是圖片,老生常談了,Bitmap 往往是 App 裡最大的一類常駐物件,JPEG/PNG 是壓縮檔,但顯示時會解碼成原始像素資料,記憶體消耗取決於像素尺寸 × 色彩格式,一張 100KB 的壓縮圖,解碼後可能需要占幾 MB,所以 Google 建議:
降採樣,不要把一張超大圖解碼後塞進一個小 thumbnail,手寫 Bitmap 載入時用 inSampleSize;Glide / Coil 預設會做 downsample,但也要檢查設定
不要把 padding 放到圖片裡,比如為了留白,直接做一張大透明邊框 PNG,應該用 View / Composable 的 padding,或者 InsetDrawable
合理選擇 Bitmap Config,不需要透明通道時可以考慮 RGB_565,它比預設 ARGB_8888 少一半記憶體,當然代價是色彩品質和透明能力
簡單幾何圖形優先用 Vector / ShapeDrawable,不要什麼都切 PNG
手動管理 Bitmap 時注意重用和釋放
Android Studio Narwhal 4 的 Profiler 也增加了重複 Bitmap 檢測:Heap Dump 後可以看黃色警告,或者用 Duplicate Bitmaps 篩選器,直接找到重複儲存的圖片。

另外記憶體洩漏也是常見問題。典型問題包括:
洩漏 Context,比如在 Compose 裡把 LocalContext.current 傳給 ViewModel,View 系統裡把 Activity 放進 companion object / static 變數,都容易造成洩漏,正確做法是 UI 層用 Context,非 UI 層透過依賴注入或狀態流轉解決,不要持有 Activity
洩漏 Listener,Compose 裡 DisposableEffect 註冊了 listener,但 onDispose 裡沒註銷
洩漏 View,比如 AndroidView 包了一套 legacy View,但沒有 release 策略,Fragment 裡持有 view binding,在 onDestroyView() 後沒清空
實際上 Android Studio Panda 3 裡就已經有專門的 LeakCanary profiler task,可以把洩漏分析從裝置端搬到開發機端,同時和 IDE 原始碼跳轉整合。

另外就是 onTrimMemory,Android 17 下更應該主動釋放快取,不要等系統來決定回收什麼記憶體,自己應該在合適時機釋放可以重建的資源,比如實作 ComponentCallbacks2.onTrimMemory(),尤其是全域快取可以放在 Application 層管理。
這裡主要關注:TRIM_MEMORY_UI_HIDDEN 和 TRIM_MEMORY_BACKGROUND,因為從 Android 14 開始,系統已經不再發送其他 legacy constants:
TRIM_MEMORY_UI_HIDDEN:你的 UI 已經不可見,可以釋放和介面強綁定的大物件,比如 Bitmap 快取、影片播放 buffer、複雜動畫資源
TRIM_MEMORY_BACKGROUND:程序已經在背景,是系統記憶體緊張時的候選終止對象,這時應該更積極釋放那些恢復時能低成本重建的資源,換取更久的 cached 存活,減少冷啟動
class App : Application(), ComponentCallbacks2 {
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
// 清 UI 相關快取:圖片、動畫、影片 buffer、頁面暫存物件
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// 更積極清理:可重建的全域 cache、暫時資料、背景非必要資源
}
}
}
除此之外,官方還推薦 ProfilingManager,本地複現不了的記憶體問題最難搞,所以 Android 15 引入了 ProfilingManager,可以讓 App 程式化收集真實使用者裝置上的 Perfetto profiles,現在 Android 17 又引入了新的事件驅動 trigger TRIGGER_TYPE_OOM 和 TRIGGER_TYPE_ANOMALY:
TRIGGER_TYPE_OOM:在 OutOfMemoryError crash 的精確時刻自動收集 Java heap dump,收集到的結果會在 App 下次啟動並註冊 registerForAllProfilingResults 回呼後提供
TRIGGER_TYPE_ANOMALY:用於嚴重效能異常,例如 excessive binder spam 或突破記憶體閾值,對於 memory anomaly,它會在系統終止 App 前收集 heap dump。
這對 Android 17 的這次記憶體限制很關鍵:以前你可能只知道「程序被殺了」,現在可以在接近被殺前拿到 heap dump,知道是誰占了記憶體。
另外官方還推薦用 Perfetto UI 的 Heap Dump Explorer 分析 heap dump,這樣可以看物件配置層級、retained size、到 GC root 的最短路徑,也能透過 flamegraph 找大物件。

最後,Android 17 也提供了 adb shell am memory-limiter 命令,它只對啟用了 memory limiter 的裝置有效,比如有:
adb shell am memory-limiter status:查看目前 memory limiter 狀態,包括 visible / non-visible 程序限制adb shell am memory-limiter ignore <uid>|none|all:讓 memory limiter 忽略某個 UID、全部 App,或者恢復不忽略adb shell am memory-limiter manual <pid> <limit>|max|none:給某個程序手動設定限制,比如 adb shell am memory-limiter manual 12345 300,把 PID 12345 的程序限制到 300MB可以看出來,這次 Android 17 的記憶體限制變動會很大,以至於官方都不得不給出一系列建議來督促大家趕緊適配,而且 Android 17 也馬上就要來了,其他都還是次要的,避免 App 被刺殺才是最重要的。
我就問你一句,你開 R8 full 嗎?
android-developers.googleblog.com/2026/06/pri…