文章内容
本文内容整理自享学团队直播课《App 启动流程源码分析(五)》
一、从启动屏动画到 Application
有一个很容易复现的现象:给 Application.onCreate 的头尾各打一个 Trace.beginSection / endSection,测出来只有 30ms;但用 adb shell am start -W 量整体,接入几个三方 SDK 前后 TotalTime 涨了一百多毫秒。多出来的时间既不在 onCreate 里,也不在 Activity 里,打点区间完全没覆盖到它。
问题由此产生:进程创建之后、入口 Activity 之前的这一段,系统替 App 做了哪些事?那些不在 onCreate 里的耗时藏在哪个环节?
原因:Application 启动阶段由系统在 bindApplication 触达主线程后,通过 handleBindApplication 一次性装配——构建 Context → makeApplication(用 Instrumentation.newApplication 反射创建 Application)→ 触发 attachBaseContext → 系统同步 install 所有 ContentProvider(这是被多数人忽略的隐藏耗时)→ 回到 callApplicationOnCreate。App 自己能写代码的位置只有 attachBaseContext 和 onCreate 两处;在 onCreate 里初始化几十个 SDK 时如果全部各起一个线程,会换来线程爆炸和上下文切换损耗,需要一套基于图论的依赖调度加统一线程池。
沿用前几篇的分段:阶段①进程创建 App 改不动,阶段②Application 和阶段③Activity 是可优化区。本篇把阶段②整个摊开。
二、Application 启动阶段由谁、按什么顺序装配

