Android 17 内存管理将严格管控,App 要注意适配

1 阅读8分钟

从 Android 17 开始,谷歌官方开始把对内存进行严格管控,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

image-20260604094653901

这是一个大家都不大想动的东西,而官方明确说,旧的 proguard-android.txt 会阻止优化,并且在 Android Gradle Plugin 9 也不再支持,还要求移除 android.enableR8.fullMode = false

不过这对很多老项目来说确实很要命,因为不少项目的 ProGuard/R8 配置里有这种“祖传保命三件套”:

-dontoptimize
-dontshrink
-dontobfuscate

不过谷歌也说了,这些全局开关会让 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,所以谷歌建议:

  • 降采样,不要把一张超大图解码后塞进一个小 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_HIDDENTRIM_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_OOMTRIGGER_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…