26、AMS 与 系统稳定性:OOM、内存泄漏、死锁对AMS的影响

做Android系统开发这么多年,我见过太多因为AMS不稳定导致的诡异问题了。手机突然重启、应用闪退、界面卡死——这些表象背后,十有八九都能追溯到AMS身上。今天我们就来聊聊,OOM、内存泄漏、死锁这三个“杀手”是怎么搞垮AMS的。

OOM:AMS的“猝死”危机

OOM(Out Of Memory)说白了就是内存不够用了。AMS作为系统核心服务,它跑在System Server进程里。这个进程的内存上限通常是256MB到512MB,视设备而定。

我遇到过最典型的情况:某款手机连续打开20多个应用后,系统突然重启。查日志发现是AMS分配ActivityRecord时抛出了OutOfMemoryError。为什么会这样?

每个ActivityRecord大约占用2-3KB内存。如果用户不断打开新Activity而不关闭,AMS的mActivities列表就会无限膨胀。再加上TaskRecord、ProcessRecord这些关联对象,内存消耗是成倍增长的。

核心问题:AMS的OOM通常不是内存碎片导致的,而是对象泄漏导致的内存持续增长。一旦System Server进程OOM,整个系统都会重启。

我曾经在项目中做过一个实验:写一个自动化脚本,不断用startActivity启动同一个Activity,但不finish。跑了大概2000次后,System Server的PSS从120MB涨到了380MB,然后系统就挂了。

内存泄漏:温水煮青蛙

内存泄漏比OOM更隐蔽。它不会立刻让系统崩溃,但会慢慢蚕食可用内存,最终导致系统越来越卡。

AMS中常见的内存泄漏场景有几种:

  • BroadcastReceiver未注销:Activity销毁时忘记unregisterReceiver,导致AMS持有Activity的引用
  • Callback回调未清理:比如IApplicationThread的死亡回调,如果没在进程死亡时移除,就会一直留在AMS的列表中
  • ContentObserver泄漏:注册了观察者但没在合适时机取消注册

我记得有一次排查一个线上问题:某款手机连续使用3天后,打开相机需要5秒。用dumpsys meminfo一看,System Server的Native Heap涨了200MB。最后定位到是某个三方应用频繁注册ContentObserver,但AMS在进程被杀时没有清理干净这些观察者。

避坑指南:我曾经在排查内存泄漏时,发现AMS的mRegisteredReceivers列表里躺着几百个已经死亡的进程的Receiver。根源是BroadcastQueue在分发广播时,如果接收进程已死,没有及时从列表中移除对应的ReceiverEntry。修复方案是在processDiedLocked方法中增加清理逻辑。

死锁:AMS的“脑血栓”

死锁比前两个更致命。OOM至少还能重启,死锁一旦发生,系统就彻底卡死了,连logcat都刷不出来。

AMS内部有大量的锁:ActivityManagerService的全局锁、WindowManagerService的锁、PackageManagerService的锁……这些锁之间如果存在循环依赖,死锁就来了。

我画了一张图,展示AMS中最容易发生死锁的锁依赖关系:

AMS 死锁风险图 AMS 全局锁 mLock WMS 锁 mWindowMap PMS 锁 mPackages Binder 线程池 等待响应 startActivity resolveActivity 调用三方应用 回调AMS 死锁

你看这个循环:AMS持有全局锁去调用WMS,WMS持有锁去调用PMS,PMS又通过Binder调用三方应用,三方应用再回调AMS——如果三方应用在主线程做了耗时操作,Binder线程就会阻塞,AMS的锁就释放不了。这时候其他线程想获取AMS锁,全部卡死。

我印象最深的一次死锁排查:用户反馈手机偶尔完全无响应,按电源键都没反应。抓了kernel log发现是AMS的mLock和WMS的mWindowMap锁形成了死锁。AMS在添加窗口时需要获取WMS锁,而WMS在报告窗口焦点变化时又需要获取AMS锁——两个线程各持一把锁等对方释放,完美死锁。

注意:死锁一旦发生,watchdog都救不了。因为watchdog本身也需要获取AMS锁来检查状态。所以死锁的后果就是系统彻底冻住,只能长按电源键强制重启。

如何预防和排查

说了这么多问题,总得给点解决方案。我总结了几条实战经验:

问题类型 预防措施 排查工具
OOM 限制Activity栈深度、及时回收无用对象 dumpsys meminfo、strace
内存泄漏 在processDiedLocked中清理所有回调 MAT、LeakCanary
死锁 避免在持有AMS锁时调用外部Binder dumpsys thread、kill -3

我个人习惯在AMS的关键路径上加超时监控。比如startActivity耗时超过5秒就打印warning,超过10秒就dump所有线程堆栈。这样死锁发生时,至少能留下现场证据。

另外,我建议所有AMS的锁操作都遵循一个原则:锁内不要调用Binder。如果必须调用,就先把需要的数据拷贝出来,释放锁后再调用。虽然代码会复杂一些,但能从根本上避免死锁。

一个小技巧:我曾经在AMS的mLock上加了一个递归计数器。每次加锁时记录调用栈,解锁时检查是否持有时间超过100ms。超过就打印警告。这个机制帮我发现了不少潜在的死锁风险点。

最后说一句,系统稳定性没有银弹。OOM、内存泄漏、死锁这三个问题,每个都需要结合具体场景来分析。但只要你理解了AMS的工作原理,掌握了排查工具,大部分问题都能在开发阶段发现并修复。


公众号:蓝海资料掘金营,微信deep3321