Android17新规:内存超限直接杀App,没有崩溃日志怎么排查?

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
    }
}

判断条件比较具体:reasonREASON_OTHERdescription 里包含 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_HIDDENTRIM_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_OTHERMemoryLimiter:AnonSwap

本地验证用 am memory-limiter status/manual/ignore,代码里补 onTrimMemory(),release 包确认 R8 优化没有被关掉。线上再用 ProfilingManager 的 OOM / anomaly 触发能力补 heap dump,定位会比只等用户复现清楚很多。

#Android #Android17 #性能优化 #内存优化 #R8

相关推荐
Yeyu1 小时前
Binder 阻塞检测:跨进程通信的性能陷阱与监控方案
android·性能优化
●VON2 小时前
鸿蒙Flutter实战:日期选择器与截止日期高亮提醒
android·flutter·华为·harmonyos·鸿蒙
流星白龙2 小时前
【MySQL高阶】20.InnoDB 磁盘文件
android·mysql·adb
●VON2 小时前
鸿蒙Flutter实战:Material 3种子色亮暗双主题系统
android·flutter·harmonyos
灰鲸广告联盟2 小时前
新老用户广告价值不同?差异化策略如何实现收益最大化
android·开发语言·flutter·ios
朱涛的自习室3 小时前
逃离“古法测试”:AI 测试的“三大定律”
android·前端·人工智能
QING6183 小时前
Android面试 —— 八股文(一)
android·面试·android jetpack
带娃的IT创业者3 小时前
围墙花园的隐形锁:当 reCAPTCHA 拒绝了“去谷歌化”的 Android 用户
android·隐私安全·人机验证·recaptcha·去谷歌化·grapheneos
awu的Android笔记4 小时前
Android 用户态实现 TCP 代理:从 SYN 到 FIN 的完整生命周期
android·tcp/ip