24、AMS 中的锁:ActivityManagerService锁、mService锁、死锁案例分析
说到AMS的锁,我估计不少做Framework开发的朋友都头疼过。说实话,我自己刚接触这部分源码时,也被各种锁绕得晕头转向。今天咱们就好好捋一捋,AMS里到底有哪些关键的锁,它们怎么用,以及——最要命的——死锁是怎么发生的。
一、AMS 的锁体系概览
AMS作为Android系统的核心服务,管理着四大组件的生命周期、进程调度、权限校验等关键任务。这么多操作要并发执行,不加锁肯定乱套。但锁加多了,性能就下来了,还容易死锁。这是个典型的权衡问题。
我个人习惯把AMS的锁分成三个层级:
- 全局锁:ActivityManagerService.class 级别的锁,也就是我们常说的
mService锁 - 实例锁:ActivityManagerService 实例上的
synchronized方法或块 - 细粒度锁:比如
mLock、mProcLock、mPidsSelfLocked等
嗯,这里要注意,不同版本的Android源码里锁的命名和粒度可能不太一样。我下面以Android 12/13的代码为例来讲。
二、ActivityManagerService 锁(mService 锁)
先说说最核心的 mService 锁。这个锁其实是 ActivityManagerService.class 的类锁。为什么用类锁?因为AMS是单例的,用类锁可以保证全局唯一性。
关键点:mService 锁保护的是AMS内部的核心数据结构,比如 mActivities、mServices、mProcessList 等。
看一段典型的代码:
// AMS 中的典型加锁方式
public final class ActivityManagerService extends IActivityManager.Stub {
// 类锁,保护核心状态
static final Object mService = new Object();
// 持有 mService 锁的方法
public int startActivity(IApplicationThread caller, ...) {
synchronized (mService) {
// 检查权限、解析Intent、启动Activity...
return mActivityStartController.obtainStarter(...).execute();
}
}
public void forceStopPackage(String packageName, int userId) {
synchronized (mService) {
// 停止进程、清理Activity、广播通知...
forceStopPackageLocked(packageName, userId);
}
}
}
我在项目中遇到过一个问题:有个第三方应用频繁调用 startActivity,导致AMS长时间持有 mService 锁,其他所有需要这个锁的操作都被阻塞了。系统UI响应变得特别慢,甚至出现ANR。后来我们加了一层限流,才把问题解决。
三、mLock 与 mProcLock
从Android 10开始,Google对AMS的锁做了优化,引入了更细粒度的锁。其中最重要的两个是 mLock 和 mProcLock。
| 锁对象 | 保护范围 | 典型场景 |
|---|---|---|
mLock |
Activity、Service、BroadcastReceiver 等组件状态 | startActivity、bindService、broadcastIntent |
mProcLock |
进程管理相关数据,如 mProcessNames、mPidsSelfLocked |
killProcess、updateOomAdj、processDied |
为什么要拆成两个锁?说白了,就是为了减少锁竞争。你想想看,如果所有操作都抢同一把锁,那并发能力肯定上不去。把进程管理和组件管理分开,很多操作就可以并行执行了。
小技巧:在分析AMS性能问题时,可以先用 systrace 抓取锁竞争情况。重点关注 mService 和 mLock 的等待时间。如果某个锁的等待时间占比超过30%,那基本就是瓶颈了。
四、死锁案例分析
好了,重头戏来了。死锁是AMS开发中最让人头疼的问题之一。我曾经花了一整个下午调试一个死锁问题,最后发现是锁的获取顺序搞反了。
先看一个典型的死锁场景:
// 线程A:持有 mService 锁,尝试获取 mLock
synchronized (mService) {
// ... 做一些操作
synchronized (mLock) {
// 更新Activity状态
}
}
// 线程B:持有 mLock,尝试获取 mService
synchronized (mLock) {
// ... 做一些操作
synchronized (mService) {
// 更新进程状态
}
}
这就是经典的"循环等待"死锁。线程A等着线程B释放 mLock,线程B等着线程A释放 mService,两个线程互相等待,谁也跑不了。
我记得有一次,系统在低内存情况下频繁触发 forceStopPackage 和 startActivity,这两个操作分别走了不同的锁路径,结果就死锁了。系统UI完全卡死,连关机都关不了,只能长按电源键强制重启。
避坑指南:我曾经在修改AMS代码时,不小心在 mService 锁内部调用了 ActivityManagerNative.broadcastStickyIntent(),而这个方法内部又会尝试获取 mService 锁。结果就是——自己等自己,死锁了。所以,千万不要在持有锁的情况下调用可能再次获取同一把锁的方法。
五、如何避免死锁
根据我的经验,避免AMS死锁有几个基本原则:
- 固定锁的获取顺序:比如规定必须先获取
mService,再获取mLock,绝对不能反过来。所有代码都要遵守这个约定。 - 减少锁的持有时间:不要在锁内部做耗时操作,比如I/O、网络请求、复杂计算等。把这些操作移到锁外面。
- 使用
tryLock超时机制:对于某些非关键路径,可以用ReentrantLock.tryLock(timeout)来避免无限等待。 - 定期做锁分析:用
ThreadDump或systrace检查是否有潜在的循环等待。
下面我用一张图来总结AMS的锁体系:
说实话,死锁问题在AMS中并不少见。尤其是当你修改了AMS代码,或者引入了新的锁时,一定要仔细检查锁的获取顺序。我个人的习惯是,在代码注释里明确标注每个锁的获取顺序,比如:
// 锁获取顺序:mService -> mLock -> mProcLock
// 任何违反此顺序的代码都可能导致死锁!
synchronized (mService) {
synchronized (mLock) {
// 安全操作
}
}
嗯,最后再强调一点:不要迷信"加锁就能解决问题"。锁只是工具,用不好反而会引入更多问题。在AMS这种高并发的系统里,锁的设计需要非常谨慎。多花点时间在设计阶段,比后期调试死锁要划算得多。
实用工具推荐:分析AMS死锁时,我常用这几个工具:
dumpsys activity- 查看AMS内部状态kill -3 [pid]- 触发Java线程dumpsystrace- 可视化锁竞争情况ThreadDump分析工具 - 快速定位循环等待
好了,关于AMS的锁就讲到这里。记住,理解锁的本质不是为了炫技,而是为了写出更稳定、更高效的代码。下次遇到系统卡死或者ANR,不妨先看看是不是锁的问题。