system_server 进程只出现一次:AMS 发出 bindApplication,oneway 调用,发完就不管了。前一篇讲过这条链的时序保证,这里只需要记住它到达 App 进程之后被折进了主线程的消息队列。
App 进程主线程是主场:
ActivityThread:handleBindApplication是整段装配的总入口,跑在主线程。ContextImpl:Context的真实实现,createAppContext在这里把 APK 的代码和资源装载起来。LoadedApk:一个已加载 APK 的运行期描述,mApplication挂在它身上。Instrumentation:用 ClassLoader 反射实例化Application,并负责回调onCreate。Application(你写的子类)和ContentProvider(你或三方 SDK 写的子类):这一段里仅有的两处 App 代码执行位置。
装配流水线按顺序是:createAppContext → makeApplication → newApplication 反射实例化 → app.attach → attachBaseContext → installContentProviders → callApplicationOnCreate → onCreate。这条线上只有 attachBaseContext、Provider 的 onCreate、Application.onCreate 三处会执行到 App 写的代码,其余全是系统动作。
三、handleBindApplication 执行流程
// handleBindApplication:主线程真正创建并装配 Application 的入口,由 BIND_APPLICATION 消息触发private void handleBindApplication(AppBindData data) { // 第一件事:为应用构建运行上下文,加载 APK 里的资源与代码 ContextImpl appContext = ContextImpl.createAppContext(this, data.info); // makeApplication 创建 Application 对象,内部会走反射 newApplication + attach Application app = data.info.makeApplication(data.restrictedBackupMode, null); // 同步启动该应用声明的所有 ContentProvider(只做安装,不阻塞主线程等待各 provider 初始化完成) installContentProviders(app, data.providers); // 最后回调 Application.onCreate,App 自己的初始化代码在这一步执行 mInstrumentation.callApplicationOnCreate(app);// handleBindApplication 方法体结束}输入是 AppBindData,也就是上一篇里 ApplicationThread 打包投递过来的那份数据,里面带着 ApplicationInfo 和 List<ProviderInfo>;输出是 void。
执行流程:进程里有了 ContextImpl,有了唯一的 Application 实例,清单里声明的所有 Provider 被装进了进程,Application.onCreate 跑完。
顺着这段代码,有几个点要单独说:
- Provider 的安装是这条流水线上的一环,而不是一个独立的、后续才发生的事件,它执行在
callApplicationOnCreate上面一行,跑在同一个主线程调用栈里。 data.providers是从 system_server 带过来的,来自 PMS 对清单的解析结果,App 侧不需要也无法干预这个列表的生成(能干预的是清单本身)。- 整个方法体没有任何线程切换,四步串行。
installContentProviders 那行的注释里写着"只做安装,不阻塞主线程等待各 provider 初始化完成"。这句需要拆开理解:安装动作本身是同步执行的,它会在当前调用栈里实例化每个 Provider 并调用其 onCreate;不阻塞指的是不会去等待 Provider 内部自己开的异步任务。所以一个 Provider 的 onCreate 里写了 200ms 的同步初始化,这 200ms 会记在主线程上。
四、makeApplication 与 Application 实例化
Application 的子类名可以叫 MyApp、App、XApplication,随便什么都行,只要清单里 android:name 对得上。这个自由度来自实例化方式。
// makeApplication:真正实例化 Application 的地方public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) { // mApplication 已存在就直接返回,保证整个进程只有一个 Application 实例 if (mApplication != null) { // 已创建过,直接复用同一个实例 return mApplication; // 复用分支结束 } // 通过 Instrumentation 用类加载器(ClassLoader)反射创建 Application Application app = instrumentation.newApplication(cl, appClass, appContext); // 缓存实例,后续 attach / onCreate 都围绕它 mApplication = app; // 返回本进程唯一的 Application 实例 return app;// makeApplication 方法体结束}// newApplication:Instrumentation 负责用反射把 Application 类名变成实例public Application newApplication(ClassLoader cl, String className, Context context) { // 用类加载器按 className 反射加载并实例化;因此你的类叫 MyApp / MyApplication 都不影响 Application app = getFactory(context).instantiateApplication(cl, className); // 创建后立刻把 Application 绑定到一个 Context 上(内部触发 attachBaseContext) app.attach(context); // 返回已完成 Context 绑定的 Application return app;// newApplication 方法体结束}输入是 ClassLoader、一个字符串形式的 className(来自清单 android:name)和一个 Context;输出是完成 Context 绑定的 Application 实例。状态变化有两处:LoadedApk.mApplication 被写入,以及这个实例的 mBase 被 attach 填上。
这两段代码放到一起看,结论就出来了:Application 的创建走的是"字符串类名 + 反射",不是 new 一个固定类型。类名只是 PMS 从清单里解析出来的一个字符串,一路传到 instantiateApplication,所以取什么名字系统都不关心;同理,它也解释了为什么清单里 android:name 写错会在启动瞬间抛 ClassNotFoundException 而不是编译期报错。
mApplication != null 的判空前一篇已经用过,它保证进程内 Application 的唯一性,也让 performLaunchActivity 里那次 makeApplication 调用只是一次取值,不会重复构造。
还有一个顺序细节:app.attach(context) 写在 newApplication 内部、return 之前。也就是说 Application 对象一构造出来立刻就被绑定 Context,attachBaseContext 在 makeApplication 返回之前就已经执行完了。
五、Application 里第一行代码不是 onCreate
顺着上一节的 app.attach(context) 往下,App 侧第一行被执行的代码在这里。
// MyApplication:你自己写的 Application 子类public class MyApplication extends Application { // attachBaseContext 是 Application 里最早执行的方法,早于 onCreate @Override protected void attachBaseContext(Context base) { // 必须调用 super,Context 的绑定在这里完成 super.attachBaseContext(base); // attachBaseContext 方法体结束 } // onCreate 才是真正的初始化入口(放 SDK 初始化的地方) @Override public void onCreate() { // 调用父类实现 super.onCreate(); // onCreate 方法体结束 }// MyApplication 类体结束}输入是系统传进来的 base Context;输出是 void。状态变化在于 ContextWrapper.mBase 被填上。在 super.attachBaseContext(base) 执行之前,这个 Application 上的任何 Context 方法(getResources、getSharedPreferences、getPackageName)都会因为 mBase 为 null 而抛 NullPointerException,这是这个方法里最常见的踩坑点:把初始化代码写在 super 调用之前。
执行顺序从这里开始:attachBaseContext 早于 onCreate,也早于所有 ContentProvider 的 onCreate。历史上 MultiDex.install(this) 必须写在这里,原因就是它得赶在任何二级 dex 里的类被加载之前完成,包括 Provider 里的类。
因为它是最早的位置,往这里塞东西的诱惑很大。需要清楚的代价:这一段跑完之前,Provider 装不了,Application.onCreate 进不去,Activity 更谈不上。在 attachBaseContext 里做的每一毫秒都顶在整条流水线最前面,且这个区间在只对 onCreate 打点的方案里同样不可见。
六、ContentProvider:藏在 Application 里的隐性耗时

