做Android开发这些年,只要牵涉到页面跳转、冷启动优化、ANR定位,最后几乎都会绕回到同一个问题上:启动Activity时系统到底做了什么。很多同学背了一堆生命周期顺序,onPause、onStop、onCreate背得滚瓜烂熟,但一到线上问题就懵了——为什么有时候onNewIntent不回调?为什么adjustResize不生效?为什么后台拉起Activity会被系统拦掉?这些说白了,都是因为只记了结论,没理解启动流程本身。这篇就基于我长期排查问题、读源码的经验,把Activity从startActivity到界面真正展示的完整链路拆开讲清楚,包括涉及的进程通信、核心数据结构、生命周期事务的驱动方式,以及实际开发中可以直接用来排查问题的命令和思路。不管是刚接触源码的初级开发,还是已经被线上问题折磨过几轮的进阶选手,这篇都能提供一些可落地的参考。
1. 整体设计与核心思路拆解
1.1 一条启动链路牵扯的三方角色
Activity启动绝不是"当前页面调一下startActivity,新页面就自己蹦出来"这么简单。它本质上是一次跨进程协同,最少涉及三方:发起方所在的应用进程、负责全局调度与状态管理的系统服务进程(主要是AMS/ATMS),以及即将承载新页面的目标应用进程(如果目标应用还没启动,还要先完成进程创建)。
用生活中的例子来类比:你想去商场里一个新的餐厅吃饭(启动一个新Activity),你不能自己直接推门进去,得先通过商场服务台(系统服务)查询这家餐厅在几楼、有没有营业、需不需要排队。服务台确认之后,通知餐厅接待员(目标进程的ActivityThread)准备座位,然后你才被引导过去。整个过程中,商场服务台负责"认路"和"发号施令",餐厅负责"执行"。
放到系统里,**ATMS(ActivityTaskManagerService)**早期其实叫AMS(ActivityManagerService),后来谷歌把任务管理这块单独拆了出来,成了ActivityTaskManagerService,但从Binder通信层面看,应用进程最终还是通过AMS的代理接口把请求送进系统服务。搞清楚了这三方,后面看代码就不会迷路。
1.2 为什么要用Binder而不是直接方法调用
Android应用进程和系统服务不在同一个进程,这就引出第二个关键点:它们之间靠Binder驱动。Binder是Android整个IPC机制的底座,startActivity的调用链上至少有两次经典的Binder事务:
第一次,应用进程通过ActivityManager.getService().startActivity()把请求发给system_server进程里的ATMS;第二次,如果目标Activity所在进程没有创建,系统会通过Process.start()向zygote发起创建进程的请求,新进程起来之后通过ActivityThread.main()进入应用消息循环。之后系统服务要把"启动Activity"的指令下发到应用进程,走的仍然是Binder,只不过这里用的是IApplicationThread这个接口,由ActivityThread内部实现。
理解Binder在这一流程中的角色,你才能真正明白为什么"启动慢"会是性能问题的大头:一次跨进程通信至少一到两次sched切换,启动一个Activity如果链路上一口气走了十几次Binder调用,耗时自然叠加。
发起方应用进程 │ │ Binder: IActivityTaskManager.startActivity ▼ system_server(ATMS/AMS) │ │ Binder: IApplicationThread.scheduleLaunchActivity ▼ 目标应用进程(ActivityThread) │ │ 本地消息队列 ▼ Activity.onCreate / onStart / onResume1.3 一个容易被忽略的线程模型问题
另一个非常核心的设计是:应用进程里Activity所有生命周期回调,都发生在主线程(UI线程)的消息循环里。系统服务通过Binder把"启动Activity"的请求发送给应用进程时,ActivityThread内部收到的其实是一个ClientTransaction对象,里面包含了需要执行的一系列ClientTransactionItem,比如LaunchActivityItem、ResumeActivityItem。这些item不会立刻执行,而是被封装成Message抛到主线程的Handler队列中,等主线程空闲时再逐个处理。
这一点解释了无数面试题和线上问题的根源:为什么主线程卡顿会导致启动慢?不是系统服务执行得慢,而是消息队列里积压了太多消息,launch Activity的消息排不到前面去。反过来,你也就知道了为什么在主线程做耗时操作会被系统判死刑,因为紧随其后的生命周期事务根本没法执行。
2. 核心数据结构与重要概念
2.1 ActivityRecord、TaskRecord、ActivityStack
读启动流程源码时,几个数据结构绕不开,理解了它们,系统的"思路"就一目了然:
- ActivityRecord:一个Activity运行时的记录,包含Intent、包名、进程信息、生命周期状态等。可以说,系统里"一个Activity实例"对应的管理实体就是ActivityRecord。
- TaskRecord:一组ActivityRecord的集合,对应我们开发时感知的"任务栈/回退栈"。TaskRecord里维护了一个ActivityRecord的列表,栈顶即当前正在显示的Activity。
- ActivityStack:用来管理一组TaskRecord的容器,主要处理诸如"哪个Task在最前面""如何做启动窗口的切换"这类展示层事务。
- WindowContainer:更顶层的东西,Android 10以后把窗口和Activity的管理统一到了WindowContainer的树状层级里,这里不展开,但你要知道TaskRecord和ActivityRecord本身都是WindowContainer的子类,天然具备层级关系。
从taskAffinity到launchMode,最终影响的其实就是ActivityRecord被放进哪个TaskRecord、以什么姿态放进去。你在Manifest里配的singleTask、singleInstance,启动时传的FLAG_ACTIVITY_NEW_TASK,全是在ATMS做任务查找和栈操作时起作用的。
2.2 生命周期状态机与ClientTransaction
系统侧给Activity定义了多个状态,比如INITIALIZING、STARTED、RESUMED、PAUSED、STOPPED等。源码里ActivityState这个枚举定义得很直观,但普通人看启动流程只需要抓住一条主线:系统侧不断通过状态机推进Activity的状态,再通过ClientTransaction把"状态变化"翻译成应用进程里对应的生命周期回调。
ClientTransaction的设计值得一提。它里面包含了一个ActivityLifecycleItem(表示最终要到达的生命周期状态)和一个List<ClientTransactionItem>(中间要执行的一系列动作,比如LaunchActivityItem、ResumeActivityItem)。系统把这些封装好之后,一次性发给应用进程,应用进程再用TransactionExecutor按顺序执行。这样一来,一次Binder调用就能完成整个启动链路上多个生命周期回调的调度,减少了进程通信次数。
2.3 进程、任务与栈的层级关系
画个笼统的层级图就是:
一个ActivityManagerService管理多个ActivityStack,一个ActivityStack管理多个TaskRecord,一个TaskRecord管理多个ActivityRecord,每个ActivityRecord对应应用侧唯一的一个Activity实例。这个层级关系看起来复杂,但本质上是"容器套容器",掌握了这个模型,再看到"taskAffinity导致Task被切换""FLAG_ACTIVITY_CLEAR_TOP把栈顶之上全干掉"之类的行为,你就会知道都是哪一个层级上的操作。
3. 启动流程的关键路径逐段拆解
3.1 从startActivity到ATMS
拿最常见的情况举例:Activity A在主线程里执行了startActivity(intent)。这里有一个隐性知识点:如果你是在普通Context(比如ApplicationContext)里启动Activity,必须给Intent加FLAG_ACTIVITY_NEW_TASK,否则会直接抛异常。原因很简单——Activity必须挂在某个Task上,而ApplicationContext没有Activity所在的Task可依附,系统只能要求你开一个新任务。
接下来看调用链:
Activity.startActivity()最终会走到Activity.startActivityForResult()。Instrumentation.execStartActivity()登场,它负责拿着Intent去问系统"这个Activity能起吗"。Instrumentation这个名字很多人只在插桩测试里见过,其实它是应用进程与系统服务之间一个很关键的执行代理。ActivityTaskManager.getService().startActivity()开始跨进程,到这里控制权交给了system_server。
细节上,execStartActivity里还会校验调用者的权限,比如你带了一个不存在的Component,系统会在这里抛出ActivityNotFoundException。再比如Android 10以后对后台启动Activity的限制,有一部分校验逻辑也会沿着这条路径提前拦截。
3.2 ATMS如何找到正确的ActivityRecord和TaskRecord
进入系统服务之后,核心处理在ActivityTaskManagerService.startActivity(),它内部会穿到ActivityStarter.execute()。ActivityStarter是启动流程里一个非常关键的类,负责解析Intent、检查Activity信息、确定启动方式、查找到正确的Task,并最终把要启动的Activity塞进任务体系里。
这里有几个关键分支:
- Intent解析:如果是隐式Intent,系统要通过IntentFilter匹配到确切的Activity组件,匹配不到就抛异常。显式Intent则直接解析ComponentName。
- LaunchMode判定:解析Activity在Manifest里声明的launchMode,再叠加Intent里的Flags,综合决定这次启动是创建新实例、复用到栈顶,还是清空上方Activity。
- Task查找:根据Activity的
taskAffinity、launchMode等属性,去找有没有合适的TaskRecord可以复用。没有就新建。
这些逻辑最终收敛到一个方法:ActivityStarter.startActivityInner()。它向ActivityStackSupervisor上报,要开始对ActivityRecord执行"真正的启动动作"。
3.3 生命周期状态编排与ClientLifecycleManager
在Android 10之前,系统对Activity生命周期状态的调度散落在各种方法里,代码维护很痛苦。后来谷歌把"从当前状态推进到目标状态"这件事统一交给了ClientLifecycleManager,由它构造一个ClientTransaction,再调用IApplicationThread.scheduleTransaction()把事务发给应用进程。
具体到一次冷启动,目标进程可能完全不存在,那就必须先走进程创建流程:ATMS发现目标进程没起来,要告诉AMS(或者直接用进程管理模块)通过Zygote fork新进程。Zygote是Android系统的进程孵化器,本身是init进程启动的Native进程,所有应用进程都由它fork而来。fork之后,新进程入口是ActivityThread.main(),在这里会创建主线程的Looper和Handler,然后把自己attach到系统服务上。attach的作用是告诉AMS:我的进程已经准备好了,可以开始往我这边派发任务。
attach完成后,系统拿着应用进程的ApplicationThread代理,继续走ClientLifecycleManager的scheduleTransaction,把之前构建好的ClientTransaction投递给新进程。新进程的ActivityThread拿到事务后,会把它交给主线程Handler,最后在TransactionExecutor.execute()里真正开始执行。
3.4 应用进程侧:TransactionExecutor与生命周期回调执行
TransactionExecutor做的事情比想象中要谨慎。它拿到ClientTransaction之后,并不是一股脑按顺序执行list里的item,而是先根据目标生命周期状态,确定执行顺序。举个例子,如果目标是ON_RESUME,中间需要经历ON_CREATE、ON_START、ON_RESUME,它会把所有要执行到目标状态的必要的生命周期回调排列好,再逐步调用。
这里最值得注意的细节是:onPause和onStop的触发并不一定发生在新Activity启动的那次事务里。比如A启动B,A的onPause会在B启动前期就被调度,这样B才能在A之上稳定地获取焦点;但A的onStop往往会晚一些,可能等B完全启动、绘制完成之后,系统再回调A的onStop。理解了这个时序差,你再去排查"A切到B之后,A的某些资源还没释放"之类的问题,就不会手足无措。
LaunchActivityItem执行时,ActivityThread会通过反射创建目标Activity实例,然后调用attach()建立Activity与Window、WindowManager等系统的联系,再依次执行onCreate、onStart、onPostCreate等回调。这里还有一套与Window相关的逻辑:Activity创建的同时会创建PhoneWindow,并设置WindowManager。setContentView实际上就是把布局扔给PhoneWindow处理,最后在onResume之后,WindowManager才真正把DecorView添加到窗口上,界面才可见。
3.5 涉及的关键类速查
| 类/接口 | 作用 |
|---|---|
| Activity / ActivityThread | 应用侧入口,承载生命周期回调的实体 |
| Instrumentation | 应用进程内执行Activity启动的代理 |
| ActivityTaskManager / ActivityManager | 负责与AMS/ATMS通信的Binder客户端 |
| ActivityStarter | 系统侧解析并编排启动逻辑的核心类 |
| TaskRecord / ActivityRecord / ActivityStack | 系统侧管理任务栈与Activity状态的实体 |
| ClientLifecycleManager / ClientTransaction | 生命周期事务的封装与下发 |
| TransactionExecutor | 应用侧执行生命周期回调的调度器 |
| ApplicationThread / IApplicationThread | 应用进程与系统服务的Binder回调接口 |
4. 启动模式与Flags对流程的影响
4.1 launchMode对Task的干预
如果启动流程只是"新建Activity,压栈,展示",那系统设计就太简单了。实际上,随着APP页面越来越复杂,开发者需要精细控制Task的复用和清理,于是就有了launchMode和一堆操作Task的Flags。
四种launchMode的行为差异,核心在于"如何复用ActivityRecord、要不要清空Task":
- standard:每次创建一个新的ActivityRecord,压入当前Task。默认模式,最符合直觉。
- singleTop:如果当前Task栈顶已经存在同一个Activity实例,不会新建,而是回调它的onNewIntent;否则新建。
- singleTask:系统会在目标Activity的taskAffinity对应的Task里查找是否存在该Activity实例。存在则把Task调到前台,并清空该Activity之上的所有其他Activity,然后回调onNewIntent;不存在则新建Task或复用一个空的Task。
- singleInstance:比singleTask更激进,该Activity所在的Task里只能有它一个Activity。通常用于来电界面、闹钟这类需要独占任务的场景。
用错launchMode是线上问题的高发区。最常见的是:给分享页配了singleTask,用户从A页面分享到微信,再回到APP时,发现A页面之上的所有页面都被清了。你会觉得"我没写过清理代码啊",但实际上就是singleTask在"维护任务栈一致性"时把上方的记录全移除了。
4.2 Intent Flags的叠加规则更易踩坑
Flags是另一个维度的控制手段,而且和launchMode是叠加生效的。你需要重点掌握的几个:
FLAG_ACTIVITY_NEW_TASK:在新的Task中启动Activity,常与taskAffinity配合使用。FLAG_ACTIVITY_CLEAR_TOP:如果目标Activity已经存在于Task中,则清空它之上的所有Activity,并把它带到前台。FLAG_ACTIVITY_SINGLE_TOP:相当于launchMode的singleTop。FLAG_ACTIVITY_CLEAR_TASK:启动前把Task清空。FLAG_ACTIVITY_REORDER_TO_FRONT:把已有的Activity调整到栈顶,但不清空栈里的其他实例。
实际开发里,我建议在起页面之前,明确想清楚"这次跳转,我要的是复用,还是新建?" 如果只是简单的页面推进,尽量用默认行为。一上来就全套Flag叠加,后面排查问题时完全是噩梦。
4.3 taskAffinity与进程和栈的隐式关联
taskAffinity比很多人以为得更隐蔽。它不只是singleTask的辅助属性,还会影响FLAG_ACTIVITY_NEW_TASK启动时选择哪个Task,以及allowTaskReparenting时Activity会不会"搬家"。很多大厂APP里出现"从通知栏点进来,返回之后回到了某个完全不搭界的中间页",大概率就是taskAffinity设置导致的Task切换。
举一个真实案例:某APP从MainActivity跳到了WebViewActivity,WebViewActivity配了singleTask并且没有特别设置taskAffinity。因为Manifest里Application没有设taskAffinity,默认就是包名。这时在MainActivity的任务栈里,WebViewActivity可以完美压栈,不会有问题。但如果某个第三方SDK启动了同一个WebViewActivity,而且它在一个taskAffinity完全不同的Task里,那么singleTask的WebViewActivity会基于自己所属的包名去找Task,结果可能跳到另一个Task去。这个问题排查起来特别绕,因为Activity明明在当前栈里已经有了。
5. 冷启动与生命周期联动的实战问题
5.1 冷启动:从点击图标到首帧显示
Activity启动流程和APP冷启动流程并不是完全一回事,但两者有大量重叠。冷启动指的是进程不存在,从Launcher点击图标(或者从系统接收一个Intent)开始,到MainActivity第一帧真正显示出来。
整个冷启动可以分成几个阶段:
- 进程创建:system_server通过Zygote fork出新进程。这里有一个常见指标
Process.preLoad耗时,主要是ClassLoader、资源、系统服务代理的预加载。 - Application初始化:ActivityThread进入main之后,首先创建Application,执行attachBaseContext和onCreate。
- Activity创建与首帧:执行MainActivity的onCreate、onStart、onResume。onResume返回之后,系统发出第一帧绘制请求。注意,这里说的"首帧"不是Activity完全可见,而是DecorView完成了测量、布局、绘制并提交给SurfaceFlinger。
- 显示:SurfaceFlinger合成并上屏。
性能优化的很多手段都围绕这几个阶段展开:减少Application.onCreate里的任务、用启动器预加载、延迟初始化、懒加载、ContentProvider优化等。
之所以要强调"Activity启动流程",是因为你在做启动优化时,如果不清楚onCreate、onStart、onResume在启动链路中的位置,很可能优化目标打偏。比如,你花了大力气把onCreate的耗时减掉了,但如果首帧绘制卡在View的measure/layout,或者卡在Application初始化,那优化效果依然不明显。
5.2 后台启动限制对流程的干预
Android 10开始,系统对后台启动Activity做了严格限制。所谓"后台",是指应用进程当前没有可见Activity,或者处于某些不被系统认为"前台"的状态。限制的直接效果是:你在后台执行startActivity,可能被系统静默拦截,连异常都不抛。
了解这个限制,必须结合启动流程来看:解读Intent、查找Task这些步骤照常,但在ActivityStarter真正向应用进程下发生命周期事务之前,系统会先判断发起方的进程状态和有没有可见窗口。如果不符合要求,启动请求会被标记为"不允许",记录到log里,不会执行。
实际开发里,最常见的冲突场景是:推送SDK在收到离线消息后,尝试直接拉起某个Activity;或者长连接回调在进程没有可见界面时跳首页。这些行为在旧版本Android上一直能跑,到了Android 10+就神秘失效。正确做法是改用通知栏,或者使用系统允许的例外场景(比如应用有SYSTEM_ALERT_WINDOW权限、有可见的activity、最近任务里存在该任务等)。但注意,这些豁免条件在不同版本上细节有差异,需要用dumpsys activity实际查看进程的oom_adj和对应的ActivityRecord状态来判断能否拉起。
5.3 为什么onNewIntent经常不到达
这个问题出现频率非常高。很多人误以为只要设置了singleTask或singleTop,再次启动时onNewIntent就必然会回调。实际上,onNewIntent是否回调,取决于系统在复用已有ActivityRecord时走的具体分支:
- 对于singleTop:只有新的启动请求目标是栈顶Activity时,才走onNewIntent;如果目标Activity在栈顶之下,singleTop根本不会复用,而是新建一个实例。
- 对于singleTask:复用时会把位于目标Activity之上的页面全部清掉,然后回调onNewIntent。但如果目标Activity根本不在同一个Task里,系统可能新建Task或复用别的Task,行为又不一样。
此外,从Android 10之后,某些从通知栏或外部唤起的场景,是由系统直接把Intent附加到Task上,绕过了常规的startActivity路径,这时候onNewIntent也可能不回调,你需要检查Activity.getIntent()的pendingIntent行为。这个点极其隐蔽,我见过线上反馈"从通知点进来,页面数据不刷新",排查到最后才发现是onNewIntent没走,系统直接复用了已有Activity的实例,还在旧Intent上继续展示。
6. 生命周期回调与启动流程的对应关系
6.1 一个完整的冷启动回调时序
下面是一个标准冷启动、进程不存在、从Zygote fork到MainActivity显示的回调顺序(以Activity A为起点,启动Activity B为例):
- A被创建:Application
onCreate-> A的onCreate->onStart->onResume,此时A完全可见。 - A调用
startActivity(B)。 - 系统先让A进入Paused状态:A
onPause被调用。 - 如果B所在进程不存在,创建进程,初始化Application。
- B实例创建:B
onCreate->onStart->onResume,B完全可见。 - 系统再让A进入Stopped状态:A
onStop被调用。 - 如果A被完全覆盖,且用户没再看见它,之后可能还会走
onDestroy,但这取决于A是否被销毁(比如内存不足,或者被finish,或者Task被清理)。
这个顺序经常被误解:很多人以为A的onStop一定在B的onResume之前,实际上从Android 7.0开始,系统的调度顺序已经变成"先确保新Activity能赶紧展示,再收拾旧的"。所以A的onStop往往迟于B的onResume。
6.2 onSaveInstanceState与启动流程的关系
系统在Activity被"销毁但状态需要保留"时,会调用onSaveInstanceState。启动流程中,它出现的时机也很有讲究:比如A启动B,如果系统预测A可能因为内存不足被回收,它会提前调用A的onSaveInstanceState保存UI状态。因此你在onCreate的savedInstanceState参数里能恢复数据;如果A没有被回收,savedInstanceState大概率是null。
结合启动流程,你还需要注意:配置变更(旋转屏幕)会让系统销毁并重建Activity,但这个过程走的是ActivityThread.handleRelaunchActivity,它和普通的startActivity链路不一样。它不会创建新的Task、不需要走ActivityStarter,而是在同一个ActivityRecord上重新执行onDestroy->onCreate这样一轮生命周期。很多人在配置变更时发现onCreate里的intent还是老的,其实是因为系统重建时会把之前的Intent重新传进去,你需要在onCreate里及时读取最新的state。
6.3 启动模式对时序的实际影响
launchMode不仅影响"谁在栈顶",也直接影响生命周期的回调顺序。举一个例子:A(standard)启动B(singleTask)。B在另一个Task里已经存在。这时系统会先把B所在的Task移至前台,回调B的onNewIntent。同时A所在的Task退到后台,A会依次走onPause、onStop。你的A如果依赖onStop来释放摄像头、麦克风等资源,就要考虑清楚:从A跳B时,A到底会不会立刻收到onStop,这取决于B能否快速显示和B的启动方式。如果是单进程、页面较复杂,onStop延迟几百毫秒甚至更久都有可能。
7. 实际排查工具与技巧
7.1 dumpsys activity让你看清系统视角
掌握了启动流程,最高效的验证方式就是直接用系统工具看运行时状态。adb shell dumpsys activity有几个子命令非常实用:
dumpsys activity activities:打印所有Task栈和ActivityRecord的当前状态。dumpsys activity processes:查看所有运行进程的oom_adj、pid、进程里包含的Activity。dumpsys activity top:查看当前焦点Activity及其所在的Task。dumpsys activity service:查看Service绑定和调度情况。
排查"页面为什么起不来"这类问题时,我会走一遍这个流程:
- 从日志里找到ATMS打印的
START日志,关键词类似START u0 {intent} from uid ...。如果这里直接报错,看是ActivityNotFoundException、SecurityException,还是后台限制。 - 到
dumpsys activity activities里确认ActivityRecord是否创建,如果创建了但状态卡在STOPPED或INITIALIZING,说明应用进程侧没收到事务,或者事务执行中途卡住。 - 到
dumpsys activity processes里看目标进程是否存在、pid有没有变化、线程堆栈是否有主线程阻塞。
这套方法比纯看日志要靠谱得多,因为它直接反映了系统侧的状态。
7.2 adb shell am start 手动模拟启动
想快速验证某个Activity在某个launchMode下的行为,不需要写代码,用adb shell am start就能模拟。比如:
adb shell am start -n com.example.app/.MainActivity adb shell am start -n com.example.app/.MainActivity --activity-single-top adb shell am start -a android.intent.action.VIEW -d "https://example.com"结合dumpsys activity activities观察Task和ActivityRecord的变化,你就知道FLAG、launchMode到底怎么影响栈结构。遇到"微信分享回跳后页面状态不对"一类的问题时,这一招尤其有效,可以快速排除是不是Intent传递的问题。
7.3 systrace/Perfetto看启动耗时分布
当问题从"能不能启动"变成"为什么这么慢",就要上性能分析工具。现在主流推荐用Perfetto(systrace的替代者),抓取方式:
# 在较新的Android设备上 adb shell perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace -t 10s sched freq idle am wm gfx view抓完之后在Perfetto UI里重点看几个片段:
AppProcessCreate:进程创建耗时。bindApplication:Application初始化耗时。ActivityTaskManager里的LaunchActivity、Displayed相关标记。Choreographer的帧耗时。
主线程出现长时间HandleMessage占用,基本可以定位到代码里的耗时块;如果卡在Binder调用上,则要看系统服务侧的繁忙程度。
7.4 通过日志快速定位启动失败的几类原因
| 症状 | 常见原因 | 排查方向 |
|---|---|---|
| 点击无反应 | 后台启动限制 | 检查应用是否有可见窗口,查看ATMS START日志里是否带有blocked标志 |
| 崩溃且没有异常 | 目标Activity设置了exported=false | 检查Manifest导出配置 |
| 页面跳转后瞬间闪退 | Application初始化异常 | 看logcat中的FATAL EXCEPTION,重点看ContentProvider和Application.onCreate |
| onNewIntent未回调 | Task里Activity不在栈顶 | dumpsys查看ActivityRecord和Task的栈顶情况 |
| 返回键回到老页面而不是首页 | 多个Task被复用 | 检查taskAffinity和FLAG_ACTIVITY_NEW_TASK的配合情况 |
8. 高级实践:自定义方案与优化经验
8.1 基于启动流程做冷启动优化
启动优化本质上是压缩从"进程创建"到"首帧显示"的链路时间。按我的经验,优先级是这样:
- Application初始化阶段最需要收敛。所有ContentProvider里的初始化都发生在Application.onCreate之前,如果你的SDK在ContentProvider里做了大量懒加载设计,一定要查一遍启动阶段实际执行了哪些。能改成懒加载就改,改不掉的想办法放到子线程,但要注意子线程加载和主线程使用之间的同步。
- 首帧Activity的onCreate只做必要的视图初始化和数据展示。常见误区是把网络请求、数据库操作都放在Activity.onCreate里,导致首帧被拖慢。正确做法是主线程先渲染骨架,数据到了再填充。
- **启动窗口(StartingWindow)**优化。Android 12上系统提供了启动画面(SplashScreen)API,可以控制启动窗口的显示内容和持续时间。如果你不做任何配置,系统会用默认主题和图标生成一个启动界面,首帧上来后退出。这个窗口和Activity的启动流程是强相关的,充分利用它可以在视觉上大幅优化"点击到出内容"的等待感。
- 预加载Zygote相关的类,一般App用不上,但对大厂来说,尽量减少冷启动时fork进程后的类加载压力,能稳定压缩几十毫秒。
8.2 跳页提速的三个行之有效的方法
除了冷启动,日常页面跳转提速也有很多手段。我这里列三个比较有效的:
- 减少主线程事务排队:跳转前不要在主线程做耗时操作,比如复杂计算、大对象创建、SharedPreferences写盘等。这些操作会导致
scheduleLaunchActivity事务被执行的时间延后。 - 用startActivityForResult替代onActivityResult的全局分发:如果你在一个复杂页面跳转后需要回传数据,尽量用明确的requestCode + resultCode路径,避免在onResume里反复读取全局变量,导致逻辑混乱和额外性能损耗。
- 适当使用单例/对象池复用页面数据:对于列表页跳详情页这种模式,详情页每次重新创建,onCreate里如果都要查一次数据库或请求网络,体验会差很多。可以把分页数据和列表位置状态暂存在内存缓存里,页面重建时直接从缓存恢复,减少首帧等待。
8.3 后台Activity被回收后的恢复策略
系统在内存吃紧时可能杀掉后台Activity所在进程。用户回到这个界面时,系统会尝试按Task里的ActivityRecord做恢复:如果进程已死,系统会重建进程,再用之前保存的savedInstanceState恢复Activity。
这个恢复流程,本质上是启动流程里"启动已存在但进程已死"的ActivityRecord的特殊分支。恢复时TargetActivity的onCreate会收到非空的savedInstanceState。要想不丢状态,你需要:
- 在onSaveInstanceState里保存关键UI状态(列表位置、输入内容、滚动位置)。
- 在onCreate里判断savedInstanceState,能恢复就及时恢复。
- 避免在重建后的Activity里再依赖原来进程里的静态变量。静态变量在进程被杀后全部消失,这是很多"后台切回后白屏"问题的直接原因。
这一步一定要配合启动流程理解:ActivityRecord还在Task里,系统只需要重启进程,重新创建Activity即可。它不是一次普通启动,不会走ActivityStarter里新建Task的逻辑,而是走ActivityStackSupervisor.realStartActivityLocked之类的恢复路径。所以你onCreate里的Intent、启动模式,都和首次启动不一样。
9. 常见问题与排查思路实录
9.1 点击图标无响应但是其他App正常
这种情况要优先怀疑system_server本身是否卡住。因为所有App的启动都要通过system_server的ATMS调度。如果它主线程卡死(比如系统服务里有人做了慢查询),你会看到大量App的启动请求都排在后面。排查方式就是看系统日志里有没有ANR in system_server,或者用adb shell top看CPU占用。
如果只有本App无响应,再看dumpsys activity processes里本App进程是否存在。如果进程活着但Activity没起来,多半是主线程卡死。抓一次ANR trace,看主线程堆栈卡在哪个方法上即可定位。
9.2 startActivity抛异常:Calling startActivity from outside of an Activity context
这条异常上文已经提过,是因为使用了ApplicationContext或ServiceContext启动Activity,但没有设置FLAG_ACTIVITY_NEW_TASK。系统为了保护Task结构,在ContextImpl.startActivity里做了校验:如果context不是Activity类型,就必须有NEW_TASK标志。
解决方式就是补充标志:
Intent intent = new Intent(context, TargetActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);但注意,加了NEW_TASK之后,新Activity会进入一个新Task。如果你的设计是希望它和主流程Task在一起,就要考虑taskAffinity;否则返回时会直接离开整个任务栈,体验很怪。
9.3 onActivityResult回调延迟甚至不回调
onActivityResult被废弃之后(AndroidX Activity 1.3.0加入了ActivityResult APIs),但仍有大量老代码在用。它与启动流程的关联在于:系统在startActivityForResult时记录了发起方ActivityRecord,并在目标Activity finish时回传结果。如果启动活动走的是singleTask复用路径,并且目标Activity和发起方不在同一个Task,那么返回时结果可能不会准确回传。
更隐蔽的问题在于:启动后发起方Activity因为内存被杀,恢复后的新实例其实是一个新的ActivityRecord,但系统仍持有旧的ActivityRecord引用,结果回传时会找不到原来的回调对象。这就是为什么旧版Fragment里onActivityResult经常失灵。迁移到ActivityResultLauncher之后,因为底层用了新的回调注册机制,这类问题少了很多。
9.4 启动窗口(Splash)一直不退
Android 12上如果采用默认启动窗口,首帧绘制完成后系统会自动移除启动窗口。但如果你做了自定义主题,把启动窗口的windowBackground设置成了一个复杂drawable,或者调用了SplashScreen相关API但返回时机不对,启动窗口可能长时间不消失。
排查上,建议先确认首帧是否及时。如果首帧很慢,启动窗口当然一直展示;如果首帧已出但启动窗口还在,多半是主题或API配置问题。你用Perfetto抓trace时,可以看到StartingWindow从创建到移除的时间节点,对照Activity首帧的时间点,很快能分清是哪一边的问题。
9.5 多进程应用与启动流程的冲突
如果你的App开了多进程,要尤其注意:只有当"目标Activity所在进程"没有创建时,系统才会fork新进程。如果你在A进程里启动了B进程的Activity,B进程要额外创建,而B进程里的Application也会再次执行onCreate。这可能触发一些耗时逻辑,拖慢Activity启动。更麻烦的是,多进程之间不能共享内存状态,所以静态变量、单例全都得小心处理。判断应用开了几个进程最简单的方式是:adb shell ps | grep 包名,看到多个PID即说明多进程。
最后的实践心得
Activity启动流程这块,读源码的时候我最大的感受是:不要一开始就陷进ActivityTaskManagerService那一大坨代码里,先把"应用进程、系统服务、Binder、消息队列"这几个核心概念在脑子里立住,再顺着一次startActivity的数据流往下追,会轻松很多。真到了线上排查,也别慌,先看系统侧状态,再判断是系统调度问题还是应用主线程问题,一步步缩小范围。你现在遇到的绝大多数诡异跳页、生命周期混乱、启动崩溃,基本都能在这条链路里找到答案。最后再分享一个我自己常用的习惯:每次提交和Activity跳转相关的代码,都顺手跑一遍adb shell dumpsys activity activities,哪怕功能正常也要确认Task里各ActivityRecord的状态是否符合预期。很多问题在早期就是从那多出来的一条记录开始的。