24、AMS 中的锁:ActivityManagerService锁、mService锁、死锁案例分析

说到AMS的锁,我估计不少做Framework开发的朋友都头疼过。说实话,我自己刚接触这部分源码时,也被各种锁绕得晕头转向。今天咱们就好好捋一捋,AMS里到底有哪些关键的锁,它们怎么用,以及——最要命的——死锁是怎么发生的。

一、AMS 的锁体系概览

AMS作为Android系统的核心服务,管理着四大组件的生命周期、进程调度、权限校验等关键任务。这么多操作要并发执行,不加锁肯定乱套。但锁加多了,性能就下来了,还容易死锁。这是个典型的权衡问题。

我个人习惯把AMS的锁分成三个层级:

  • 全局锁:ActivityManagerService.class 级别的锁,也就是我们常说的 mService
  • 实例锁:ActivityManagerService 实例上的 synchronized 方法或块
  • 细粒度锁:比如 mLockmProcLockmPidsSelfLocked

嗯,这里要注意,不同版本的Android源码里锁的命名和粒度可能不太一样。我下面以Android 12/13的代码为例来讲。

二、ActivityManagerService 锁(mService 锁)

先说说最核心的 mService 锁。这个锁其实是 ActivityManagerService.class 的类锁。为什么用类锁?因为AMS是单例的,用类锁可以保证全局唯一性。

关键点mService 锁保护的是AMS内部的核心数据结构,比如 mActivitiesmServicesmProcessList 等。

看一段典型的代码:

// 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的锁做了优化,引入了更细粒度的锁。其中最重要的两个是 mLockmProcLock

锁对象 保护范围 典型场景
mLock Activity、Service、BroadcastReceiver 等组件状态 startActivity、bindService、broadcastIntent
mProcLock 进程管理相关数据,如 mProcessNamesmPidsSelfLocked killProcess、updateOomAdj、processDied

为什么要拆成两个锁?说白了,就是为了减少锁竞争。你想想看,如果所有操作都抢同一把锁,那并发能力肯定上不去。把进程管理和组件管理分开,很多操作就可以并行执行了。

小技巧:在分析AMS性能问题时,可以先用 systrace 抓取锁竞争情况。重点关注 mServicemLock 的等待时间。如果某个锁的等待时间占比超过30%,那基本就是瓶颈了。

四、死锁案例分析

好了,重头戏来了。死锁是AMS开发中最让人头疼的问题之一。我曾经花了一整个下午调试一个死锁问题,最后发现是锁的获取顺序搞反了。

先看一个典型的死锁场景:

// 线程A:持有 mService 锁,尝试获取 mLock
synchronized (mService) {
    // ... 做一些操作
    synchronized (mLock) {
        // 更新Activity状态
    }
}

// 线程B:持有 mLock,尝试获取 mService
synchronized (mLock) {
    // ... 做一些操作
    synchronized (mService) {
        // 更新进程状态
    }
}

这就是经典的"循环等待"死锁。线程A等着线程B释放 mLock,线程B等着线程A释放 mService,两个线程互相等待,谁也跑不了。

我记得有一次,系统在低内存情况下频繁触发 forceStopPackagestartActivity,这两个操作分别走了不同的锁路径,结果就死锁了。系统UI完全卡死,连关机都关不了,只能长按电源键强制重启。

避坑指南:我曾经在修改AMS代码时,不小心在 mService 锁内部调用了 ActivityManagerNative.broadcastStickyIntent(),而这个方法内部又会尝试获取 mService 锁。结果就是——自己等自己,死锁了。所以,千万不要在持有锁的情况下调用可能再次获取同一把锁的方法

五、如何避免死锁

根据我的经验,避免AMS死锁有几个基本原则:

  1. 固定锁的获取顺序:比如规定必须先获取 mService,再获取 mLock,绝对不能反过来。所有代码都要遵守这个约定。
  2. 减少锁的持有时间:不要在锁内部做耗时操作,比如I/O、网络请求、复杂计算等。把这些操作移到锁外面。
  3. 使用 tryLock 超时机制:对于某些非关键路径,可以用 ReentrantLock.tryLock(timeout) 来避免无限等待。
  4. 定期做锁分析:用 ThreadDumpsystrace 检查是否有潜在的循环等待。

下面我用一张图来总结AMS的锁体系:

AMS 锁体系结构图 ActivityManagerService mService (类锁) 保护核心数据结构 mLock (实例锁) 保护组件状态 mProcLock 保护进程管理数据 mActivities, mServices mBroadcastQueues ActivityRecord, ServiceRecord BroadcastRecord mProcessNames mPidsSelfLocked ⚠ 死锁风险:循环等待 + 锁顺序不一致 解决方案:固定锁获取顺序 + 减少锁持有时间 + tryLock超时

说实话,死锁问题在AMS中并不少见。尤其是当你修改了AMS代码,或者引入了新的锁时,一定要仔细检查锁的获取顺序。我个人的习惯是,在代码注释里明确标注每个锁的获取顺序,比如:

// 锁获取顺序:mService -> mLock -> mProcLock
// 任何违反此顺序的代码都可能导致死锁!
synchronized (mService) {
    synchronized (mLock) {
        // 安全操作
    }
}

嗯,最后再强调一点:不要迷信"加锁就能解决问题"。锁只是工具,用不好反而会引入更多问题。在AMS这种高并发的系统里,锁的设计需要非常谨慎。多花点时间在设计阶段,比后期调试死锁要划算得多。

实用工具推荐:分析AMS死锁时,我常用这几个工具:

  • dumpsys activity - 查看AMS内部状态
  • kill -3 [pid] - 触发Java线程dump
  • systrace - 可视化锁竞争情况
  • ThreadDump 分析工具 - 快速定位循环等待

好了,关于AMS的锁就讲到这里。记住,理解锁的本质不是为了炫技,而是为了写出更稳定、更高效的代码。下次遇到系统卡死或者ANR,不妨先看看是不是锁的问题。