回到开头那个现象:接入 SDK 后启动变慢,但 Application.onCreate 的打点没涨。多出来的时间在 Provider 上。
或许你会想:Provider 不是我主动调的,我代码里从来没写过 getContentResolver().query(),它怎么就被启动了?
答案在 handleBindApplication 那一行 installContentProviders(app, data.providers)。清单里声明过的 Provider,系统在装配 Application 的时候就会全部实例化一遍,不需要任何人主动调用。
// installContentProviders:把清单里声明的 provider 全部安装到当前进程private void installContentProviders(Context context, List<ProviderInfo> providers) { // 逐条安装清单文件里声明的 ContentProvider for (ProviderInfo cpi : providers) { // installProvider 内部会实例化并调用 provider.onCreate() ContentProviderHolder holder = installProvider(context, ...); // 循环体结束 } // 待核实:安装过程是否有与 onCreate 的同步/顺序约束随版本变化// installContentProviders 方法体结束}输入是 Context 和一份 List<ProviderInfo>(PMS 解析清单得到,随 bindApplication 跨进程带过来);输出是 void,代价是每个 Provider 被 new 出来、onCreate 被调用、并注册进 ActivityThread 的 Provider 表,进程里多了 N 个活着的 Provider 实例。
这里的问题在于:Provider 的 onCreate 是一个由系统在主线程串行触发的回调,数量等于清单里声明的条数,且完全不受 App 主动控制。循环体里没有任何异步调度,一个 Provider 的 onCreate 慢,后面所有 Provider 和 Application.onCreate 都要等它。
三方 SDK 正是看中了这个"免调用"的特性。
<!-- 应用节点:声明的 provider 会在 Application 构建时由系统自动启动 --><application android:theme="@style/AppTheme"> <!-- ContentProvider:三方 SDK 常借它做"免调用"的启动初始化 --> <provider android:name=".StartupProvider" android:authorities="${applicationId}.StartupProvider" /> <!-- 入口 Activity:主界面,桌面点击落点 --> <activity android:name=".MainActivity"> <!-- 意图过滤器:声明它是可被 Launcher 启动的主入口 --> <intent-filter> <!-- 主动作:MAIN 表示这是主入口 --> <action android:name="android.intent.action.MAIN" /> <!-- 启动器类别:桌面据此生成可点击图标 --> <category android:name="android.intent.category.LAUNCHER" /> <!-- 意图过滤器声明结束 --> </intent-filter> <!-- 入口 Activity 声明结束 --> </activity><!-- 应用节点声明结束 --></application>androidx.startup 的 InitializationProvider、Firebase 的初始化 Provider、WorkManager、部分崩溃采集 SDK,走的都是这条路径:它们只需要在自己的 AAR 里声明一个 Provider,清单合并之后就自动出现在宿主的 AndroidManifest 里,宿主开发者甚至不知道它存在。authorities 用 ${applicationId} 占位是为了避免多个 App 装在同一台设备上时 authority 冲突(冲突会导致安装失败)。
这就是"隐性"的两层含义:代码上不可见(宿主工程里搜不到调用点),打点上不可见(区间落在 attachBaseContext 结束到 Application.onCreate 开始之间,只对 onCreate 打点完全覆盖不到)。
七、耗时分析

要把上面那段区间量出来,打点位置需要覆盖流水线的三个边界,而不是只包住 onCreate。
三个点分别是:attachBaseContext 进入(super 之后立刻记,此时 Context 才可用);Application.onCreate 结束;入口 Activity.onCreate 开始。用 Trace.beginSection / Trace.endSection 成对埋,跑 Perfetto 时它们会以嵌套区间的形式出现在主线程轨道上。
三个点切出来的区间含义是:
attachBaseContext自身的耗时:通常是 MultiDex、热修复、加固壳解密的开销。- 从
attachBaseContext结束到Application.onCreate开始,这段区间里只有一件事在跑,就是installContentProviders。所有 Provider 的onCreate全部计在这里。这是第六节那些隐性耗时唯一能被夹出来的方式。 Application.onCreate自身——自己写的 SDK 初始化。Application.onCreate结束到Activity.onCreate开始,主线程消息队列的调度间隙,正常情况下很短;如果这里有明显空档,说明有别的消息插在中间抢了主线程。
除了自己埋点,Perfetto 抓下来的系统 trace 里还能直接看到 bindApplication、activityStart 这类系统段,以及每个 Provider 安装时的 ContentProvider.onCreate 段(待核实:系统 trace 里是否为每个 Provider 单独打段,取决于版本与 trace 分类是否开启)。两套点交叉对照,能确认自己的埋点没漏区间。
一个常见的错误做法:只在 Application.onCreate 的头尾打点,然后拿这个数字向外汇报"Application 阶段耗时 30ms"。这个数字漏掉了 attachBaseContext 和整个 Provider 区间,接入的三方 SDK 越多,漏得越厉害。
八、Application.onCreate 优化:异步、按需、懒加载

