上周线上突然来了一波用户反馈,说App在后台待一会儿再切回来就闪退。我一开始以为是普通崩溃,检查了一圈日志却什么都没找到------没有ANR,没有Java异常,连个像样的stack trace都没有。
直到我翻到ApplicationExitInfo,才发现"凶手"是谁:MemoryLimiter。
Android 17的内存限制机制,就这么神不知鬼不觉地杀掉了我的App。
怎么确认是MemoryLimiter干的
说实话,这个问题排查了我两天。不是因为难,是因为根本不知道从哪下手。
常规的崩溃排查根本不管用。ANR日志没有,Crash日志也没有。我甚至怀疑是手机系统问题,换了三台设备测试,结果两台Pixel 8 Pro中招,一台老款小米没事------这才让我意识到可能是版本适配的问题。
最后是Google官方文档救了我。官方说可以通过ApplicationExitInfo来判断退出原因:
kotlin
val am = context.getSystemService(ActivityManager::class.java)
val exitInfos = am.getHistoricalProcessExitReasons(
/* packageName = */ null,
/* pid = */ 0,
/* maxNum = */ 10
)
exitInfos.forEach { info ->
if (info.description?.contains("MemoryLimiter") == true) {
Log.e("ExitTracker", "Killed by MemoryLimiter, rss=${info.rss}")
// 这里可以上报到你的监控后台
reportKilledByMemoryLimiter(info)
}
}
跑完这段代码,我的心凉了------半小时内被杀了7次。用户的手机是8GB RAM的旗舰机,没想到内存限制这么激进。
内存限制到底是什么
Android 17引入了一个新机制:系统会根据设备总RAM为每个App设定动态的内存上限。超过这个上限,系统就会直接终止进程,不留痕迹。
官方说"目前的限制比较保守",主要针对极端内存泄漏和会把整个系统拖下水的异常行为。但问题是------保守不代表没有。我这台测试机,App的实际内存上限大概在1.2GB左右,而我的某个页面加载了一大堆图片...
这里有个细节:被MemoryLimiter杀掉的进程,退出原因确实是REASON_OTHER,description里包含MemoryLimiter:AnonSwap字符串。如果你不主动去查,根本不知道是被谁干掉的。
触发式分析:在被杀之前抓现场
既然知道了凶手是谁,下一步就是找到"犯罪现场"------到底是哪段代码吃掉了这么多内存。
Google给了个新工具:触发式Profiling。核心是ProfilingManager和TRIGGER_TYPE_ANOMALY:
scss
val profilingManager = applicationContext
.getSystemService(ProfilingManager::class.java)
val triggers = arrayListOf(
ProfilingTrigger.Builder(
ProfilingTrigger.TRIGGER_TYPE_ANOMALY
)
.setAnomalyAllocationThresholdBytes(50 * 1024 * 1024) // 50MB阈值
.setAnomalyExecutionTimeThresholdMillis(5000) // 5秒阈值
.build()
)
profilingManager.startProfiling(
profilingRequest,
triggers,
executor
) { result ->
// 拿到heap dump后,分析哪块内存涨得最快
analyzeHeapDump(result.heapDump)
}
说实话这套API我没用太熟,官方的Demo跑了几遍才理解。核心思路是:在内存快要超标之前,先把heap dump抓下来,这样你就能看到"凶案现场"的完整快照,而不是事后猜谜。
我的优化踩坑
坑1:以为Glide会自动回收
一开始我以为图片框架会自动处理内存,结果发现不对------Glide的内存缓存策略在某些场景下会hold住大量Bitmap。我那个页面有30多张商品图,用户快速滑动时Glide会预加载,预加载的Bitmap全堆在内存里。
解决方案:手动限制Glide的内存缓存大小
scss
Glide.get(this).clearMemory()
// 在Application里配置
val requestOptions = RequestOptions()
.format(DecodeFormat.PREFER_RGB_565) // 比ARGB_8888省一半内存
.disallowHardwareConfig() // 某些情况下避免使用Hardware Bitmap
Glide.with(this)
.load(url)
.apply(requestOptions)
.into(imageView)
RGB_565这个参数我一开始没注意,后来查文档才发现默认的ARGB_8888每像素占4字节,RGB_565只占2字节。一张1080P的图片,RGB_565能省大约2MB内存。
坑2:LeakCanary在Debug版本才生效
我一开始把LeakCanary配置到正式包想监控线上内存,结果发现正式包根本不触发------LeakCanary 2.x之后默认只在debuggable=true的包生效。
官方文档说要配合TRIGGER_TYPE_ANOMALY使用,或者在测试阶段充分验证。但我的建议是:在测试阶段就把LeakCanary跑满,线上靠ApplicationExitInfo来监控。
坑3:ViewBinding和ButterKnife混用
项目里有个老模块用的是ButterKnife,新模块用的是ViewBinding,结果findViewById和binding.xxx混用,某些情况下会导致view泄漏。我花了一晚上才排查到这个原因。
统一迁移到ViewBinding之后,内存占用下降了大概15%。
适配Android 17的正确姿势
结合这次踩坑经历,总结几点建议:
- 先升级targetSdk到37
这是最基础的一步。Android 17的很多新API和能力需要targetSdkVersion 37才能完全发挥。
- 跑一遍内存基准测试
用ProfilingManager配合TRIGGER_TYPE_ANOMALY,在测试阶段摸清App的内存峰值。
- 监控线上MemoryLimiter事件
ApplicationExitInfo这套API在正式环境也能用,建议集成到你的崩溃监控里。
- 优化图片加载策略
- 使用RGB_565而非ARGB_8888
- 限制Glide的内存缓存大小
- 对大图使用缩略图
- 避免在后台持有大量对象
特别是Context、Bitmap、LargeHeap的误用。
最后说两句
这次适配Android 17的内存限制,让我重新审视了一下自己写的代码。以前觉得"内存够用就行",现在发现内存管理这事,不出问题是你运气好,出问题是必然。
Google的官方态度很明确:Android 17只是开始,后续版本会越来越严格。现在不优化,以后用户升级系统就等着收投诉吧。
建议各位Android开发同行,有空把自己的App在Android 17上跑一遍ApplicationExitInfo的排查代码,看看有没有被MemoryLimiter"暗杀"的记录。发现问题早优化,比等用户投诉再改要体面得多。