28、Activity生命周期调试:ActivityManager、启动流程、生命周期回调、任务栈
说实话,Activity生命周期这块,很多Android开发都觉得“我懂了”。但真到了线上问题排查,或者要解释一个诡异的闪退时,往往就卡住了。我个人习惯把生命周期调试分成四个维度来看:谁在管(ActivityManager)、怎么启动的(流程)、回调怎么走(生命周期)、以及它们住在哪(任务栈)。今天咱们就一个一个掰开聊。
一、ActivityManager:幕后的大管家
ActivityManager(简称AM)其实是个系统服务。你写的startActivity(),最终都会通过Binder调用到系统进程里的ActivityManagerService(AMS)。
我在项目中遇到过一个问题:App启动特别慢,log里看到AMS一直在等某个锁。后来发现是某个三方SDK在Application.onCreate里做了太多同步操作,导致AMS分配ActivityRecord时被阻塞。说白了,AMS是单线程处理请求的,你主线程卡了,它也跟着卡。
关键点:AMS负责创建ActivityRecord、管理任务栈、调度生命周期。调试时重点关注AMS相关的log,tag通常是ActivityManager或am_开头。
二、启动流程:从点击到onCreate
一个Activity的启动,大致分这么几步:
- 发起请求:你的App通过Binder向AMS发送startActivity请求。
- AMS处理:AMS解析Intent,检查权限,创建ActivityRecord,决定放在哪个任务栈。
- 进程检查:如果目标Activity所在进程没启动,AMS会先让Zygote fork一个新进程。
- Application初始化:新进程里先创建Application,调用onCreate。
- 创建Activity:AMS通过Binder通知App进程,App进程的ActivityThread开始创建Activity实例,走生命周期。
嗯,这里要注意:Application.onCreate()是在Activity.onCreate()之前执行的。如果你在Application里做了耗时操作,第一个Activity的启动就会被拖慢。
调试技巧:用adb shell am start -W <包名/Activity全路径>可以查看启动耗时。输出里会有ThisTime、TotalTime和WaitTime,分别代表当前Activity、整个任务链、以及包含AMS调度的时间。
三、生命周期回调:不是只有onCreate/onDestroy
标准的生命周期回调大家都熟,但调试时真正有用的,是那些“边界情况”。
| 回调 | 触发时机 | 调试关注点 |
|---|---|---|
| onCreate | Activity第一次创建 | 检查savedInstanceState是否为空,判断是否冷启动 |
| onStart | Activity可见但不可交互 | 此时View已测量,但还没绘制完成 |
| onResume | Activity可交互 | 前台生命周期开始,注意不要在这里做重量级操作 |
| onPause | Activity失去焦点 | 必须快速完成,否则系统会认为ANR |
| onStop | Activity不可见 | 可以做一些轻量级保存,但别依赖它一定会被调用 |
| onDestroy | Activity被销毁 | 检查是否因为finish()还是系统回收 |
我曾经踩过一个坑:在onPause里做了网络请求,结果用户快速切换App时,onPause执行太久,系统直接杀了我的进程。后来我改成在onStop里做,但onStop也不保证一定执行——比如低内存时系统直接杀进程,onStop和onDestroy都不会走。
避坑指南:不要在onSaveInstanceState里保存大数据。这个方法在onStop之前调用,但如果你保存的数据太大,序列化时间过长,会影响切换体验。我曾经见过一个App在onSaveInstanceState里保存了整个列表的JSON,结果每次切后台都卡顿。
四、任务栈:Activity的“宿舍楼”
任务栈(Task)是存放Activity的容器。默认情况下,同一个App的Activity都在同一个栈里,后进先出。但通过launchMode和Intent Flag,你可以改变这个行为。
调试任务栈,我最常用的命令是:
adb shell dumpsys activity activities
这个命令会输出当前所有任务栈的信息,包括:
- 每个栈的ID(stackId)
- 栈里有哪些Activity(ActivityRecord)
- 每个Activity的状态(resumed、paused、stopped等)
- Affinity(亲和性)信息
举个例子,你看到输出里有:
Stack #0:
Task id #123:
* ActivityRecord{xxx u0 com.example/.MainActivity t123}
State: RESUMED
* ActivityRecord{yyy u0 com.example/.DetailActivity t123}
State: STOPPED
这说明当前栈顶是MainActivity,DetailActivity在它下面,处于STOPPED状态。如果你按返回键,DetailActivity会重新onRestart -> onStart -> onResume。
我个人习惯在调试多任务切换问题时,先跑一遍dumpsys activity activities,看看栈结构是不是预期的。比如singleTask启动模式,如果目标Activity已经在别的栈里,系统会把那个栈整个拉到前台。这时候看栈ID就能发现问题。
五、一张图看懂整体流程
下面这张SVG图,把Activity从启动到销毁的完整流程、AMS的角色、以及任务栈的关系串起来了。建议你保存下来,排查问题时对照着看。
六、调试实战:一个典型问题排查
假设你遇到一个Bug:从Activity A跳转到B,再按返回键,结果回到了桌面而不是A。你会怎么查?
我的排查步骤:
- 先跑
dumpsys activity activities,看任务栈结构。 - 如果发现A和B不在同一个栈里,检查B的launchMode是不是设置了
singleTask或singleInstance。 - 再看Intent里有没有加
FLAG_ACTIVITY_NEW_TASK。 - 最后检查Affinity,如果B的taskAffinity和A不同,系统会把它放到新栈。
我曾经就遇到过类似问题:一个第三方SDK的Activity设置了singleTask,结果每次跳转都创建新栈,返回时直接回到桌面。最后在Manifest里把taskAffinity改成和主App一致才解决。
快速定位技巧:在log里搜索am_create_activity和am_resume_activity,可以精确看到每个Activity的创建和恢复时间点。配合dumpsys activity,基本能覆盖90%的生命周期问题。
好了,Activity生命周期调试的核心内容就这些。记住:AMS是调度中心,生命周期是表象,任务栈是容器。下次遇到诡异的问题,先看栈结构,再看log时间线,最后检查launchMode和Flag。这套组合拳打下来,大部分问题都能定位。