聊到Android开发,Activity启动源码解析绝对是被问烂了、也最容易翻车的一道题。面试官爱考,同事聊天爱聊,可真正能把“从startActivity按下到onResume执行”这条链路完完整整讲清楚的,说实话不多。我自己也是啃了几年源码、实际排查过一堆启动相关问题之后,才把整条链路理顺的。
这篇文章我会基于Android 13的AOSP源码,把Activity启动的完整流程拆开揉碎讲一遍。不光是看代码路径,更重要的是讲清楚每一步代码“为什么存在”、每一步在解决什么问题。如果你是正准备面试、或者被线上启动问题折磨得头皮发麻的开发者,这篇文章应该能帮你在脑子里建立一张真正的“调用链地图”,而不是死记硬背几个类名。
先说清楚范围:我们聚焦“冷启动一个新的Activity”,也就是从应用进程内部调用startActivity开始,直到目标Activity执行到onResume结束。涉及的类比较多,我会用“应用进程内、系统进程内、新进程创建、回到应用进程”这四个阶段来切分,每一段的代码都给出关键走向,并解释背后的设计逻辑。
1. 启动链路全景:先建立地图,再看细节
1.1 一条启动链路,本质上是“三段转手 + 一次进程fork”
Activity启动之所以复杂,最大的原因是它横跨了两个进程:发起方和应用自身所在的进程、系统服务进程SystemServer。你调用的startActivity并不是在自己的进程里直接把Activity new出来,而是先把请求通过Binder跨进程发给系统服务,由系统服务来“裁决”该不该启动、启动谁的、放在哪个任务栈,最后再决定是复用现有进程还是fork一个新进程来承载这个Activity。
所以这张地图可以这么记:
- 第一段:应用进程内,从
Activity.startActivity一路走到Instrumentation.execStartActivity,最终通过ActivityTaskManager.getService().startActivity把请求抛给系统服务。 - 第二段:系统服务进程内,
ActivityTaskManagerService(ATMS)接收请求,交给ActivityStarter做启动决策,找到或创建对应的ActivityRecord和TaskRecord,再通过ActivityTaskSupervisor确认目标进程是否存在。 - 第三段:如果目标Activity所在进程不存在,就需要Zygote fork新进程。新进程启动后,
ActivityThread在main方法里创建主线程Looper,并attach到系统服务,把之前挂起的启动请求继续推进。 - 第四段:回到应用进程,
ActivityThread通过TransactionExecutor执行系统服务下发的一组“执行条目”,其中最关键的是LaunchActivityItem,它负责真正通过类加载器创建Activity实例,并依次触发onCreate、onStart、onResume。
1.2 为什么推荐从Android 10之后的源码入手
如果你现在去搜Activity启动流程的老文章,会发现很多还在用ActivityManagerProxy、ActivityStackSupervisor这些已经被改写的类名。Android 10是一个重要分水岭,从这一版开始,系统把Activity启动改成了基于ClientTransaction的“事务式”模型:系统进程不再在Binder回调里一条一条调用ApplicationThread的各个方法,而是把一批要执行的动作(比如启动Activity、恢复Activity)打包成一个ClientTransaction,里面装着一组ClientTransactionItem,一次性发给应用进程,由应用进程的TransactionExecutor在目标线程里统一执行。
这个改动解决了几个实际问题:减少了Binder跨进程次数、保证了回调顺序的一致性、也让系统进程的代码更内聚。理解这个模型,比纠结某个老版本里realStartActivityLocked的具体行号更有价值,因为整个设计思想在后续版本里是延续的。
2. 关键类职责速查:别让类名把你绕晕
2.1 一张表认全启动链路里的核心角色
Activity启动源码难读,很大程度是因为类名长得像、职责又高度耦合。我做了一个速查表,建议先把这张表刻在脑子里再往下看代码。
| 类名 | 所在的进程 | 核心职责 |
|---|---|---|
Activity | 应用进程 | 启动发起方,调用startActivity的地方 |
Instrumentation | 应用进程 | 应用进程内监控Activity创建和生命周期回调的“插桩管家” |
ActivityTaskManager | 应用进程 | 向系统服务发起Binder调用的门面类,提供getService() |
ActivityTaskManagerService(ATMS) | 系统进程 | 系统服务侧负责Activity任务管理的大管家,入口是startActivity |
ActivityStarter | 系统进程 | 启动决策引擎,负责解析Intent、判断启动模式、计算任务栈策略 |
ActivityTaskSupervisor | 系统进程 | 统筹各个任务栈的调度员,负责真正执行“启动一个Activity”的收尾动作 |
ActivityRecord | 系统进程 | 系统侧用来描述“一个Activity实例”的数据结构,保存Intent、启动模式、token等 |
TaskRecord | 系统进程 | 系统侧的任务栈,用来组织一堆ActivityRecord |
ActivityStack | 系统进程 | 包含一组TaskRecord的容器,管理栈内前后台状态 |
ProcessRecord | 系统进程 | 系统侧的应用进程描述,记录进程状态、uid、进程名等 |
ActivityThread | 应用进程 | 应用进程的主线程宿主,Activity的实际启动和管理都发生在它内部 |
H | 应用进程 | ActivityThread的内部Handler,主线程消息调度全靠它 |
ClientTransaction | 跨进程 | 一组待执行动作的封装,系统侧构造,应用侧执行 |
LaunchActivityItem | 跨进程 | ClientTransaction内部的一个Item,负责真正创建Activity并触发onCreate |
2.2 ActivityRecord和TaskRecord是理解一切的基础
我见过很多人把ActivityRecord当成Activity,其实不准确。更合适的说法是:ActivityRecord是系统进程侧维护的“Activity档案”,它保存了Activity的包名、类名、Intent、启动模式、所在进程、状态等信息。系统服务每次启动决策的核心,就是要为一个Intent找到或创建一个合适的ActivityRecord。
TaskRecord则对应我们常说的“任务栈”,它是ActivityRecord的容器。从Android 10开始,系统对任务栈的管理更加细化,Task几乎成了第一公民,ActivityStack反而退化为一个包着多个Task的壳。所以看新源码时,你会看到大量对Task的操作,比如把某个ActivityRecord移动到栈顶、复用栈内已有的ActivityRecord、清空某个Task等。
理解这两者的关系后,很多现象就能解释通了。比如你启动一个singleTask模式的Activity时,系统做的事就是“找到一个已有的ActivityRecord,把它所在的Task拉起来,再把栈顶的ActivityRecord替换成它”。这背后全是针对TaskRecord和ActivityRecord的操作,而不是单纯地new一个对象。
3. 应用进程到系统进程:startActivity是怎么出发的
3.1 Activity.startActivityForResult:所有启动请求的统一入口
不管你在代码里写的是startActivity(intent)还是startActivityForResult(intent, requestCode),最终都会收敛到Activity.startActivityForResult这个私有方法。它做的主要事情有:检查Intent是否为空、检查是否设置了FLAG_ACTIVITY_NEW_TASK等关键标志、准备.ActivityResult回调逻辑、然后进入Instrumentation。
这里有一个容易被忽略的细节:如果当前Activity已经处于finishing状态,系统会直接跳过启动请求,避免在销毁过程中的Activity继续触发新页面。源码里对应的是:
if (mActivityResult != null) { mActivityResult.mResultCode = ... // 需要交给外部处理结果 } else if (mActivityResult == null && options != null) { // 处理过渡动画等options } if (parent != null) { ... }这块逻辑虽然看起来繁琐,但你要理解的是:系统在每次启动请求前,都会先检查当前Activity是否还有资格发起这次启动,这是Android 10之后后台启动Activity限制的底层基础之一。
3.2 Instrumentation.execStartActivity:应用进程的最后一道关口
离开Activity.startActivityForResult之后,代码进入Instrumentation.execStartActivity。Instrumentation这个名字很多人不陌生,android单元测试里经常用到,但平时开发中它处于“隐身”状态。实际上,系统在应用进程里的所有Activity生命周期回调,几乎都经过它:创建Activity调用newActivity、执行onCreate调用callActivityOnCreate、执行onResume调用callActivityOnResume。
execStartActivity的关键代码大致是:
public ActivityResult execStartActivity(...) { try { intent.migrateExtraStreamToClipData(); intent.prepareToLeaveProcess(who); int result = ActivityTaskManager.getService().startActivity(whoThread, who.getOpPackageName(), who.getAttributionTag(), intent, token, targetSDKVersion, requestCode, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { ... } }注意这里的ActivityTaskManager.getService()返回的是一个IActivityTaskManager接口的Binder代理。也就是说,从这一行开始,请求就正式跨进程了。checkStartActivityResult还会检查系统服务返回的结果码,如果返回的是START_ACTIVITY_NOT_FOUND之类的错误码,这里会直接转换成ActivityNotFoundException抛给你。
3.3 Binder穿透到ATMS.startActivity
系统服务进程侧,ActivityTaskManagerService实现了IActivityTaskManager接口,所以Binder穿透后的入口就是ATMS.startActivity。这个方法的实现非常干净,核心只有几行:
@Override public final int startActivity(IApplicationThread caller, String callingPackage, String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, Bundle options) { return mActivityTaskSupervisor.getActivityStarter() .execute(...); }这里出现了一个新的关键对象:ActivityStarter。它负责处理启动请求的所有决策逻辑。之所以单独拆一个类,纯粹是因为“启动决策”太复杂了:要解析Intent、要处理启动模式、要考虑多个Task之间的复用关系、要校验调用权限、要处理动画和过渡、要处理冷热启动分支。把这些逻辑全部放在ATMS里,会让系统服务类庞大到无法维护,所以单独拆出了ActivityStarter。
4. ActivityStarter:系统进程里的决策引擎
4.1 execute()与executeRequest()的职责划分
ActivityStarter.execute是启动决策的总入口。它内部会进行一系列前置条件判断,核心走向是进入executeRequest方法。在这个方法里,系统依次完成:
- 校验启动来源进程的合法性,比如调用方进程是否可信、是否被隔离。
- 解析Intent,也就是调用
intent.resolveActivityInfo(packageManager, flags)找到真正要启动的组件。 - 检查Intent安全性、权限、
component是否explicit等。 - 处理
ActivityRecord的创建,以及任务栈的匹配逻辑。
这一步最核心的产出,是要得到一个“该往哪个Task、哪个位置放置Activity”的决策结果。整个ActivityStarter处理完之后,会返回一个结果码,比如START_TASK_TO_FRONT、START_ACTIVITY_NOT_FOUND、SUCCESS等。
4.2 启动模式与Intent Flag在源码里是怎么落地的
Activity启动模式是面试高频题,但源码里真正的决策点在ActivityStarter内部。系统会通过computeLaunchingTaskFlags处理Intent中带的Flag,再结合manifest里声明的launchMode算出最终的启动方式。简化后的判断逻辑是这样的:
- standard模式:每次启动都会创建新的
ActivityRecord,放入启动者所在任务的栈顶。这也是最符合直觉的模式。 - singleTop模式:如果当前任务的栈顶Activity已经是目标Activity的实例,就不创建新的,而是复用栈顶并触发
onNewIntent。源码中这个判断在ActivityStarter的isSameActivity相关逻辑里。 - singleTask模式:系统会先在现有Task里找有没有目标Activity。如果找到了,就把那个Activity所在Task整个拉到前台,并把该Activity之上的所有Activity清空,让目标Activity成为栈顶,同时触发
onNewIntent。这个模式在源码中会走TaskReuse的逻辑,也就是setTaskToBeReused或者reuseTask相关分支。 - singleInstance模式:在singleTask基础上,连Task都是独占的。系统会单独新建一个Task来放这个Activity,并且这个Task里永远只有它一个ActivityRecord。
Intent Flag的优先级高于manifest里的launchMode。也就是说,如果manifest里写的launchMode="singleTask",但你启动时给Intent加了FLAG_ACTIVITY_NEW_TASK和FLAG_ACTIVITY_CLEAR_TOP,最终走的行为会按Flag组合来算。曾经有同事调试了半天,以为是manifest声明没生效,其实是启动代码里的Flag把声明覆盖了,这个坑值得记一下。
4.3 为什么是Task的复用与移动,而不是简单add
很多新手以为Activity启动就是“往任务栈里push一个新Activity”。实际上,系统在决策时会优先考虑能不能复用现有的ActivityRecord。举个最常见的例子:A -> B -> C,然后在C里用FLAG_ACTIVITY_CLEAR_TOP启动A。系统并不会直接压入一个新的A,而是会从C所在的Task里找到A对应的ActivityRecord,然后把它上面的B和C全部finish掉,把A移动到栈顶并触发onNewIntent。
源码层面,这个过程涉及Task的多个操作,比如moveActivityToFront、performClearTop、removeActivityFromHistory等。理解这个“复用优先”的设计,你才能真正解释为什么有些场景下onCreate不会再次触发,而只是onNewIntent被调用了。
5. 从决策到执行:系统怎么让应用进程开工
5.1 startSpecificActivity:检查进程,决定要不要fork
ActivityStarter完成决策得到一个可以启动的ActivityRecord后,并没有结束。下一棒交给ActivityTaskSupervisor。这里的关键方法是startSpecificActivity,它的大致逻辑是:
void startSpecificActivity(ActivityRecord r, boolean andResume, boolean checkConfig) { ProcessRecord app = mService.getProcessRecordLocked(r.processName, r.info.applicationInfo.uid); if (app != null && app.getThread() != null) { // 进程已存在,直接走正常启动逻辑 try { realStartActivityLocked(...); return; } catch (RemoteException e) { } } // 进程不存在,需要先创建进程 mService.startProcessAsync(...); }这里基本决定了“热启动”和“冷启动”的分水岭:如果目标Activity所在进程已经存在,系统会直接通过realStartActivityLocked走启动流程;如果进程不存在,就需要进入另一个完全不同的路径——通过Zygote fork一个新进程。
冷启动之所以比热启动慢这么多,源头就在这里。fork一个进程本身不慢,真正慢的是新进程要初始化Application、类加载器要加载各种类、主线程要跑起来,这些全是耗时的正儿八经工作。
5.2 进程不存在:走Zygote fork,系统侧会先“挂起”这次启动
如果进程不存在,系统会调用ProcessList.startProcessLocked,最终通过ZygoteProcess去调用Zygote的fork方法。这块流程比较深,我们只需要理解关键点:新进程是从Zygotefork出来的,fork成功之后新进程的入口是ActivityThread.main。
新进程创建后,系统并不会立刻把之前挂起的启动请求丢掉,而是会把ActivityRecord暂时放在待启动列表里。等新进程里的ActivityThread通过attach连回系统服务后,系统服务会调用attachApplication,把待启动的Activity列表接着往下执行。这一步是整个启动流程中最容易让人困惑的地方,因为它涉及了“异步等待进程就绪”的状态管理。理解“挂起-恢复”这个模型后,再去看ActivityTaskSupervisor里的pendingActivity列表,就很清晰了。
5.3 进程已存在:把启动动作封装成ClientTransaction
如果目标进程已经存在,系统不需要再创建进程,而是直接通过ClientLifecycleManager把“要执行的动作”发给应用进程。在Android 10之后,这个动作被封装成ClientTransaction。
核心代码走向类似:
clientTransaction = ClientTransaction.obtain(app.getThread(), token); clientTransaction.addCallback(LaunchActivityItem.obtain(...)); clientTransaction.setLifecycleStateRequest(ResumeActivityItem.obtain(...)); mService.getLifecycleManager().scheduleTransaction(clientTransaction);这里有两个关键对象:
LaunchActivityItem:它代表“创建一个Activity并让它走onCreate”这个动作。ResumeActivityItem:它代表“让Activity走onStart、onResume”这个动作。
封装好之后,系统会把整个ClientTransaction通过Binder发给应用进程的ApplicationThread。注意,这里不是一个个回调方法分开发,而是打包成一个事务发过去。这就是前面说的“事务式”模型的核心优势:减少跨进程通信次数,也保证执行顺序的一致性。
6. 回到应用进程:Activity真正被创建出来
6.1 ApplicationThread收到消息后,主线程H是怎么调度的
应用进程侧接收系统服务消息的入口是ActivityThread.ApplicationThread,它是IApplicationThread的Binder实现。系统服务通过Binder调用ApplicationThread.scheduleTransaction,把ClientTransaction转成消息,投递到主线程HandlerH中。
H是ActivityThread内部的一个Handler,它有一个关键消息类型EXECUTE_TRANSACTION。主线程Looper轮询到这条消息后,会调用TransactionExecutor.execute来真正执行事务里的所有Item。
这段逻辑已经把“跨进程通信”和“主线程执行”之间的衔接处理得非常清晰了。也正因如此,Activity的生命周期回调才一定发生在主线程,因为HHandler绑定的就是主线程的Looper。
6.2 LaunchActivityItem.execute:newActivity + attach + performCreate
TransactionExecutor拿到LaunchActivityItem后,会调用它的execute方法。这里就是Activity实例真正被创建的地方。关键走到ActivityThread.performLaunchActivity。
我挑几个关键步骤说一下:
第一步,创建ContextImpl。系统先通过ContextImpl.createActivityContext创建一个ContextImpl实例,这个ContextImpl将来会作为Activity的mBase。Activity持有的Context并不是自己new的,而是由系统提前创建好并交给它attach的。
第二步,通过类加载器创建Activity实例。核心代码是:
java.lang.ClassLoader cl = appContext.getClassLoader(); activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);其实底层就是cl.loadClass(className).newInstance()。这也就解释了为什么Activity必须有一个无参构造函数——系统在创建实例时根本不管你的构造方法参数,它只调用默认构造函数。
第三步,绑定Window与WindowManager。新创建的Activity被attach到Context以及Window体系:
activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback, r.assistToken);在这个attach方法内部,会创建PhoneWindow,并把WindowManager和Context、token绑定在一起。这一步是setContentView能够工作的前提。
第四步,调用performCreate并触发onCreate。Activity.performCreate内部会先调用mInstrumentation.callActivityOnCreate,最终回调到Activity.onCreate。
这里很多人会问,为什么要绕一圈Instrumentation才调onCreate?因为Instrumentation本质上是一个“插桩监控器”,系统执行的任何生命周期动作,都会通过它在实际回调之前或之后做一些监控、测试或者埋点。比如单元测试环境下,Instrumentation可以替换成自定义实现来mock生命周期。
6.3 setContentView为什么能生效:Window与DecorView的配合
onCreate里写setContentView几乎是Android开发的第一课,但源码里它到底怎么运作的,很多人不一定清楚。
Activity.setContentView最终会调用PhoneWindow.setContentView。PhoneWindow内部先调用installDecor,确保mDecor这个DecorView已经创建;然后根据主题从系统布局库里加载一个基础布局,比如R.layout.screen_simple;最后把你在setContentView里传入的layout资源inflate成View,add到DecorView的内容区域。
所以setContentView在源码里本质上是“往PhoneWindow的DecorView里填充View”。这也是为什么一个Activity的布局最顶层拿到的是DecorView,而不是你写的那个根布局。
6.4 handleResumeActivity:onStart、onResume和首帧显示
当LaunchActivityItem执行完成后,TransactionExecutor会继续处理事务里的ResumeActivityItem,它最终会走到ActivityThread.handleResumeActivity。
这个方法里,系统先调用performResume逻辑,内部通过Instrumentation.callActivityOnResume触发onResume。但在触发onResume之前,如果Activity之前是stop状态,会先执行performRestart或者performStart。所以你在日志里看到的onStart在onResume前面,并不是巧合,而是源码里明确规定的顺序。
之后还有一个非常关键的步骤:
wm.addView(decor, l);也就是把DecorView通过WindowManagerGlobal真正添加到WindowManager中,完成窗口与系统侧的正式关联。这一步直接决定了用户能不能看到这个Activity。很多人以为onResume执行了页面就显示了,其实onResume只是表示Activity进入了可交互状态,真正让系统渲染出第一帧的,是后面WindowManager添加DecorView并申请布局的流程。
7. 启动模式与Task栈背后的那些坑
7.1 K个经典组合场景的源码级结论
启动模式单独背不难,难的是组合使用时的行为。我整理几个最常见的场景,对应到源码层面的结果:
| 启动方式 | 实际结果 | 触发回调 |
|---|---|---|
| standard模式 + 普通Intent | 每次新建ActivityRecord,压入当前Task栈顶 | onCreate/onStart/onResume |
| singleTop + 目标已在栈顶 | 复用栈顶ActivityRecord,不新建 | onPause/onNewIntent/onResume |
| singleTask + 目标已在栈中 | 复用该ActivityRecord所在Task,清除其上所有Activity | onNewIntent + 可能触发onCreate(如果Activity被销毁重建) |
| singleInstance | 独占一个Task,任何新Activity都不能进这个Task | 取决于是否复用 |
| FLAG_ACTIVITY_CLEAR_TOP | 清除目标Activity之上的所有Activity | 目标Activity可能走onNewIntent,也可能销毁重建 |
| FLAG_ACTIVITY_SINGLE_TOP + CLEAR_TOP | 栈顶与栈中复用组合拳 | onNewIntent |
7.2 单例不生效?多半是taskAffinity在捣乱
实战中特别容易踩的一个坑是singleTask不生效。明明设置了launchMode="singleTask",结果每次启动还是新建了一个Activity。
源码层面,ActivityStarter在处理singleTask时,会先根据目标Activity的taskAffinity去找对应的Task。如果有多个Task的affinity都指向同一个值,系统会选它认为最合适的那个Task进行复用。如果你在目标Activity上没有显式设置taskAffinity,默认它会继承Application的包名,看起来没问题;但如果你启动端设置了奇怪的TaskAffinity,或者目标Activity在别的进程,就可能出现“找不到可复用的Task,于是新建了一个Task”的结果,看起来就像单例模式没生效。
排查这类问题,不要只在manifest里找原因,先看启动端是否设置了Intent.FLAG_ACTIVITY_NEW_TASK,再看目标Activity的taskAffinity。这两个是最容易导致行为不符合预期的变量。
7.3 后台启动Activity的限制在源码里是怎么体现的
Android 10之后,后台启动Activity被限制得越来越严。源码层面,ATMS在接收启动请求时,会检查调用方的uid与进程状态,如果不在允许的窗口期(比如前台应用、系统应用、有可见窗口的应用),就会拒绝这次的启动请求,并打出一条带Background activity start restriction字样的日志。
这给实际开发带来的教训是,不要试图在后台任意时机直接弹Activity,而应该使用通知、全屏Intent等更合规的方案。从源码层面理解这个限制,也能帮你在排查“页面为什么没弹出来”时少走弯路。
8. 常见问题排查与冷启动优化思考
8.1 启动类问题排查快查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
启动直接抛ActivityNotFoundException | Intent没有显式指定Component,且没有匹配到任何隐式Intent的接收方 | 检查packageName、className、category、action是否配置正确 |
启动闪退,日志显示Unable to instantiate activity | Activity没有无参构造函数,或者类名写错 | 检查类名全路径,确保构造函数是无参的 |
| 启动后黑屏很久 | Application的onCreate做了大量耗时操作,或主线程被阻塞 | 用adb shell am start -W看启动耗时,再结合systrace定位 |
| 冷启动首帧慢,但Application没耗时 | 类加载器加载了大量未使用到的类,或者布局过度复杂 | 开启baseline profile,检查启动阶段的主要耗时函数 |
singleTask不生效 | taskAffinity不一致,或启动时带了NEW_TASK之外的其他Flag | 核对manifest和启动代码里的Intent Flag |
| 在后台调用startActivity无反应 | 被系统后台启动限制拦截 | 查看日志中是否出现Background activity start restriction |
| 页面启动了,但没走onCreate | 目标Activity已被复用,走的是onNewIntent | 检查启动模式,确认是否走了复用逻辑 |
8.2 冷启动优化:从源码里读出三个方向
理解了启动链路,冷启动优化的思路就很清晰了。整个冷启动过程可以看成三段时间的叠加:进程fork与初始化、Application/组件初始化、Activity创建与首帧渲染。
第一个方向,减少Application阶段的工作量。Application.onCreate里不要做太多同步耗时操作,尤其不要在主线程执行网络请求、数据库大查询、重复初始化第三方SDK。能用懒加载就懒加载。
第二个方向,减少类加载耗时。可以开启启动阶段的baseline profile,把启动阶段需要加载的类提前放进profile里,让AOT编译提前优化;同时注意不要用重方法做启动初始化,比如反射、json解析等。
第三个方向,优化首帧渲染。Activity创建之后到首帧显示,中间有LayoutInflater加载布局、DecorView与WindowManager关联、系统渲染第一帧等过程。布局层级越深,这个时间越长。启动背景图可以用windowBackground设置一个和首页背景一致的占位图,用户在等待窗口创建完成时看到的视觉体验会好很多。
8.3 一个排查启动问题的通用套路
最后说一个自己实际排查启动问题的通用套路。先用adb shell am start -W 包名/Activity全路径拿到系统记录的启动耗时,这里面会直接告诉你冷启动时间、waitTime、totalTime,能快速判断问题是在系统侧还是应用侧。
然后看Logcat里的ActivityTaskManager和ActivityTaskManager相关tag,系统在启动失败、后台限制、找不到Activity时都会打日志。少了这个步骤,很多问题你会无从下手,就像无头苍蝇一样去改代码,效率非常低。
我自己用过的最有效的方法,是在Android Studio里直接把AOSP源码对到SDK版本上,打断点看ActivityStarter.execute和ActivityThread.performLaunchActivity这两处的实际走向。看会这两处之后,绝大多数启动相关的问题都能从源码层面上找到依据,而不是靠猜。理解启动链路,带给你的不只是面试时的谈资,更重要的是在线上问题出现时,你心里能有一条清晰的排查路径,知道这一步走了什么分支、为什么是这个结果。