Android 17 开始引入 App 内存限制,限制值会根据设备总 RAM 决定。
如果进程超过限制,系统可以直接杀掉这个进程,而且不会给一段常规 crash 堆栈。
这个变化对大多数正常会话影响不大,但对内存泄漏、图片缓存过大、前台服务长期占内存这类问题,会更早暴露出来。

限制怎么触发
以前遇到低内存,很多时候是 LMK 先处理后台进程。某个 App 如果占了很多内存,系统可能会连续回收其他 cached app,用户回到这些 App 时就变成冷启动,页面状态也可能丢。
Android 17 的做法更确定一些:在部分设备上,系统会按设备总 RAM 给 App 设置限制。进程越过这个限制后,系统可以终止当前进程,避免一个异常进程把整机多任务体验拖下去。
这里要注意两点。第一,内存限制只会在一部分 Android 17 设备上启用,不是所有设备都有同样行为。第二,这不是 Java 堆 OOM,也不一定会在 Crash 平台里看到一段清晰堆栈。
如果用户反馈"App 被系统杀了",但没有普通 crash,历史退出原因是第一个入口。Android 11 以后可以通过 ActivityManager.getHistoricalProcessExitReasons() 读取 ApplicationExitInfo。
bash
fun findMemoryLimiterExit(context: Context): Boolean {
val activityManager = context.getSystemService(ActivityManager::class.java)
val exits = activityManager.getHistoricalProcessExitReasons(
context.packageName,
0,
20
)
return exits.any { info ->
info.reason == ApplicationExitInfo.REASON_OTHER &&
info.description?.contains("MemoryLimiter:AnonSwap") == true
}
}
判断条件比较具体:reason 是 REASON_OTHER,description 里包含 MemoryLimiter:AnonSwap。只看 REASON_OTHER 不够,因为这个 reason 还会覆盖其他退出情况。
本地复现
Android 17 的行为变更文档里补了 am memory-limiter 命令。它可以查看当前 memory limiter 状态,也可以给某个进程手动设置限制。
先拿到包名对应的 pid:
bash
adb shell pidof com.example.app
再看 memory limiter 当前状态:
bash
adb shell am memory-limiter status
给目标进程设置一个较低限制,例如 300 MB:
bash
adb shell am memory-limiter manual <pid> 300
如果要恢复系统默认限制:
bash
adb shell am memory-limiter manual <pid> none
如果要移除当前进程上的所有限制:
bash
adb shell am memory-limiter manual <pid> max
还有一个 ignore 子命令,用来让 memory limiter 忽略某个 UID、忽略全部,或者取消忽略:
bash
adb shell am memory-limiter ignore <uid>
adb shell am memory-limiter ignore all
adb shell am memory-limiter ignore none
这些命令只在启用了 memory limiter 的设备上生效。如果设备本身不施加这类限制,命令不会产生实际影响。

R8 先打开
内存优化不要只盯着 heap dump。发布包里的代码、资源、反射 keep 规则,也会影响运行时常驻内存。
release 构建里至少要确认 R8 优化是打开的:
bash
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
这里不要继续用 proguard-android.txt。这个文件偏兼容旧行为,会阻止一部分优化;AGP 9 里也不再支持它。
再检查 gradle.properties 里有没有关闭 full mode:
bash
# 如果项目里还留着这一行,删掉
android.enableR8.fullMode=false
proguard-rules.pro 里也要少用全局开关。下面这些写法会直接挡住 R8 对整个代码库的优化:
bash
-dontoptimize
-dontshrink
-dontobfuscate
反射、序列化、三方 SDK 需要 keep 时,规则要收窄到具体类、字段或注解。比如只保护某个 JSON 模型包,通常比 -keep class com.example.** { *; } 更可控。
如果是 SDK 或组件库,消费者需要的规则放在 consumer-rules.pro,库内部为了自己编译和测试保留的规则放在模块自己的 proguard-rules.pro。这两个文件混在一起,会让接入方拿到过宽的 keep 规则。
图片和泄漏
图片是 Android 内存里很容易被低估的部分。一个压缩后只有几百 KB 的 PNG,解码成 ARGB_8888 后,内存按宽、高和每像素字节数计算。图片尺寸大,内存就会直接上去。
Compose 项目里用 Coil,View 项目里用 Glide,都不要绕过库自己手写一套大图加载。缩略图场景要让加载尺寸贴近目标 View 或 Composable 的显示尺寸,不要把原图解码后再交给 UI 缩放。
如果图片不需要透明通道,可以评估 RGB_565。它比 ARGB_8888 少一半像素内存,但颜色质量和透明能力会受影响,适合头像占位、列表缩略图这类对透明要求不高的场景。
重复 Bitmap 可以直接从 Android Studio Profiler 里查。Heap Dump 结果里会标出 duplicate bitmaps,点进去能看到图片预览,再回到代码里定位是缓存策略错了,还是列表项重复解码。
内存泄漏排查也有新入口。Android Studio Panda 3 里加入了 LeakCanary profiler task,分析工作放到开发机侧,leak trace 还能和源码跳转连起来。对 Fragment binding 没清空、listener 没注销、Compose DisposableEffect 没释放这类问题,比只看一段文本 trace 更快。