onCreate 是唯一完全由 App 掌控的区间,优化空间也集中在这里。把要初始化的东西按"什么时候真正被用到"分三类:
必须在主线程同步完成的。崩溃采集(要尽早,晚一秒就漏一秒的崩溃)、埋点的会话初始化、多进程共享配置的读取、以及被首屏直接依赖的东西。这类保持原样,但要控制数量。每加一个都直接顶在首帧前面。
可以异步初始化的。网络库、图片库、推送、地图、支付等,它们的初始化和首屏渲染之间没有强依赖,可以丢到后台线程。要求是这些 SDK 的初始化本身线程安全,且首次调用点能容忍"初始化尚未完成"(多数 SDK 内部有自己的就绪判断,用之前需要确认)。
可以延后到首帧之后甚至用到才初始化的。分享、扫码、客服、直播这类只在特定页面用到的,做成懒加载:第一次进入相关页面时再初始化。判断标准是"用户这次启动有多大概率会用到它"。
多进程要单独判一次。带 :push、:remote 这类子进程的 App,子进程同样会走完整的 Application 装配流程,同样会跑 onCreate。在 onCreate 开头判断当前进程名,非主进程直接跳过绝大部分初始化,这一条经常能省掉可观的时间,尤其是推送进程被拉起时。
Provider 侧也能治理。三方 SDK 通过清单合并塞进来的自动初始化 Provider,可以在自己的清单里用 tools:node="remove" 移除,改成在需要的时候手动初始化。移除之前必须确认该 SDK 提供了手动初始化的 API,否则会在运行时崩。androidx.startup 自己就提供了这个开关(它的 InitializationProvider 支持逐个 Initializer 用 meta-data 声明 tools:node="remove")。
九、异步的代价:50+ SDK 与线程池、启动框架设计

"能异步的都异步"这条规则在 SDK 数量少的时候有效,数量上去之后会翻车。
假设有 50 个可异步的初始化任务,每个都 new Thread().start()。代价有几层:创建 50 个线程本身有开销(每个线程默认栈空间以 MB 计);这 50 个线程和主线程一起竞争 CPU,中低端机上核心数只有 4~8 个,调度器把时间片切给了后台初始化,主线程反而拿不到 CPU;线程上下文切换的成本随线程数上升。结果是 Application.onCreate 的打点确实变短了(任务都被扔出去了),但首帧时间没变甚至更长——耗时从一个可见的区间转移到了不可见的 CPU 争抢里。
第一层优化是统一线程池:所有初始化任务提交到同一个固定大小的线程池,池大小按 CPU 核心数设定。并发度可控,线程复用,不再有创建开销。
第二层修正来自一个更麻烦的现实:任务之间有依赖。埋点 SDK 要在网络库之后,网络库要在配置读取之后,某个业务初始化要在用户信息加载之后。手工用 CountDownLatch 串这些依赖,任务数一多就没法维护,加一个任务要改一堆等待关系。
这里把任务集合建模成一张有向无环图:节点是初始化任务,边是"A 必须先于 B"。启动时对这张图做拓扑排序(Kahn 算法:反复取出入度为 0 的节点),得到一个合法的执行序列;把入度为 0 的任务提交到线程池,某个任务完成后把它的后继入度减一,减到 0 就提交。这样依赖关系是声明式的:每个任务只声明自己依赖谁,调度顺序由框架算出来。图里有环就是配置错误,可以在启动时直接抛异常暴露出来,而不是运行时死锁。
除了拓扑调度,启动框架还要处理两件事:
阻塞节点。有些任务必须在首帧之前完成(比如首屏要用的配置)。框架需要支持把某个任务标为"主线程等待",在合适的位置(通常是
Application.onCreate末尾或首个 Activity 的onCreate开头)阻塞等它完成。等待点越靠后越好,给异步任务留出的时间越多,实际等待越短。让主线程也参与执行。
Application.onCreate提交完任务之后,主线程接下来要做的是等待消息队列里的LAUNCH_ACTIVITY,这中间它是空闲的。把一部分任务安排给主线程自己跑,能避免"主线程闲着、线程池排队"的浪费。这一条属于推断层面的优化思路,实际收益取决于任务的粒度和机型,需要在目标机型上实测确认。
这套东西就是常说的"启动框架":任务声明 + 依赖图 + 拓扑调度 + 统一线程池 + 阻塞点管理。它没有减少任何一个 SDK 的初始化耗时,做的是把这些耗时排布到合适的线程和合适的时刻。
总结
进程创建之后到入口 Activity 之间,系统在主线程上串行做完了五件事:createAppContext 装载 APK 与资源、makeApplication 反射实例化 Application、attachBaseContext 完成 Context 绑定、installContentProviders 把清单里所有 Provider 实例化并逐个调 onCreate、callApplicationOnCreate 回到 App 自己的初始化代码。
耗时藏在哪里,答案是第四步。Provider 的 onCreate 由系统自动触发、由清单合并自动引入、区间又恰好落在多数人打点范围之外。这三件事凑到一起,就是那段查不到的时间。
在 onCreate 里初始化几十个 SDK 而不拖慢启动,路径是分类(同步 / 异步 / 懒加载)加调度(依赖图 + 拓扑排序 + 统一线程池),而不是给每个任务开一个线程。
本文由 AgentWorkFlow 生成