App 启动流程源码分析(一):先定义清楚"启动",再谈优化

文章内容

本文内容整理自享学团队直播课《App 启动流程源码分析(一)》

一、耳熟能详的优化方向,为什么不足以解决问题

避免主线程 IO、减少主线程耗时、减少布局层级、优化线程的使用方式、对部分启动任务做懒加载与按需加载、用 profile 工具定位热点——这些条目出现在几乎每一份启动优化清单里。它们单独看都成立,麻烦在于它们是并列的散点,条目之间没有先后关系,也没有权重。

散点式优化的典型失效方式,是注意力被局部现象吸走。测到页面渲染有耗时,就一路做布局层级压缩,改完却发现整体启动时间没有明显变化——真正的时间开销可能落在 Application 初始化,或者某个同步执行的 ContentProvider 上。单点越挖越细,整体反而离最优越来越远。

大厂对这件事的处理是组织化的:字节跳动一类的公司设有专门的性能优化组,具体业务部门的项目里还会再配备专职的性能优化人员。岗位的存在本身说明了性质:启动优化是需要长期维护的工程方向,而不只是一轮代码清理。

常见的启动优化方向

二、把优化装进一个可以循环的流程

  1. 理论分析:从全局出发找可优化点,先建立对整条启动路径的认识,再决定往哪里投入精力。
  2. 现状分析:用 profile 工具实测摸底,把"猜测的瓶颈"换成"测出来的瓶颈",并结合线上指标数据确定最终方向。
  3. 启动优化:动手改代码。
  4. 持续测试:回归验证,确认改动确实生效,并且没有把耗时挪到别处。

这四步是一个环,不是一条走完就结束的直线。理由和业务节奏直接相关:性能问题往往在特定高峰节点才暴露出来,比如双 11、双 12、618,或者抖音这类产品的上下班高峰。这些窗口错过一次,下一次可能要等一年。所以线上线下都需要持续监控,而不是把优化当作某个版本的专项任务。

反过来说,这也是性能优化这条路可以一直走下去的原因:做得越深,积累的现场数据和判断经验越多,可替代性越低。

启动优化的四步循环

三、"启动"从哪一刻开始,到哪一刻结束

1. 两种口径

代码视角:Zygote 进程 fork 出 App 进程之后,App 进程执行第一行代码,就可以说进程已经启动。

用户感知视角:点击 icon → logo 界面 → 广告界面 → 主界面。

优化必须采用后一种口径。因此启动的定义是:从点击 icon 开始,到第一个 Activity(主 Activity)显示为止。优化的时间窗口,就是这一整段,不是其中某个阶段。

2. 指标定义

窗口一旦划定,logo 页与广告页就都在窗口之内。它们呈现给用户的第一块画面,在窗口最前端,由入口 Activity 的主题决定;这也是"黑白屏"话题的来源。关键帧中的 AndroidManifest 配置对应的就是这一段可见准备:

xml
<!-- 应用节点:这里声明的主题在进程创建后、首帧内容到来前就已经生效 --><application android:theme="@style/AppTheme">    <!-- 入口 Activity:桌面 icon 的点击最终落到这个组件上 -->    <activity android:name=".MainActivity">        <!-- 意图过滤器:声明该组件可以被哪一类 Intent 唤起 -->        <intent-filter>            <!-- 主动作:标识这是应用的主入口 -->            <action android:name="android.intent.action.MAIN" />            <!-- 启动器类别:桌面据此为应用生成可点击的图标 -->            <category android:name="android.intent.category.LAUNCHER" />        <!-- 意图过滤器声明结束 -->        </intent-filter>    <!-- 入口 Activity 声明结束 -->    </activity><!-- 应用节点声明结束 --></application>

application 标签上还带有 allowBackup、icon、label、roundIcon、supportsRtl 等属性,与本文结论无关,这里省略;@style/AppTheme 的具体内容在关键帧中未展开,待核实。

启动的定义:点击 icon 到第一个主 Activity 显示

四、启动路径简介

例如医生若不清楚肠胃长什么样、如何运作,误判会把小病拖成大病;优化同理,不了解启动流程就动手,精力容易花在与主 Activity 无关的路径上。类比到此为止——医生面对的是个体差异,而启动流程是一条可以逐行读源码确认的确定路径,它的每一步都有明确的调用方、被调用方和通信方式。

这条路径横跨 4 个进程:Launcher 进程、system_server 进程、Zygote 进程、App 进程自身。

参与其中的对象包括:ActivityManagerProxy(客户端侧代理)、ActivityManagerService(服务端)、ApplicationThreadProxy、ApplicationThread、ActivityThread、Activity.onCreate。跨越边界时用到三种通信方式:进程间调用走 Binder,Zygote 创建进程的请求走 Socket,进程内的消息投递走 Handler(Looper)。

1. 一道容易答错的面试题

AMS(ActivityManagerService)是不是一个独立进程? 答案是否定的。AMS 只是 system_server 进程里的一个服务,运行在 Binder 线程池分配出来的某个事务处理线程上。system_server 里有 90 多个服务,每个服务的运行由一个独立线程承担。

这道题考的是进程与线程的区分:一个进程可以有多个线程,"服务"这个词在 Android framework 里指的是前者内部的执行单元,而不是一个新的进程。

App 启动流程:四个进程

五、App 进程内部的启动序列

进程被 fork 出来之后,时间消耗集中在这一段:

Zygote fork 创建进程 → bindApplication → attachBaseContext → installProvider → Application.onCreate → handleMessage → Activity 生命周期 → View 构建 → View 渲染 → 数据加载。

这条序列需要单独记住,因为它把"启动慢"拆成了可归因的区间。installProvider 阶段的耗时通常来自第三方 SDK 借 ContentProvider 做的初始化;Application.onCreate 是各类 SDK 初始化集中的地方;View 构建与渲染才是布局层级优化真正生效的位置;数据加载的耗时则取决于首屏接口与本地缓存策略。把耗时归到错误的区间,正是第一节所说的失效方式。

App 进程内部的启动序列

六、回到定义

启动优化里最先要落定的不是手段,是边界:从点击 icon 到主 Activity 显示,这一整段才是被优化的对象。边界模糊时,任何一次改动都缺少判断依据:耗时是真的减少了,还是被挪到了窗口的另一端,无从分辨。

因此下一步的动作是给这段窗口装上刻度:按上一节的序列做分阶段计时,在 attachBaseContext、Application.onCreate 首尾、入口 Activity 的 onCreate 与首帧回调处埋点,配合 systrace / Perfetto 抓取同一次冷启动的完整轨迹,得到每个区间的实际占比。有了这张分布图,再回头看第一节那份清单,才知道该动哪一条。

下一篇进入源码,从 Launcher 发出 startActivity 开始,跟到 AMS 请求 Zygote fork 新进程为止。


本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

App 启动流程源码分析(一):先定义清楚"启动",再谈优化 · Yinchen 的博客