主动释放缓存
App 退到后台以后,系统可能回收一部分内存。问题是系统不一定知道哪些对象马上会用,哪些对象可以低成本重建。
可以在 Application 或组件里处理 onTrimMemory(),把 UI 相关缓存、图片缓存、临时 buffer 这类可重建对象释放掉。
bash
class App : Application(), ComponentCallbacks2 {
override fun onTrimMemory(level: Int) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
imageMemoryCache.clear()
videoPreviewCache.clear()
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
searchResultCache.clear()
temporaryBufferPool.trim()
}
}
}
这里主要看 TRIM_MEMORY_UI_HIDDEN 和 TRIM_MEMORY_BACKGROUND。Android 14 以后,其他一些旧的 trim 常量不再继续下发,Android 15 里也已经标记废弃。
TRIM_MEMORY_UI_HIDDEN 适合清 UI 相关的大对象,比如图片、视频预览 buffer、动画资源。TRIM_MEMORY_BACKGROUND 说明进程已经在后台,更适合清掉重新进入页面时能再生成的缓存。
不要在这里释放马上无法恢复的业务状态。比如正在编辑的草稿、支付流程状态、用户选择路径,这些应该进入持久化或 ViewModel / saved state 的设计里,而不是当成普通缓存清掉。
线上抓现场
有些内存问题本地不容易复现。Android 15 引入的 ProfilingManager 可以在 App 内注册 profiling 结果回调,Android 17 又加了事件触发能力。
这次和内存关系比较大的触发类型有两个:
bash
ProfilingTrigger.TRIGGER_TYPE_OOM
ProfilingTrigger.TRIGGER_TYPE_ANOMALY
TRIGGER_TYPE_OOM 面向 OutOfMemoryError,在 OOM crash 发生时采集 Java heap dump。采集结果会在 App 下次启动并注册 registerForAllProfilingResults 回调后返回。
TRIGGER_TYPE_ANOMALY 面向系统识别出的严重性能异常,内存限制触发时可以在进程被杀前采集 heap dump。这个点适合补在"没有堆栈的系统杀进程"问题上。
最小接入只需要把结果回调接住,拿到文件路径后交给自己的上传任务:
bash
val profilingManager = context.getSystemService(ProfilingManager::class.java)
val executor = Executors.newSingleThreadExecutor()
profilingManager.registerForAllProfilingResults(executor) { result ->
if (result.errorCode == ProfilingResult.ERROR_NONE) {
enqueueProfileUpload(result.resultFilePath)
} else {
logProfilingError(result.errorCode)
}
}
真正接线上时,还要考虑采样比例、用户同意、文件大小、上传时机和保留时间。heap dump 里可能包含对象内容,不适合当普通日志随便传。

最后
Android 17 的 App 内存限制,最关键的判断点是 ApplicationExitInfo.REASON_OTHER 和 MemoryLimiter:AnonSwap。
本地验证用 am memory-limiter status/manual/ignore,代码里补 onTrimMemory(),release 包确认 R8 优化没有被关掉。线上再用 ProfilingManager 的 OOM / anomaly 触发能力补 heap dump,定位会比只等用户复现清楚很多。