2026/10/18 8:51:00

启动性能优化:画清Boot、冷启动、热启动的时间漏斗

启动性能优化:画清Boot、冷启动、热启动的时间漏斗 做启动性能优化做得越多我越觉得一个项目里最难的部分往往不是“怎么优化”而是“怎么定义优化目标”。很多团队一说启动优化第一反应就是压冷启动耗时把几十个初始化任务丢到异步线程跑一轮机器看到数字降了就算交差。但真到线上用户还是说“打开慢”你一头雾水。等把整个链路拉通之后你才会发现真正的问题往往藏在 Boot、冷启动、热启动三种窗口的交界处你优化了 A 场景用户感知的 B 场景却更差了——因为你根本没把启动链路的时间漏斗画完整。这篇是稳定性性能系列的第九篇。我不打算给你列一份“启动优化 checklist”而是想从三种启动窗口的边界讲起把冷启动优化真正值得动手的杠杆、热启动和 Boot 场景容易被忽略的隐性耗时、以及“优化之后怎么保证稳定”这件事讲透。内容面向做应用性能、稳定性、框架优化的小伙伴也适合刚接手启动性能专项、想建立全局判断的同学。1. 先把时间漏斗画清楚Boot、冷启动、热启动的边界到底在哪1.1 三个启动窗口的差异在移动端项目里“启动”并不只有我们在测试环境按一下冷启动那一下。从设备上电到用户点开应用再到用户第二次、第三次从最近任务切回整个过程至少可以分成三个完全不同的窗口Boot、冷启动、热启动。Boot 指的是设备从开机到系统可用这一整段窗口。这个阶段系统在做包扫描、系统服务初始化、桌面启动、开机广播分发等大量工作CPU、磁盘 IO、内存带宽都属于“被争抢”的状态。很多应用会注册开机广播在 Boot 窗口内做自启、拉活、数据预取。这个窗口的特点是你看到的每一项任务单独看都不大但叠加在一起会明显拉长开机完成时间也会让开机后的第一次应用冷启动变得异常缓慢。冷启动是应用进程从头创建的过程。系统里原本没有这个应用的进程用户点击图标后系统要创建进程、创建 Application、创建 Activity、完成第一帧渲染再到用户真正可以操作。这是整个启动链路里成本最高、也是大多数团队重点优化的场景。热启动则分成两种常见形态一种是应用进程还活着Activity 也还在任务栈里用户只是切回前台另一种是进程还活着但 Activity 已经被系统回收用户切回时需要重建 Activity 而不是重建进程。很多人把热启动等同于“切后台再切回来”这其实不够准确。重建 Activity 的热启动恢复成本很多时候比你想的要高。1.2 为什么边界不清会让优化“白做”我见过不少团队在冷启动上花了很大力气结果线上首帧数据好看用户却依然觉得“打开慢”。原因往往出在三个地方。第一首帧定义被系统启动图“骗”了。Activity 还没有真正创建时系统已经把 windowBackground 作为启动图显示出来了这个画面出现得很快但它不能代表业务代码已经就绪。如果你以这一帧作为结束点所有优化都会显得“已经到底了”。第二热启动路径没有纳入优化范围。用户冷启动优化完之后日常使用中最频繁的反而是热启动。如果热启动恢复路径上有大量重复初始化、状态恢复、图片重新解码用户感知到的“慢”会被归因到冷启动头上但你无论如何压冷启动都压不动。第三Boot 窗口的影响被误判成机型性能问题。中低端机开机后的第一次冷启动经常比平时慢出好几倍。如果不看系统启动状态只拿开机后的首启数据做优化前后对比结论可能是错的。所以在动任何优化之前先把三种窗口的时间漏斗画出来Boot 阶段的资源竞争、冷启动的进程创建与初始化、热启动的 Activity 恢复与状态回填。每个窗口的优化手段和度量口径都不一样混在一起做得到的只能是一团糊涂账。2. 度量先行不同启动场景的埋点口径与样本采集2.1 系统工具能告诉你什么启动优化如果只靠人手一只秒表基本等于瞎优化。第一步是先建立起一套可复现的度量方式。在 Android 侧最基础的工具是adb shell am start -W。这条命令可以打印从启动 Activity 到首帧呈现的系统侧耗时适合在固定机型上做横向对比。实际使用中我一般会配合am force-stop先杀掉应用进程保证测的是冷启动语义# 先停止应用确保进程不存在 adb shell am force-stop com.example.debug # 再启动目标 Activity并打印启动耗时 adb shell am start -W -n com.example.debug/.MainActivityam start -W输出的参数可以拿来评估同一台设备、同一个版本的前后变化但不要直接把它当成线上用户可感知的启动时间。原因在于Activity 首帧出现并不等于页面真正可交互而且这条命令覆盖不到 Application 初始化之后的大量异步逻辑。2.2 埋点里最容易被带偏的两个口径真正要建立的是应用内的自定义启动打点。建议把启动过程切成几段进程创建、Application.attachBaseContext、Application.onCreate、Activity.onCreate、首帧、reportFullyDrawn、用户可交互。第一段是进程创建。如果你在 API 29 及以上可以拿到进程真实的启动时间低版本则在attachBaseContext里记录一个近似起点。注意attachBaseContext在多进程场景下会被调用多次必须用进程名过滤只统计主进程。第二段是结束点。首帧和“用户可交互”不是同一回事。reportFullyDrawn()这个机制可以让你在首帧之后、布局和数据都准备好的时机主动上报“完全绘制”。我建议同时统计首帧时间和 fully drawn 时间两个都上报后续分析时才不会因为某个页面“首帧很快、内容很慢”而被误导。还有一个容易翻车的点冷、热启动的判定。你可以在进程内维护一个标记位进程第一次执行到启动流程时必然是冷启动Activity 只是从后台回前台、没有重新执行 onCreate 的属于热启动Activity 被重建、但是进程仍然在的属于“温启动”统计时建议单独归类。很多团队把冷启动和热启动混在一个指标里版本对比自然不稳定。2.3 样本分布决定优化方向启动耗时是典型的右偏分布少数中低端机、后台任务繁忙、存储接近满的场景会把平均值拖得很难看。所以线上大盘不要只盯平均启动耗时至少要看 P50、P90、P95 三档。P50 代表大多数普通用户能感受到的水平P90 到 P95 代表尾部用户也就是最容易在应用商店打低分的那批人。如果优化后 P50 降了但 P95 反而涨了说明你在提升平均体验时伤到了某些机型这时候要做的是分层优化而不是直接上线。样本数量也要注意。新版本刚发布时启动打点样本量可能不够尤其是低端机样本很少这时候得出的分位值波动会非常大。我一般会等新版本样本覆盖到指定比例之后再对比避免把版本升级后用户分布变化误判成优化收益。3. 冷启动优化真正值得动手的四个杠杆3.1 初始化任务重排从“首帧前必须完成”倒推冷启动优化最有效的动作往往不是“把代码写快”而是“把不该放在启动路径上的代码移走”。我建议把 Application 启动过程里所有初始化任务列成一张表逐项回答三个问题第一帧渲染必须要它吗业务主流程必须要它吗用户首次操作前必须准备好吗整理完之后任务通常能分成三类。第一帧前必须完成的任务渲染核心、崩溃采集、启动必用的基础配置。首帧后但不一定要立刻完成的任务网络库、推送、埋点、部分安全组件、账号状态同步。真正可以懒加载的任务地图、浏览器内核、广告、低频业务模块。对于第二类任务不要简单丢到一个线程池里就算完。任务之间往往有依赖关系比如登录态要先初始化网络库才能发请求配置先读出来业务模块才能初始化。这种场景适合用有向无环图的方式做任务编排把“并行初始化”变成“按依赖关系并行”而不是靠线程数硬堆。并行度并不是越高越好线程太多反而会增加锁竞争和 CPU 调度开销。3.2 主线程减负布局、存储与类加载主线程上的每一毫秒浪费在冷启动阶段都会被放大几倍。 这三个方向是我每次排查启动性能时必查的。布局方面启动路径上第一个业务页面的 View 树复杂度会直接影响首帧耗时。不要只看布局文件里有多少层还要看有没有嵌套过深、权重测量、复杂 RelativeLayout 约束。减少一层无用容器、把一个重量级自定义 View 拆成更轻的实现效果可能比你想的更可观。存储方面启动过程最怕在主线程做磁盘 IO。SharedPreferences 首次读取、数据库连接初始化、大文件扫描这些操作在主线程上很容易造成明显的启动卡顿。如果首帧前一定要读配置应该把配置读进内存后复用而不是每次启动都重新解析数据库迁移这类重活尽量放到独立线程或者升级后的专门流程里。类加载方面Java 和 Kotlin 类第一次被使用时要经历加载、校验、初始化等过程启动路径上类的数量越多耗时越长。这句话的另一面是如果你用 R8/ProGuard 做代码缩减把启动路径上用不到的类剪掉冷启动收益通常很直接。但反射场景要格外小心缩减规则配错会直接导致运行时崩溃。反过来“提前预加载”也要克制只有在确实测到某个类首次加载阻塞了主线程时才值得做否则只会白白增加启动内存和 IO。3.3 首帧到可交互的过渡时间别忽略冷启动优化如果只盯首帧很可能会漏掉真正影响体验的“半秒空白”。首帧画出来了用户看到的是一个壳子业务数据还没回来按钮点了没反应这种状态往往比白屏更让人烦躁。所以首帧之后到 fully drawn 之间的过渡时间反而是优化空间很大的区间。操作上可以在首帧之后的第一次主线程空闲时再去触发真正耗时的初始化比如数据预加载、页面渲染、部分上报。用 IdleHandler 或者类似的空闲回调比硬编码 sleep 可控得多。不过要注意不能把所有延迟任务都塞进空闲回调因为用户可能在空闲回调触发前就退出了应用导致任务永远执行不完。另一个常见问题是首帧之后立刻发起一大堆线程任务把主线程和 CPU 全部打满结果页面滑动掉帧、网络请求互相排队。启动优化讲究的是“前慢后快”该慢的初始化宁可晚一点也别让首帧和用户交互抢资源。3.4 启动图与闪屏看着快了但不一定是真快很多团队会用 windowBackground 做一个品牌启动图系统在 Activity 创建前就把这张图显示出来视觉上“秒开”。但这只是把白屏换成了图片并没有减少 Activity 初始化和业务加载的时间。如果启动图展示的是品牌图而 Activity 真正的首帧要等 2 秒才出现用户的真实感受是“卡在启动图半天进不去”。更合理的方式是用 windowBackground 消除白屏同时让真实的启动页或者业务页尽快接管接管之后立刻做无缝转场不要故意用“启动图倒计时”拉长展示时长。这里说一句可能不太讨喜的话启动图广告本质上是用性能换营收它会把用户感知的启动时间拉长和启动优化是直接冲突的。如果你同时负责这两块心里要有数——你压下去的启动耗时很可能被商业策略又加了回来。4. 热启动与Boot场景容易漏掉的隐性耗时4.1 热启动不是“换个页面”恢复路径也要做减法热启动看起来快但并不意味着没有优化空间。特别是“进程还在、Activity 被系统回收”这种场景无需重新创建进程和 Application但 Activity 的 onCreate、setContentView、布局 inflate、数据恢复全部要重新执行一遍。最容易出问题的是状态恢复。如果 onSaveInstanceState 里塞入了大对象、位图或者复杂的序列化结构恢复路径上就会出现反序列化开销如果恢复数据还要重新从磁盘读这个成本会进一步放大。热启动优化的方向不是压缩系统启动逻辑而是给 Activity 的“重建”做减法能不入栈的数据不入栈能缓存在内存的重用缓存避免每次回来都重新计算。另外还要关注进程被系统 Trim Memory 之后的情况。很多页面把图片缓存放在内存里内存紧张时候这些缓存被清掉用户切回页面时只能重新解码热启动瞬间会感觉特别卡。治本的方法是把热启动路径上的资源缓存策略调整成“内存不足时先释放可重建的低优资源”而不是把页面恢复依赖的资源也一起清掉。一个参考判断如果你们热启动的 P50 比冷启动 P50 的一半还高大概率是因为启动流程里有重复初始化和恢复开销。这时候回头查 Activity 重建路径收益往往比继续压冷启动更大。4.2 Boot窗口开机广播与冷启动的“低气压区”Boot 窗口是很多应用团队完全没接管、却直接影响用户体验的一块。系统开机后要做包扫描、预加载系统组件、恢复上次运行状态磁盘和 CPU 都处于繁忙状态。如果你的应用注册了开机广播并且在广播回调里同步做了大量初始化你抢到的不是“自己的启动速度”而是整个系统开机完成的时间还会连累同窗口内其他应用。对于绝大多数应用开机广播里最该做的事情是什么都不做。把真正的任务交给系统级延迟调度等到系统空闲、满足网络条件或充电条件后再执行。即使业务上确实需要开机后尽快运行也应该控制启动的线程数、IO 量和锁的持有时间不要在系统最忙的时候添乱。Boot 窗口还会直接影响应用冷启动。应用更新后首次启动、系统对代码做编译优化时冷启动会比平时慢很多。这种慢不是业务代码造成的而是系统在后台做包优化。如果你在线上发现某次版本发布后冷启动数据突然变差先确认是不是新包触发了系统编译优化再决定要不要追业务代码。这类问题的处理方式不是“优化”而是“区分责任”把系统侧耗时从业务侧指标里剥离出来。5. 快之外的稳定性红线别把启动优化做成事故5.1 异步化不等于安全化启动优化最常用的手段是异步化但异步化恰恰是启动阶段引入崩溃和 ANR 的高发区。一个典型的例子某个初始化任务原来在主线程上执行顺序确定的改成异步之后另一个任务依赖它的结果却不知道它在哪个线程执行完于是一边等待一边超时最后主线程被卡死出现 ANR。还有的团队把崩溃上报 SDK 初始化改成异步结果应用在异步初始化完成之前就崩溃了线上崩溃直接漏报。我给所有做启动优化的团队一个很实用的建议把启动任务按“同步必要性”和“状态一致性”分等级而不是简单地分成“主线程任务”和“异步任务”。以下几类任务要非常谨慎崩溃采集和日志上报必须最早初始化通常不要异步或者异步后要有“等待完成”的兜底。影响 UI 展示的数据可以异步加载但主线程要有一个明确的超时和空态策略。多个模块共享的状态必须保证初始化顺序或者提供统一的“就绪回调”不能靠猜。与账号、支付、安全相关的能力不要为了启动快而牺牲正确性。5.2 上线前必须锁定的联合指标启动优化上线之前不只是看启动耗时降了多少还要同时锁定一组稳定性指标冷启动崩溃率、启动 ANR 率、启动后 1 秒和 3 秒的线程数、内存 PSS 峰值、文件描述符数量、GC 次数。为什么强调联合指标因为启动优化的很多手段是“拆东墙补西墙”。比如把数据库初始化挪到异步线程首帧快了但用户第一次进入数据页时主线程因为等待数据库就绪而卡顿ANR 率反而上升比如把图片压缩力度加大启动快了但放大后清晰度下降用户觉得页面糊。这些都是优化生效但整体体验变差的案例只靠一个指标根本发现不了。我看一个启动优化改动是否成立会要求同时给出“优化前后启动指标对比”和“优化前后稳定性指标对比”。只要稳定性指标出现恶化即使启动耗时降了很多也不会轻易放它上线。6. 从本机跑分到线上大盘一套能落地的回归闭环6.1 线下的固定基线让“跑分”可复现启动优化的基础功是维护一批固定的测试设备覆盖高中低端、不同内存档位用固定的脚本在每个版本上跑启动测试。测试脚本至少要做到自动清理进程、自动启动目标页面、自动抓取系统启动耗时和应用内埋点。不要靠人工按秒表人的反应误差有时候比优化幅度还大。也不要只跑一次同一台机器上连续跑多次会因为有系统缓存而越来越快。常规做法是先杀掉进程跑两三次“预热”让系统完成必要的包编译和缓存构建然后再连续采样并取中位数。脚本本身也可以写成固定命令配合 CI 在每次版本出包后自动跑# 清理最近任务并强停应用保证冷启动语义 adb shell am force-stop com.example.debug # 启动并等待首帧 adb shell am start -W -n com.example.debug/.MainActivity # 抓取应用内启动埋点日志 adb logcat -d -s StartupMetric线下固定基线的意义不是“测得绝对精确”而是让前后版本之间具备可比性。所以线上机型、系统版本、测试流程都不要随意更换每次对比都用同一台基线机。6.2 线上的分位值监控别被平均数骗了线下测完不等于优化闭环结束。真正起作用的是线上大盘冷启动 P50、P90、P95热启动 P50、P95首帧到可交互的分段耗时启动崩溃率和 ANR 率。线上版本对比时要特别小心两点。第一统计口径不能随便改。启动完成时间从首帧改成 fully drawn相当于换了一条新的基线前后数据没有可比性。如果必须调整口径要把新旧口径并行统计一个版本周期再切换。第二不要只看版本平均值。新版本用户结构可能发生变化比如灰度账号偏高端机导致启动数据虚好。所以我会按机型档位做分层对比重点看中低端机上的 P95一旦优化对低端机不友好马上能看出来。另外建议建一个启动性能回归看板每个版本发布后自动比对。它的价值在于当你下次再改某项功能时能第一时间发现“这个版本给启动路径加了 50 毫秒”而不是等到用户反馈变差之后才追查。我现在的工作习惯是任何启动优化改动必须附带三类数据——固定基线机上的前后对比、启动任务链路各分段时间、线上灰度后的分位值和稳定性指标。拿不出这三样就不要说自己优化了启动。启动性能是一个长期维护的地基工程不是某一次上线完成就结束的事情。