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。
我的做法是:
- 预加载共享库:在Zygote中预加载常用的so库
- 并行化:进程创建和ActivityRecord初始化并行执行
- 优先级调度:前台进程的启动优先级高于后台进程
避坑指南:我曾经在优化进程启动时,把所有的预加载都放到了Zygote中,结果导致Zygote启动时间从5秒变成了30秒。后来我改用按需加载+缓存策略,既保证了启动速度,又不会拖慢系统启动。
四、性能监控与调优工具
光说不练假把式。我平时常用的工具:
- Systrace:查看AMS各个阶段的耗时分布
- dumpsys activity:查看ActivityRecord、ProcessRecord的状态
- Binder日志:通过binder_stats查看IPC调用频率
- 自定义Trace:在关键路径上打点,统计耗时
嗯,这里要注意:性能优化不是一蹴而就的。我建议每次只改一个点,然后通过Systrace对比优化前后的差异。这样能确保每个改动都是正向收益。
最后说一句,性能优化是个持续的过程。没有银弹,也没有一劳永逸的方案。关键是理解每个优化背后的原理,然后根据实际场景选择合适的手段。嗯,希望这些经验能帮到你。
公众号:蓝海资料掘金营,微信deep3321