AMS 性能优化:减少跨进程调用、优化Activity创建速度、避免ANR

说实话,做Android系统开发这么多年,我见过太多因为AMS性能问题导致的卡顿和ANR。你想想看,AMS作为整个系统的核心调度器,它要是慢了,整个系统都得跟着遭殃。今天我就把我在实战中积累的一些优化经验分享出来。

一、减少跨进程调用的实战策略

跨进程调用(IPC)是AMS性能的最大杀手。我曾在项目中遇到过,一个简单的Activity启动流程,竟然触发了十几次Binder调用。每次Binder调用都有上下文切换的开销,累积起来就很可观了。

核心原则:能合并的调用绝不拆分,能缓存的调用绝不重复。

1.1 Binder调用合并

举个例子,当应用需要查询多个进程的信息时,不要这样写:

// ❌ 糟糕的做法:多次跨进程调用
ProcessInfo info1 = AMS.getProcessInfo(pid1);
ProcessInfo info2 = AMS.getProcessInfo(pid2);
ProcessInfo info3 = AMS.getProcessInfo(pid3);

应该合并成一次调用:

// ✅ 优化的做法:批量查询
List<ProcessInfo> infos = AMS.getProcessInfos(pid1, pid2, pid3);

我个人习惯在写AMS代码时,时刻问自己一句:这个调用真的需要跨进程吗?很多时候,把一些高频访问的数据缓存到本地,就能省掉大量IPC。

1.2 使用本地缓存减少IPC

我记得在优化一个系统应用启动速度时,发现每次启动都要通过AMS查询包信息。这其实是不必要的——包信息在安装后就不会变了。

// 在应用进程侧缓存包信息
public class PackageCache {
    private static final Map<String, PackageInfo> sCache = new ArrayMap<>();
    
    public static PackageInfo getPackageInfo(String packageName) {
        PackageInfo info = sCache.get(packageName);
        if (info == null) {
            // 只有缓存未命中时才走IPC
            info = ActivityManagerNative.getDefault().getPackageInfo(packageName);
            sCache.put(packageName, info);
        }
        return info;
    }
}

小技巧:对于ActivityRecord、ProcessRecord这些频繁访问的对象,可以考虑在SystemServer进程内使用对象池复用,减少GC和对象创建的开销。

二、优化Activity创建速度

Activity创建慢,用户感知最明显。说白了就是点一下图标,半天才出界面。我总结了一套优化链路,从AMS到WMS再到应用进程,每个环节都有优化空间。

2.1 预创建与懒加载的平衡

AMS在启动Activity时,需要经历:解析Intent → 检查权限 → 创建ActivityRecord → 通知WMS → 创建进程(如果需要)→ 加载类 → 创建Activity实例 → 执行生命周期。

这里面最耗时的就是进程创建和类加载。我的做法是:

  • 预创建常用进程:系统启动时,把一些高频应用(如桌面、输入法)的进程提前fork出来
  • 类预加载:在Zygote进程启动时,预加载常用的Activity类
  • 延迟初始化:非关键路径的初始化放到Activity可见之后再做

2.2 优化ActivityRecord创建

ActivityRecord的创建涉及大量对象分配。我曾经在Trace文件中看到,创建一个ActivityRecord竟然花了8ms。优化后降到了2ms。

// 优化前:每次创建都new大量对象
ActivityRecord r = new ActivityRecord();
r.intent = new Intent(intent);
r.info = new ActivityInfo(info);
r.state = new ActivityState();

// 优化后:复用对象池
ActivityRecord r = sRecordPool.acquire();
if (r == null) {
    r = new ActivityRecord();
}
r.reset(intent, info, state); // 复用已有对象,减少分配

注意:对象池不是万能的。如果复用不当,可能导致旧数据残留。我建议只对高频创建且生命周期短的对象使用池化技术。

2.3 减少WMS交互次数

Activity启动过程中,AMS和WMS之间需要多次通信。我做过一个优化:把窗口创建和显示的两个步骤合并成一个事务提交。

// 优化前:两次独立的事务
wms.createWindow(record);
wms.showWindow(record);

// 优化后:合并成一个事务
wms.createAndShowWindow(record);

三、避免ANR的实战经验

ANR是AMS性能问题的终极表现。说白了就是系统等不及了。我处理过上百个ANR问题,总结出几个关键点。

3.1 ANR的触发机制

ANR类型 超时时间 触发场景
Service ANR 20秒 Service的onCreate/onStartCommand执行超时
Broadcast ANR 10秒 BroadcastReceiver的onReceive执行超时
Input ANR 5秒 输入事件分发超时
ContentProvider ANR 10秒 ContentProvider的query/insert等操作超时

3.2 避免AMS主线程阻塞

AMS运行在SystemServer的主线程上。一旦这个线程被阻塞,所有应用都会受影响。我曾经遇到过一个案例:某个三方应用频繁通过Binder查询大量数据,导致AMS主线程长时间被占用,最终引发全局ANR。

我的优化方案:

  • 异步化:所有可能耗时的操作都放到HandlerThread中执行
  • 超时保护:对Binder调用设置超时,防止死锁
  • 限流:对高频调用进行速率限制
// 异步处理耗时操作
private final HandlerThread mWorkerThread = new HandlerThread("ams-worker");
private final Handler mWorkerHandler;

public void handleHeavyTask(IBinder caller, SomeData data) {
    // 快速返回,避免阻塞主线程
    mWorkerHandler.post(() -> {
        // 在后台线程执行耗时操作
        processData(data);
        // 完成后通过回调通知主线程
        mMainHandler.post(() -> notifyCaller(caller));
    });
}

3.3 进程启动优化

进程启动是ANR的高发区。应用冷启动时,AMS需要创建进程、加载类、初始化资源。这个过程如果超过5秒,输入事件就会ANR。

我的做法是:

  1. 预加载共享库:在Zygote中预加载常用的so库
  2. 并行化:进程创建和ActivityRecord初始化并行执行
  3. 优先级调度:前台进程的启动优先级高于后台进程

避坑指南:我曾经在优化进程启动时,把所有的预加载都放到了Zygote中,结果导致Zygote启动时间从5秒变成了30秒。后来我改用按需加载+缓存策略,既保证了启动速度,又不会拖慢系统启动。

四、性能监控与调优工具

光说不练假把式。我平时常用的工具:

  • Systrace:查看AMS各个阶段的耗时分布
  • dumpsys activity:查看ActivityRecord、ProcessRecord的状态
  • Binder日志:通过binder_stats查看IPC调用频率
  • 自定义Trace:在关键路径上打点,统计耗时

嗯,这里要注意:性能优化不是一蹴而就的。我建议每次只改一个点,然后通过Systrace对比优化前后的差异。这样能确保每个改动都是正向收益。

AMS 性能优化核心链路 减少跨进程调用 优化Activity创建速度 避免ANR 具体措施 • Binder调用合并 • 本地缓存减少IPC • 批量查询替代逐条查询 具体措施 • 预创建常用进程 • ActivityRecord对象池 • 减少WMS交互次数 具体措施 • 异步化耗时操作 • Binder调用超时保护 • 进程启动优先级调度 最终目标:流畅的用户体验 常用工具:Systrace | dumpsys activity | Binder日志 | 自定义Trace

最后说一句,性能优化是个持续的过程。没有银弹,也没有一劳永逸的方案。关键是理解每个优化背后的原理,然后根据实际场景选择合适的手段。嗯,希望这些经验能帮到你。


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