5、AMS(ActivityManagerService)核心原理:Activity生命周期管理、任务栈、进程调度
AMS,也就是ActivityManagerService,是Android系统里最核心的服务之一。我经常跟团队里的新人说,搞懂了AMS,你就掌握了Android系统的半壁江山。它管着Activity的生死、任务的切换、进程的调度,说白了就是整个系统的“大管家”。
今天咱们就深入聊聊AMS的三个核心模块:Activity生命周期管理、任务栈机制、进程调度策略。我会结合我这些年踩过的坑,给你讲透。
5.1 Activity生命周期管理
Activity的生命周期,大家都很熟悉了。但AMS是怎么管理这些状态的?这才是关键。
每个Activity在AMS里对应一个ActivityRecord对象。AMS维护着一个全局的Activity栈,通过这个栈来管理所有Activity的状态切换。
核心要点:AMS通过ActivityRecord记录每个Activity的状态,通过ActivityStack管理栈结构,通过ActivityStackSupervisor协调多个栈。
生命周期回调的触发,其实是一个跨进程通信的过程。当用户按Home键时,AMS会通知当前Activity进入onPause,然后启动新的Activity。这个过程涉及多次Binder调用,我当年调试一个ANR问题时,就发现是onPause耗时太长,导致AMS卡住了。
5.1.1 生命周期状态机
AMS内部维护了一个状态机,Activity的状态变化都在这里流转。我画了一张图,你看完就明白了:
嗯,这里要注意:状态切换不是瞬间完成的。AMS会先发送消息到主线程,等应用处理完再回调。这个异步过程,就是很多生命周期问题的根源。
5.1.2 生命周期与进程优先级
Activity的状态直接影响进程的优先级。AMS会根据当前Activity的状态,动态调整进程的oom_adj值。
| Activity状态 | 进程优先级 | oom_adj范围 | 被杀概率 |
|---|---|---|---|
| RESUMED | 前台进程 | 0-100 | 极低 |
| PAUSED | 可见进程 | 100-200 | 低 |
| STOPPED | 服务进程 | 200-300 | 中 |
| DESTROYED | 缓存进程 | 300-900 | 高 |
我曾经遇到过一个诡异的问题:应用切到后台后,过一会儿再回来就重启了。查了半天,发现是onStop里做了大量IO操作,导致进程被系统判定为“不响应”,直接给杀了。后来我把IO操作移到后台线程,问题就解决了。
避坑指南:onPause和onStop里不要做耗时操作。系统对这两个方法的执行时间有严格限制,超过5秒就会触发ANR。我建议只做状态保存和资源释放,其他事情交给onSaveInstanceState。
5.2 任务栈管理
任务栈,也就是Task,是AMS管理Activity的核心数据结构。每个Task包含一组ActivityRecord,按照启动顺序排列。
AMS维护着一个全局的Task栈列表。每个Task有自己的栈,ActivityStackSupervisor负责协调这些栈之间的关系。
5.2.1 启动模式与栈行为
Activity的启动模式,说白了就是告诉AMS:这个Activity应该怎么放进栈里。我整理了一个表格,把四种启动模式的行为说清楚:
| 启动模式 | 栈行为 | 典型场景 |
|---|---|---|
| standard | 每次启动都创建新实例,压入栈顶 | 普通页面 |
| singleTop | 如果栈顶已有实例,复用;否则创建新实例 | 通知栏点击 |
| singleTask | 如果栈中已有实例,清除其上所有Activity并复用 | 主页面 |
| singleInstance | 独占一个Task栈 | 来电界面 |
你想想看,如果每个Activity都无脑用standard模式,栈会越来越深。用户按返回键要按半天才能退出。我一般建议主页面用singleTask,子页面用standard,特殊场景用singleTop。
5.2.2 Intent Flag对栈的影响
除了启动模式,Intent Flag也能控制栈行为。常用的几个Flag:
- FLAG_ACTIVITY_NEW_TASK:在新Task中启动Activity
- FLAG_ACTIVITY_CLEAR_TOP:如果目标Activity已在栈中,清除其上所有Activity
- FLAG_ACTIVITY_SINGLE_TOP:类似singleTop模式
- FLAG_ACTIVITY_REORDER_TO_FRONT:把栈中已有的Activity移到栈顶
我记得有一次,一个同事用FLAG_ACTIVITY_NEW_TASK启动了一个Activity,结果发现按返回键直接回到桌面了。原因就是新Task和原Task没有关联,返回时直接退出了整个Task。后来我建议他用FLAG_ACTIVITY_CLEAR_TOP配合使用,问题就解决了。
注意:FLAG_ACTIVITY_NEW_TASK和singleTask模式配合使用时,要小心TaskAffinity的设置。如果两个Activity的TaskAffinity不同,它们会被分配到不同的Task中,导致返回行为不符合预期。
5.3 进程调度
AMS的进程调度,核心就是根据进程的重要性,决定什么时候该杀谁。这个机制叫Low Memory Killer(LMK),它和AMS紧密配合。
5.3.1 进程优先级体系
AMS把进程分为5个等级,每个等级对应不同的oom_adj值:
- 前台进程:正在和用户交互的Activity所在进程
- 可见进程:Activity可见但不处于前台
- 服务进程:正在运行Service的进程
- 后台进程:Activity已停止但进程还在
- 空进程:没有任何组件的进程
LMK会根据内存压力,从最低优先级的进程开始杀。我调试过一个案例:应用在后台播放音乐,结果被杀了。查日志发现是Service被系统判定为“可杀”,因为Service的优先级比可见进程低。后来我把Service绑定到前台通知,优先级就上去了。
5.3.2 AMS与LMK的协作
AMS和LMK的协作,是通过一个叫“proc”的数据结构完成的。AMS会更新每个进程的oom_adj值,LMK根据这个值决定杀谁。
// AMS更新进程优先级的核心逻辑
void updateOomAdjLocked(ProcessRecord app) {
// 根据Activity状态计算优先级
if (app.activities.size() > 0) {
ActivityRecord r = app.activities.get(0);
if (r.state == RESUMED) {
app.setOomAdj(ADJ_FOREGROUND); // 前台进程
} else if (r.state == PAUSED) {
app.setOomAdj(ADJ_VISIBLE); // 可见进程
} else {
app.setOomAdj(ADJ_BACKGROUND); // 后台进程
}
}
// 通知LMK更新
mProcessList.updateLruProcessLocked(app);
}
这段代码看起来简单,但实际逻辑要复杂得多。AMS还要考虑Service、BroadcastReceiver、ContentProvider等因素。我见过一个极端情况:一个进程同时有前台Activity和后台Service,AMS会取最高优先级,所以Service不会被杀。
核心要点:进程的优先级是动态变化的。AMS会根据当前所有组件的状态,取最高优先级作为进程的最终优先级。所以,如果你的应用需要保活,最好的办法是让用户看到你的界面。
5.3.3 进程冻结与解冻
Android 11引入了冻结机制。当应用进入后台一段时间后,AMS会冻结它的进程,停止所有线程的执行。这比直接杀进程更友好,因为恢复时更快。
冻结的触发条件:
- 应用进入后台超过一定时间(默认5分钟)
- 没有前台Service或通知
- 没有正在执行的JobScheduler任务
解冻的触发条件:
- 用户再次打开应用
- 有高优先级的广播需要处理
- 系统内存充足时自动解冻
我曾经遇到一个bug:应用被冻结后,AlarmManager设置的定时任务不执行了。查了源码才发现,冻结状态下AlarmManager的闹钟会被延迟。解决方案是用WorkManager替代AlarmManager,因为WorkManager有专门的唤醒机制。
避坑指南:如果你的应用需要在后台执行定时任务,不要依赖AlarmManager。建议使用WorkManager,它兼容冻结机制。我踩过这个坑,后来全部迁移到WorkManager,问题就解决了。
5.4 总结
AMS的核心原理,说白了就是三件事:管好Activity的生死、管好任务的栈结构、管好进程的优先级。这三件事相互关联,一个Activity的状态变化,会影响它所在Task的结构,也会影响它所在进程的优先级。
我做了这么多年Android开发,最大的体会是:理解AMS,就能理解Android系统的设计哲学。它把复杂的状态管理抽象成简单的栈结构,把进程调度和组件生命周期紧密绑定。这种设计思路,值得我们每个开发者学习。
嗯,今天就聊到这里。如果你在实际开发中遇到AMS相关的问题,欢迎来公众号找我交流。
公众号:蓝海资料掘金营,微信deep3321