冷启动预算先拆路
Android 冷启动优化最容易走偏的地方,是一上来就盯着某个耗时数字。启动 2 秒,能不能压到 1.5 秒?首帧 900ms,能不能再省 200ms?这些目标当然有价值,但如果没有把启动路径拆清楚,优化会变成到处抠一点,最后既难复盘,也难稳定。
一次移动端首页改版后,冷启动时间突然变长。团队一开始怀疑图片资源太大,压缩了一轮;又怀疑首页接口慢,加了缓存;后来发现真正的问题在 Application 初始化里新增了一个统计 SDK,同步读取配置并等待磁盘结果。它不是最显眼的业务代码,却在每次冷启动第一段就拦住了主线程。
冷启动预算不是给研发一个压力数字,而是把启动过程拆成几段:进程创建、Application、首个 Activity、首屏布局、首屏数据、可交互状态。每一段都要知道谁在花时间,哪些必须同步,哪些可以延后。
冷启动不是一个单点指标
用户感知到的启动慢,背后可能是多个阶段叠加。系统创建进程慢、Application 初始化重、首个 Activity inflate 慢、首页接口慢、图片解码慢、主线程被阻塞,都可能让用户觉得“打开慢”。如果只看总耗时,你不知道该改哪里。
更实用的方式是定义几个观测点。比如进程启动到 Application 完成,Application 到首个 Activity onCreate,onCreate 到首帧,首帧到首屏数据可见,首屏数据可见到可交互。不同团队定义会有差异,但必须统一。否则一个人说启动完成是首帧,另一个人说是数据出来,讨论就会混乱。
首帧也不等于可用。一个页面很快画出骨架,但按钮不能点、数据还在转圈,用户未必觉得快。冷启动优化不能只追求“有画面”,还要看关键任务是否可继续。对于内容类应用,首屏内容很重要;对于工具类应用,主要操作入口是否可点更重要。
Application 初始化要像过安检
Application 是冷启动最容易堆东西的地方。统计、推送、崩溃采集、埋点、广告、数据库、配置、主题、账号状态,很多 SDK 都希望尽早初始化。每个 SDK 都说自己很轻,合起来就不轻了。
我会把 Application 初始化分成三类。第一类是必须同步完成的,例如崩溃兜底、必要安全配置、最基础的进程判断。第二类是可以异步但要尽早的,例如埋点、远程配置预拉取。第三类是可以延后到使用时的,例如某个二级页面才需要的能力。
过安检的意思是:任何新增初始化都要说明为什么必须在这里执行、是否占主线程、是否读磁盘、是否发网络、失败会怎样。不能因为“放这里方便”就进 Application。启动路径里的每一行同步代码,都应该有理由。
对于第三方 SDK,要特别关注它是否在初始化时做了隐藏工作。有些 SDK 文档只写一行 init,但内部可能读文件、创建线程、加载 so、访问 ContentProvider。接入前最好用 trace 看一次,不要只信“轻量级”描述。
首屏布局要控制复杂度
很多冷启动优化最后会落到布局。首页 XML 太深、过度嵌套、复杂自定义 View、首屏一次性 inflate 太多模块,都会拖慢首帧。Compose 场景下也类似,首屏组合层级、状态读取和同步计算都会影响首次绘制。
布局优化不等于把所有东西都懒加载。用户第一眼需要看到什么,应该先和产品确认。首屏关键区域要稳定,非关键模块可以延后。比如首页顶部导航、主要列表骨架先出来,底部运营位、二级推荐、非首屏卡片可以等首帧后再加载。
还要避免首屏做复杂同步计算。比如根据大量本地数据计算推荐、同步解析大 JSON、首帧前读取多份配置。这些逻辑即使不在 Application,也会阻塞首屏。能预计算就预计算,能后台线程就后台线程,能用缓存就用缓存,但缓存也要考虑过期和兜底。
数据加载要和画面策略一起设计
冷启动常见争论是:先显示缓存,还是等接口回来?这没有固定答案。内容稳定、缓存可信的页面,可以先展示缓存再更新;强实时页面,比如交易、支付、库存,不适合展示过期数据;工具型页面可以先展示操作入口,再局部加载状态。
如果选择缓存优先,要告诉用户当前数据是否可能更新,不要让旧数据伪装成最新结果。如果选择接口优先,要控制等待时间,超过预算就展示骨架、错误入口或离线状态。最差的是既等接口,又没有明确超时,用户只能看空白页。
数据加载还要拆关键和非关键。首页可能有多个接口,不是每个接口都应该阻塞可交互。用户进入页面最需要的 1-2 个接口优先,其余接口可以首帧后并行。接口聚合也要谨慎:聚合能减少请求数,但一个慢依赖可能拖住整包结果。
这里还有一个细节:缓存命中不等于体验稳定。缓存读取如果在主线程做大量反序列化,同样会拖慢首帧。缓存数据如果没有版本和过期策略,也可能让用户看到错误状态。冷启动里的缓存应该是轻量、可解释、可兜底的,而不是把接口等待换成磁盘等待。
如果首页数据强依赖登录态,还要把账号状态放进启动预算。比如 token 刷新、用户信息读取、权限拉取,这些任务经常被塞在启动早期。它们确实重要,但不一定都要阻塞首帧。可以先展示不依赖权限的基础框架,再根据权限补齐模块。前提是产品能接受这种分阶段出现的体验。
主线程不是万能承载层
Android 启动优化里,主线程是最敏感的资源。任何磁盘 IO、网络等待、大对象创建、图片解码、反射扫描、同步锁等待,都可能在启动阶段放大影响。平时 50ms 的阻塞,在冷启动里会直接体现在用户等待上。
排查主线程问题,Trace 比猜测可靠。不要只看代码觉得“这里应该不慢”。把冷启动 trace 抓出来,看主线程长任务在哪里、哪个 SDK 初始化占时间、哪个布局 inflate 慢、是否有锁等待。很多问题只有在 trace 里才会变得清楚。
但把所有任务丢到后台线程也不是万能解。后台线程过多会抢 CPU,任务之间没有优先级也会互相影响。更好的做法是给启动任务分级:首帧前必须完成、首帧后尽快完成、用户触发时再完成。不同级别用不同调度策略。
锁等待也要特别看。很多启动卡顿不是某个任务自己慢,而是主线程等待后台线程持有的锁,或者多个 SDK 同时争同一份资源。trace 里看到主线程空等时,不要只优化当前方法,要顺着锁的持有者往下查。启动阶段的并发如果没有编排,很容易从“异步优化”变成“异步互相堵”。
还有一类隐藏成本来自 ContentProvider。部分 SDK 会通过 Provider 自动初始化,甚至早于 Application。你在 Application 里删掉 init,不代表它没有启动成本。接 SDK 时需要看 manifest,确认是否引入了自动初始化组件。必要时用工具禁用默认 Provider,改成按阶段显式初始化。
优化要有回归保护
冷启动很容易被后续需求重新拖慢。今天优化掉 300ms,下一次接入 SDK 又加回 400ms。如果没有回归保护,启动性能会反复退化。
可以在 CI 或日常测试里加入启动耗时采集。指标不一定要求每次都极准,但要能发现明显回退。比如同一机型、同一网络、同一账号状态下,冷启动总耗时、Application 耗时、首帧耗时超过阈值就提醒。不要等线上用户反馈“变慢了”再查。
上线前也要分机型看。高端机优化 200ms 用户可能不明显,低端机上同一段代码可能放大成 800ms。冷启动预算不能只在开发机上验证,至少要覆盖一台中低端设备。
回归保护还要固定场景。冷启动、温启动、热启动不能混在一起;首次安装、已有缓存、登录态过期、弱网环境也不能混在一起。每个场景的预算不同。把所有启动混成一个平均值,往往会掩盖真正影响用户的慢路径。
线上监控也要谨慎看分位数。平均值变好不代表大多数用户变好,P95 变差可能说明低端机或特定版本被拖慢。冷启动优化更应该关注 P50、P90、P95 和关键机型分布。尤其是启动这种高频体验,长尾用户的痛感会很明显。
回归保护最好能落到准入规则。比如新增 SDK 必须提供启动阶段 trace,首页新增首屏模块必须说明是否阻塞首帧,启动阶段新增磁盘读取必须给出理由。规则不需要很重,但要让性能预算成为评审的一部分,而不是上线后补救。
如果团队有性能平台,可以把启动阶段拆分指标长期展示。没有平台,也可以先从简单记录开始:每个版本记录 Application 耗时、首帧耗时、首屏数据耗时、可交互耗时和主要变更。几次版本之后,你会看到哪些需求最容易拖慢启动。这个记录比单次优化报告更有价值,因为冷启动是长期治理问题。
验收冷启动,要同时看快和稳
冷启动验收不能只跑一次。启动耗时受设备温度、系统负载、缓存状态、网络状态影响很大。至少要在固定设备上多跑几轮,去掉极端值,再看分位数。否则一次看起来很快,可能只是缓存和系统状态刚好有利。
验收场景也要覆盖真实路径。首次安装后启动、已有缓存启动、登录态过期启动、弱网启动、从通知点击启动,这些路径的瓶颈不一样。只测“开发机已登录且网络很好”的路径,不能代表真实用户。
还要看画面稳定性。首帧很快但随后大面积闪烁、模块跳动、骨架反复变形,用户仍然会觉得粗糙。冷启动预算不只是时间预算,也包含视觉稳定预算。首屏模块可以分阶段出现,但布局空间最好提前占好,不要让用户刚看到页面就被内容推来推去。
如果优化方案牺牲了稳定性,也要谨慎。比如为了快一点先展示旧缓存,但旧缓存和新数据差异很大,用户看到内容瞬间替换;或者为了省首帧时间延后权限判断,导致入口先出现再消失。这些方案可能让指标变好,却让体验变差。技术取舍要回到用户任务,而不是只回到秒表。
什么时候该重构启动链路
有些问题靠局部优化能解决,比如去掉同步 IO、延后 SDK、简化布局。但如果启动链路长期靠补丁维持,可能需要重构。典型信号是:Application 里堆了大量业务初始化;启动任务之间依赖不清;每个模块都想抢首帧前执行;没人能解释启动完成到底由哪些任务组成。
这时可以把启动任务显式编排。每个任务声明依赖、优先级、是否主线程、超时、失败策略。首帧前只允许少数关键任务,其余任务按阶段执行。这样新增任务时,团队能看到它会进入哪个阶段,而不是偷偷加进最前面。
重构启动链路有成本,不适合小项目一开始就上复杂框架。但当启动问题反复出现,靠口头约定已经管不住时,显式编排会比继续靠经验稳。
最后给一个判断标准:冷启动优化不要只问“还能省多少毫秒”,要问“这段时间花在哪里,为什么必须现在花”。冷启动预算先拆路径,优化才有方向,也才不会被下一次需求轻易打回原形。
