编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

    • README
    • 公共方法论

    • 体系建设篇

    • 资源专项篇

    • 流水线专项

    • 业务专项篇

      • App冷启动优化
        • 01.阅读说明
        • 02.贯穿案例
          • 2.1 案例背景
          • 2.2 经验派 6 周折腾(典型反面教材)
          • 2.3 方法派 5 天闭环
          • 2.4 上线效果
          • 2.5 案例如何串起本文
        • 03.启动本质定义
          • 3.1 启动的物理本质
          • 3.2 启动的现象与代价
          • 3.3 度量准则
          • 3.4 行业基准与目标
          • 3.5 8 个反直觉问题
        • 04.关键路径原理
          • 4.1 关键路径模型
          • 4.2 Amdahl 定律的启动版本
          • 4.3 关键路径上的三类典型问题
          • 4.4 跨平台同构原理
          • 4.5 平台差异点矩阵
        • 05.度量与采集
          • 5.1 三类采集方案的本质
          • 5.2 各方案的可见盲区
          • 5.3 跨平台采集对照表
          • 5.4 数据可信度评估
        • 06.归因决策树
          • 6.1 启动归因决策树
          • 6.2 关键路径分析法
          • 6.3 阶段拆解归因法
          • 6.4 任务依赖归因法
        • 07.Android 全链路 ⭐
          • 7.1 进程创建阶段
          • 7.2 ART 加载与 DEX 阶段
          • 7.3 ContentProvider 串行陷阱
          • 7.4 Application.onCreate 阶段
          • 7.5 Activity 创建到首帧
        • 08.iOS 全链路 ⭐
          • 8.1 iOS 启动五阶段
          • 8.2 dyld 链接阶段
          • 8.3 +load 与 Static Init 禁忌
          • 8.4 main() 与 AppDelegate
          • 8.5 viewDidLoad 到 viewDidAppear
        • 09.Web 全链路 ⭐
          • 9.1 Web 启动的物理本质
          • 9.2 关键渲染路径(CRP)
          • 9.3 LCP / FCP / TTI 的差异
          • 9.4 SSR / SSG 与 Hydration
          • 9.5 Service Worker 与离线
        • 10.跨端框架全链路 ⭐
          • 10.1 Flutter 启动模型
          • 10.2 React Native 启动模型
          • 10.3 Compose Multiplatform / KMP
          • 10.4 跨端框架启动开销对比
        • 11.嵌入式全链路 ⭐
          • 11.1 嵌入式启动的物理约束
          • 11.2 RTOS 启动模型
          • 11.3 嵌入式启动优化
        • 12.跨端启动对照
          • 12.1 端到端流程对照表
          • 12.2 启动优化技术对照
          • 12.3 统一启示
        • 13.治理一层路径
          • 13.1 一层命题
          • 13.2 策略 1.1:任务分级 + IdleHandler 推迟
          • 13.3 策略 1.2:任务依赖 DAG 调度
          • 13.4 策略 1.3:数据预取 + 网络并行
          • 13.5 一层反思
        • 14.治理二层调度
          • 14.1 二层命题
          • 14.2 策略 2.1:线程池大小科学化
          • 14.3 策略 2.2:ContentProvider 整合(Android 特异)
          • 14.4 策略 2.3:主进程瘦身(Android)
          • 14.5 二层反思
        • 15.治理三层资源
          • 15.1 三层命题
          • 15.2 策略 3.1:SP / 配置文件异步预读
          • 15.3 策略 3.2:类预加载 + Baseline Profiles
          • 15.4 策略 3.3:SVG/资源异步解码
          • 15.5 策略 3.4:Native 库按需加载
          • 15.6 三层反思
        • 16.治理四层架构
          • 16.1 四层命题
          • 16.2 策略 4.1:插件化 / 动态特性
          • 16.3 策略 4.2:单页多模块(Web)
          • 16.4 策略 4.3:服务端渲染 + 流式输出
          • 16.5 策略 4.4:嵌入式 Snapshot
          • 16.6 优先级判定(ROI)
          • 16.7 四层反思
        • 17.求证实验
          • 17.1 实验一:阿姆达尔上限
          • 17.2 实验二:异步代价测量
          • 17.3 实验三:MultiDex 影响
          • 17.4 实验四:IdleHandler 推迟收益
          • 17.5 实验五:类预加载收益
          • 17.6 五大实验启示
        • 18.实战案例
          • 18.1 金融 App 启动优化最终结果
          • 18.2 跨端同构案例:分级初始化框架
          • 18.3 平台特异案例:Android 启动白屏感知优化
          • 18.4 案例统一启示
        • 19.防劣化体系
          • 19.1 三道防线总览
          • 19.2 编码期 Lint
          • 19.3 CI 卡口与线上 SLO
          • 19.4 灰度与线上回滚
        • 20.跨平台速查
          • 20.1 工具速查
          • 20.2 关键 API 速查
          • 20.3 通用 SLO 速查
        • 21.总结与延伸
          • 21.1 五条核心原则
          • 21.2 五个常见误区
          • 21.3 贯穿案例的方法论提炼
          • 21.4 三个外延
          • 21.5 给团队的建议
          • 21.6 延伸阅读
        • 一句话总结
      • 网络性能分析优化
      • 图片性能解码优化
      • 列表与滚动性能
      • 功耗与电量优化
    • 交付防御篇

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 专栏
  • 性能优化实践
  • 业务专项篇
杨充
2026-05-27
目录

App冷启动优化

# App 启动优化

📊 学习成本预估 | 难度:⭐⭐⭐⭐⭐(5/5)| 阅读:约 55 分钟 | 实操:4 小时 🔗 前置阅读:卷二·01, 04, 06, 卷三·01 | ➡️ 后续延伸:卷五·02

# 目录介绍

  • 01.阅读说明
  • 02.贯穿案例
  • 03.启动本质定义
  • 04.关键路径原理
  • 05.度量与采集
  • 06.归因决策树
  • 07.Android 全链路 ⭐
  • 08.iOS 全链路 ⭐
  • 09.Web 全链路 ⭐
  • 10.跨端框架全链路 ⭐
  • 11.嵌入式全链路 ⭐
  • 12.跨端启动对照
  • 13.治理一层路径 ⭐
  • 14.治理二层调度 ⭐
  • 15.治理三层资源 ⭐
  • 16.治理四层架构 ⭐
  • 17.求证实验 ⭐
  • 18.实战案例
  • 19.防劣化体系
  • 20.跨平台速查
  • 21.总结与延伸

# 01.阅读说明

  • 本文卷归属:卷四 · 业务专项 · 第 1 篇
  • 本文目标层级:L2 进阶 → L3 专家 → L4 架构
  • 适用平台:Android(主) / iOS / Web / 跨端 / 嵌入式
  • 前置阅读:
    • 卷零·01 性能工程总论与第一性原理
    • 卷零·03 性能求证实验方法论
    • 卷三·03 卡顿捕获与归因(启动期同样有卡顿问题)
  • 本文核心命题:

    启动是关键路径上的最优化问题——总启动时长 = 关键路径上所有串行任务的耗时之和。 一切优化都是要么缩短关键路径上的某个任务,要么把它从关键路径上移走(异步 / 延迟)。 不在关键路径上的任务,再慢也不影响启动时长——这是 80% 启动优化"动了很多东西却没什么收益"的根源。


# 02.贯穿案例

本案例贯穿全文:§03 看懂物理本质、§04 用关键路径模型识别瓶颈、§07-§11 拆解各端原理、§17 用实验复盘、§13-§16 给出分层闭环。

# 2.1 案例背景

某头部金融 App,主功能为股票行情 + 交易,用户多在开盘瞬间打开。启动慢直接影响开盘交易额:

  • 冷启动 P50 4.2 秒、P95 6.8 秒——用户反馈:"开盘前 30 秒打开 App,看到行情已经过了"。
  • 业务损失:开盘 30 秒内的交易额比同行少 35%。
  • 应用商店投诉:"启动慢"在投诉前三。
  • 与竞品对比:友商 P50 1.5 秒、P95 2.8 秒——启动慢 2.5-3 倍。

研发组初步反应:"SDK 太多没办法、用户网络慢导致的。"——这是典型的"外因背锅"。

# 2.2 经验派 6 周折腾(典型反面教材)

周次 假设 措施 结果
第 1-2 周 Application 太重 把 28 个 SDK 全部异步初始化 4.2s → 3.9s(-7%)
第 3 周 主页太复杂 主页拆 4 块按需加载 4.2s → 3.7s
第 4 周 启动图太大 压缩闪屏 50% 无变化(图本来就异步加载)
第 5 周 网络太慢 启动期改 HTTP/2 -100ms
第 6 周 全异步化 几乎所有初始化丢线程池 反而 +250ms(异步切换开销)

6 周累积投入,启动只从 4.2s 降到 3.7s。最后一次"全异步化"反向劣化让团队彻底困惑。

复盘:六周折腾错在"经验主义"——凭直觉找耗电源、不区分关键/非关键、相信"异步就是快"。真正的根因是"非关键任务挤进关键路径 + 数据请求串行链",但没人用关键路径模型量化分析过。

# 2.3 方法派 5 天闭环

Day 1(§04 关键路径模型 + §05 度量):

  • 用 Tracing 拉出 Application 阶段任务图。
  • 关键路径只有 710ms(attachBaseContext + 多语言切换 + 安全验证 + 行情连接 + ContentProvider)。
  • 但被 980ms 不必要任务挤到 1850ms(28 个 SDK 都跑主线程)。

→ 找到根因 A:非关键任务挤进主线程。

Day 2(§06 决策树):

  • 首屏数据稳态 1100ms。
  • 行情连接初始化在主线程?Yes。
  • 行情数据请求在 onResume 后?Yes(串行链)。

→ 找到根因 B:数据请求串行。

Day 3-4(§13-§16 四层治理):

  • 第 1 层(路径):23 个非关键 SDK 推到 IdleHandler。
  • 第 2 层(调度):ContentProvider 整合 + 主进程区分。
  • 第 3 层(资源):类预加载 + Baseline Profiles。
  • 第 4 层(架构):Application 阶段就预热行情长连 + 触发数据请求,让网络往返与 UI 初始化并行。

Day 5(A/B 灰度)。

# 2.4 上线效果

指标 经验派 6 周后 方法派 5 天后 友商
启动 P50(中端机) 3700ms 1420ms 1500ms
启动 P95(中端机) 6200ms 2150ms 2800ms
Application 阶段时长 1850ms 380ms -
首屏数据稳态 4200ms 1400ms -
主进程线程数 28 12 -
开盘 30s 交易额(对比同行) -35% +7% 基准
应用商店"启动慢"差评 持续 降至前 20 外 -

核心洞察:启动优化 80% 收益来自找对关键路径,不是"做更多"。经验派错在没分清主次——一开始就该用 Tracing 拉关键路径。启动优化 = 关键路径瘦身 + 非关键任务移出,不是堆并发。

# 2.5 案例如何串起本文

  • §03 启动本质 ▶▶ 启动是任务集 + 依赖图 + 串行预算。
  • §04 关键路径原理 ▶▶ Amdahl 定律证明"全异步化"有硬上限。
  • §06 决策树 ▶▶ 沿"主线程任务过载 + 数据串行"分支精准命中。
  • §07-§11 各端原理 ▶▶ Android ContentProvider 串行 / iOS dyld / Web TTI 各自有"地形特点"。
  • §17 求证实验 ▶▶ §17.1 阿姆达尔上限直接破除"无限并发"幻觉。
  • §13-§16 四层治理 ▶▶ "路径→调度→资源→架构"四层正是案例落地路径。

# 03.启动本质定义

# 3.1 启动的物理本质

启动 = 把"应用从无到有"过程中的所有必要工作,按依赖关系串行 / 并行地执行,直到用户能看到并交互。

三个不可商量的物理约束:

约束一:启动是有"必要工作集"的

启动不是单一任务,而是一个任务集 T = {t₁, t₂, ..., tₙ},每个任务做一件事(如加载类、初始化 SDK、读配置、建立网络连接、渲染首帧)。任务集分两类:

  • 必要任务(critical):直接产出"用户可见可交互"的状态。
  • 可选任务(deferrable):当下不需要,但传统实现中放在启动期(如打点埋点、广告拉取、后台同步)。

启动优化的第一原则:严格区分必要与可选,把可选的全部移出启动期。

约束二:任务之间存在依赖图

任务集不是并行池,而是有向无环图(DAG):

   t1 (读本地配置) ──┐
                     ├──▶ t4 (初始化 SDK)
   t2 (初始化日志) ──┘
                     ├──▶ t5 (建立网络) ──▶ t7 (拉取首屏数据) ──▶ t8 (渲染首屏)
   t3 (准备主线程) ──┘
                     └──▶ t6 (初始化 UI 框架) ──┘
1
2
3
4
5
6

依赖图决定了"哪些任务必须串行""哪些可以并行"。

约束三:启动有"开始"和"结束"两个明确边界

  • 开始:用户点击图标 / 系统拉起应用。
  • 结束:用户能看到首屏并能交互(TTFD)。

这两个边界是物理可观测的(屏幕变化 + 输入响应),所以启动是个"有头有尾的 epoch"问题,可以精确度量。

探索性思考:为什么启动慢比"卡顿慢"更难治? 卡顿是"运行中某一帧超预算"——可以孤立分析。启动是"几百个任务 + 复杂依赖"——优化任何单点都不一定让整体快。启动优化的核心难点不是"算法",而是"系统观"——理解任务之间的相互作用、关键路径的演变、并发的边际收益。这就是为什么 §02 案例 经验派"动了很多东西"却没用——他们一直在调点(动作),没在调系统(路径)。

# 3.2 启动的现象与代价

启动慢是用户对一款应用的"第一印象",且印象一旦形成极难翻转:

  • 首次冷启动慢:用户感知"是不是没装好?"
  • 每次启动黑屏 / 白屏:用户怀疑应用故障。
  • 可见但点不动(TTID 已到但 TTFD 未到):用户操作无响应,主观体验比慢更差。
  • 启动后不稳定(卡顿、ANR、闪退):往往与启动期的"过度异步并发"或"线程争用"相关。

业务代价(行业实测数据):

  • Google Play Console 数据:冷启动 P50 > 5s 的应用,次日留存比 < 2s 的低 15–20%。
  • 头部 App 数据:每减少 1 秒冷启动时长,次日留存 +1–3%、首屏转化率 +2–5%。
  • iOS 系统在冷启动 > 20s 时直接 watchdog 杀死进程,是硬约束。
  • Web:Google Web Vitals 把 LCP > 4s 标记为 Poor,影响 SEO。
  • 嵌入式车机:启动 > 30s 在某些国家会触发安规警告。

# 3.3 度量准则

按 卷零·02 §3 USE/RED/APDEX 的统一指标体系,启动属于"一次性请求"类,主要用 RED + APDEX:

请求视角(RED):

指标 含义 阈值参考
TTID(Time To Initial Display) 首帧可见时间 冷启 < 2s / 热启 < 0.5s
TTFD(Time To Full Display) 首屏完整可交互 冷启 < 3s / 热启 < 1s
启动失败率(Errors) 启动崩溃 / 超时占比 < 0.1%
启动期卡顿率 启动期间帧时长 > 16.67ms 的比例 < 5%

用户感知(APDEX):

  • Satisfied:TTFD < T_target(如 1.5s)
  • Tolerating:T_target ≤ TTFD < 4 × T_target
  • Frustrated:TTFD ≥ 4 × T_target

关键约定:

  • TTID ≠ TTFD:用户看到首帧但内容未加载完,主观仍是"卡"。优化必须并重,TTFD 才是用户感知的"启动完成"。
  • 必须使用 P50 + P99 双轨度量。P99 代表"长尾用户体验",往往揭示低端机 / 弱网下的真实痛点。

# 3.4 行业基准与目标

平台 TTID 良好 TTID 合格 TTFD 良好 TTFD 合格
Android(中端机) < 1.5s < 2.5s < 2.5s < 4s
iOS < 1.0s < 2.0s < 2.0s < 3s
Web(4G) LCP < 2.5s LCP < 4s TTI < 3.8s TTI < 7.3s
嵌入式 HMI < 5s < 10s < 8s < 15s

# 3.5 8 个反直觉问题

带着这些问题阅读:

  1. 减少启动任务数一定能让启动更快吗?
  2. 把所有任务都改异步,启动是否就能快到极致?
  3. CPU 利用率不饱和但启动慢,是哪里的问题?
  4. 多线程并发启动,开多少线程最快?
  5. MultiDex 一定会让启动变慢吗?慢多少?
  6. SharedPreferences / NSUserDefaults 在启动期为什么这么慢?
  7. 异步化任务之后总耗时是不是必然减少?
  8. 8 核 CPU 启动时为什么有时只用了 2 核?

▶▶ 回扣 §02 案例:原团队用单一数字"4.2 秒"描述启动——这掩盖了真相。重定义为分阶段:

阶段 时间 状态
attachBaseContext → Application.onCreate 完成 0 → 1850ms ❌ 超出预算 4×
Application.onCreate → MainActivity.onCreate 1850 → 2400ms ⚠️ 略慢
MainActivity.onCreate → 首帧 2400 → 3100ms ⚠️ 略慢
首帧 → 首屏数据稳态(行情数据可见) 3100 → 4200ms ❌ 超出预算 2×

单一"4.2s"无法定位优化方向;分阶段后立刻看出 Application 阶段(44%)+ 首屏数据阶段(26%)是双瓶颈。


# 04.关键路径原理

本节回答四个根本问题:①为什么 DAG 总耗时 = 最长路径?②Amdahl 定律的启动版本是什么?③异步化的硬上限怎么算?④为什么 race-to-idle 在启动也成立?

# 4.1 关键路径模型

DAG 的总耗时不是所有任务耗时之和,而是最长路径的耗时(关键路径,Critical Path)。这是 PERT/CPM 项目管理理论的核心,同样适用于启动:

   t1 (10ms) ──┐
                ├─▶ t4 (50ms) ──▶ t8 (100ms)  关键路径:t2 → t4 → t8 = 170ms
   t2 (20ms) ──┘
                ├─▶ t5 (30ms) ──▶ t7 (40ms)
   t3 (5ms)  ──┘                              非关键路径:t3 → t5 → t7 = 75ms 实际并行
1
2
3
4
5

关键路径的四条法则(启动优化的"金科玉律"):

  1. 法则一:总启动时长 = 关键路径耗时(不是任务总和)。
  2. 法则二:缩短非关键路径上的任务对总耗时无收益(除非它升级成新关键路径)。
  3. 法则三:缩短关键路径任务,最多缩短到"次关键路径"长度(之后次关键路径成为新瓶颈)。
  4. 法则四:把任务从关键路径移除(异步 / 延迟)的收益与缩短它一样有效,但成本通常更低。

这就是为什么"看似减少了 20 个启动任务,启动时长只少了 100ms"—— 因为只有关键路径上的任务才贡献。

探索性思考:为什么"次关键路径"是个隐藏陷阱? 当你把 A 关键路径优化掉之后,B 路径自动升级成新关键路径——这意味着收益是"阶梯式"而不是线性。比如优化前关键路径 1850ms、次关键路径 1100ms,把关键路径砍到 600ms 后实际总耗时不是 600ms 而是 1100ms(次关键路径接管)。所以"优化前必须看次关键路径多长"——它定义了你的天花板。

# 4.2 Amdahl 定律的启动版本

设启动可异步化的部分占比为 P(0 ≤ P ≤ 1),无法异步化的串行部分占比为 1 − P,并行加速比为 N,则:

   启动加速比 = 1 / [(1 − P) + P/N]

   即使 P = 0.8,N = ∞:加速比上限 = 1 / 0.2 = 5×
1
2
3

这就是反直觉问题 ② 的答案:即使所有可异步化任务都开无限线程并行,启动加速也存在硬上限。要突破这个上限,必须减少串行部分(缩短或删除关键路径任务),而不是单纯堆并发。

P S_max 含义
0.5 2× 即使无限并行最多快一倍
0.7 3.3×
0.8 5×
0.9 10×
0.95 20×

关键洞察:要让启动加速 10×,可异步化部分必须 ≥ 90%。实际启动中能异步化的占比通常 50–70%,所以加速上限通常 2–3×。

探索性思考:为什么 P 在工程中很少超过 0.8? 因为"必须串行"的部分往往不是"代码不会写",而是业务依赖图本身决定的——网络要先于数据请求、UI 框架要先于 Activity、安全 SDK 要先于其他网络。这些是"语义依赖",不是"实现依赖"。P 的天花板由系统架构决定,不是工程能力。要把 P 提到 0.9,必须重构架构(如懒加载、按需初始化、移除前置依赖),不是堆线程。

# 4.3 关键路径上的三类典型问题

把"关键路径阻塞"映射到具体根因,可以分成三类:

  关键路径耗时 = 关键任务自身耗时 + 任务间等待 + 资源竞争

  ┌──────────────────────────────────────┐
  │ A. 任务自身慢                          │
  │    特征:单个任务 CPU 高 / IO 长       │
  │    根因:算法差、磁盘 IO、网络等        │
  ├──────────────────────────────────────┤
  │ B. 任务依赖链长                        │
  │    特征:单任务都不慢,但串成长链路    │
  │    根因:依赖图设计不合理              │
  ├──────────────────────────────────────┤
  │ C. 资源竞争 / 调度等待                 │
  │    特征:CPU 不饱和但任务慢            │
  │    根因:锁、Binder、磁盘竞争、线程不够 │
  └──────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

探索性思考:为什么 C 类问题最难调? 因为 CPU 占用看起来"很正常"——总占用 30%、单核也不饱和,但启动就是慢。根因是"等待"——线程在等锁、等 Binder、等磁盘。这种问题在 Trace 上呈现"白条"(off-CPU 状态)。所以启动调优必须看"实际跑了多少 vs 应该跑多少"——Trace 工具的核心价值。

# 4.4 跨平台同构原理

不同平台的启动看起来差异巨大(Android Zygote fork → Application → Activity;iOS dyld → main → AppDelegate;Web HTML 解析 → JS 执行 → 首次渲染;嵌入式 init → 服务初始化 → UI 主循环),但抽象成"任务依赖图 + 关键路径"后完全同构:

  通用启动模型:

   [系统级初始化] ──▶ [运行时启动] ──▶ [应用初始化] ──▶ [首屏渲染] ──▶ 可交互
        │                  │                │                │
        进程创建            VM/解释器           业务对象            用户可见
1
2
3
4
5

每个平台都必须有:

抽象阶段 解决什么问题
进程 / 上下文创建 OS 内核为应用分配资源(地址空间、文件描述符、线程)
运行时启动 VM / 解释器 / 浏览器引擎初始化(GC、JIT、解析器)
应用初始化 Application 创建、SDK 初始化、配置加载
首页准备 数据加载、UI 框架初始化、首帧渲染

正因为它们都长这个样子,启动的物理本质也是同一个:

启动慢不是平台的特性,是"任务集 + 依赖图 + 串行预算"这个组合的必然产物。

跨平台启动阶段对照

抽象阶段 Android iOS Web 嵌入式
进程创建 Zygote fork exec / dyld 加载 浏览器进程创建 Tab OS 创建任务
运行时启动 ART 加载 dex dyld 链接 + libObjc 启动 V8 / JS 引擎初始化 RTOS 调度
应用初始化 Application.onCreate UIApplication 初始化 + AppDelegate DOMContentLoaded main() / app_init
首页准备 Activity.onCreate → onResume UIViewController.viewDidLoad → viewWillAppear window.load UI 任务循环
可交互完成 Activity 首帧绘制完成 viewDidAppear + 主 RunLoop 空闲 TTI / window.onload + INP UI 主循环就绪

# 4.5 平台差异点矩阵

同构之下,各平台的实现差异主要集中在以下维度:

维度 Android iOS Web 嵌入式
进程模型 Zygote fork(共享 ART) 每次 fork+exec 浏览器子进程 单进程 / 任务
运行时 ART(AOT + JIT) Native + ObjC RT V8 / SpiderMonkey C/C++ 直接执行
类加载策略 Multi-DEX + ART 优化 dylib / framework JS Bundle 解析 静态链接
首屏可见时机 首帧渲染完成 viewDidAppear LCP UI 主循环就绪
系统级阻塞 ContentProvider onCreate 串行 dyld 链接慢 DOM 解析阻塞 设备 init
优化手段 App Startup / 任务调度 SDK Static Init 延迟 / dyld3 Code Splitting / SSR 优先级 + DMA

后续 §07-§11 各端全链路章节会逐一展开。


# 05.度量与采集

# 5.1 三类采集方案的本质

所有平台的启动采集方案,本质上只有 3 类,区别在于在启动生命周期的哪一段下钩子:

   进程开始 ──▶ 运行时就绪 ──▶ Application 创建 ──▶ Activity 创建 ──▶ 首帧 ──▶ 完整可交互
        │              │               │                  │              │            │
        ▼              ▼               ▼                  ▼              ▼            ▼
                       ① 应用埋点(手动打点 / 自动插桩)
                                                                          ② 系统级 API
                                              ③ 端到端外部测量(高速摄像 + LED 触发)
1
2
3
4
5
6

① 应用埋点(手动打点 / 自动插桩)

  • 核心原理:在启动关键路径上的多个代码点埋下时间戳,结束后差值计算各阶段耗时。
  • 物理本质:启动是一个有清晰阶段的进程,在每个阶段边界放一个度量哨兵,事后差值即得各阶段耗时。
  • 局限根源:只能从 Application.attachBaseContext 开始打点,看不到"图标点击 → fork → dyld → ART 初始化"——这段在中低端机上可能占 1–2s。
  • 适用场景:自家代码内部精细监控,配合插桩可获得 ~50 行代码级颗粒度。

② 系统级 API(reportFullyDrawn / MetricKit / Performance API)

  • 核心原理:操作系统知道"进程从被创建到 Application/UI 就绪"的完整时序,提供专门 API 直接告诉你各阶段耗时。
  • 物理本质:应用层无法看到"进程创建 → 运行时启动"这段,但 OS 自己看得到,把这部分数据通过 API 暴露给你。
  • 局限根源:API 颗粒度由系统决定("Application 初始化"打包成一个数字);新版本系统才支持。
  • 适用场景:作为应用埋点的"前置补全",两者必须配合使用。
平台 API 暴露的数据
Android Activity.reportFullyDrawn() 配合 logcat Displayed TTID + TTFD
Android 12+ ApplicationStartInfo 进程级启动时序(含 cold/warm/hot 标记)
iOS MetricKit MXAppLaunchMetric dyld / Process Init / App Init / UI Init / 首帧分阶段
Web Performance.timing / PerformanceNavigationTiming navigationStart / DOMContentLoaded / loadEventEnd
Web LCP / FCP / INP 用户感知关键时刻

③ 端到端外部测量(高速摄像 / 屏幕直采)

  • 核心原理:用外部设备拍下"用户点击图标 → 屏幕显示首屏"全过程,逐帧分析得出物理端到端延迟。
  • 物理本质:软件指标永远是"内部视角",最终用户体验只能由"外部视角"验证。
  • 局限根源:门槛高(外部设备 + 自动化分析 + 固定光照);不能线上,仅实验室。
  • 适用场景:金标准基线测试 + 校准方案 ②/① 的偏差。

三类方案总览

方案 关键钩子位置 输出粒度 性能开销 跨端通用性 线上可用 主要局限
① 应用埋点 应用代码内 任意精细 低 高 ✅ 看不到运行时启动前
② 系统级 API OS 提供 阶段级 极低 中 ✅(API 受限) 颗粒度由系统决定
③ 外部测量 屏幕外 物理时序 无 极高 ❌ 设备 / 流程门槛

方案的"组合定律":没有任何单一方案能 100% 覆盖启动监控。必须组合使用:② 看进程到 Application 之前的"黑盒" + ① 看应用代码内部精细阶段 + ③ 实验室基线校准。

探索性思考:为什么 logcat Displayed 不一定是真相? Android logcat 的 Displayed 时间戳是"应用首帧绘制完成"——但用户看到的可能是 windowBackground(非真实内容)。比如启动后有 800ms 是 windowBackground,logcat 显示 1.2s 完成,但用户实际感知是 2.0s。软件指标的"完成"和用户感知的"完成"可能差几百 ms——这就是为什么金标准必须是高速摄像(方案 ③)。

# 5.2 各方案的可见盲区

阶段 方案 ① 方案 ② 方案 ③
进程创建(fork / dyld) ❌ ✅ ✅
运行时启动(ART / V8) ❌ ✅ ✅
Application.onCreate ✅ ✅ ✅
第三方 SDK 内部 ❌(除非 Hook) ❌ ✅(含在总时间内)
首帧渲染 ✅ ✅ ✅
完整可交互(TTFD) ✅(应用声明) ✅(系统判定) ✅
触摸响应延迟 ❌ 部分 ✅

盲区一:进程创建到 Application 之间:应用埋点最早只能从 Application.attachBaseContext 开始打点。必须用方案 ② 补齐。

盲区二:第三方 SDK 内部:你能埋点 SDK 调用前后,但 SDK 内部如果起 5 个线程每个跑 200ms,应用埋点完全看不到。补救:用 Trace(Systrace / Perfetto / Instruments)。

# 5.3 跨平台采集对照表

维度 Android iOS Web 嵌入式
进程级时序 ApplicationStartInfo(31+) MetricKit MXAppLaunchMetric PerformanceNavigationTiming RTOS trace
应用启动总时长 logcat Displayed viewDidAppear 时间戳 LCP / TTI 业务埋点
TTID logcat Displayed didFinishLaunching 后首帧 FCP 首帧渲染
TTFD Activity.reportFullyDrawn 自定义 os_signpost TTI / INP 业务声明
阶段拆解 Trace section os_signpost interval User Timing API RTOS event
自动插桩 ASM Gradle Plugin SwiftSyntax Babel plugin 编译期 hook

# 5.4 数据可信度评估

不同采集方案的可信度排序:③ 外部测量 > ② 系统级 API > ① 应用埋点。

指标 ① 应用埋点 ② 系统级 API 偏差来源
Application 之后阶段 误差 < 1% 误差 < 1% 几乎无
完整冷启动时长 偏低 ~30%(漏前置) 误差 < 5% ① 漏掉进程 / 运行时启动
端到端物理延迟 不可测 不可测 必须用 ③

工程实践建议:线上以 ② 为主,① 兜底(精细化诊断);每个版本在实验室用 ③ 做一次端到端基线,校准 ②/① 的偏差系数。


# 06.归因决策树

采集只能告诉我们"启动 3 秒",归因要告诉我们"3 秒里 1.2s 是 SDK 初始化、0.8s 是首页 IO"。

# 6.1 启动归因决策树

启动慢的根因有十几种,决策树把"穷举式排查"压缩成"二分式排查":先二分到阶段,再到根因,平均 2 步定位。

启动 > 目标值
   │
   ├── 阶段 A. 进程 / 运行时启动 > 0.8s ──▶ 包大小、动态库链接
   │                                          ├─ Multi-DEX 慢(详见 §17.3)
   │                                          ├─ 静态初始化代码多
   │                                          └─ dylib / .so 链接多
   │
   ├── 阶段 B. Application 阶段 > 0.5s ────▶ Application.onCreate 重
   │                                          ├─ 同步初始化的 SDK 太多
   │                                          ├─ ContentProvider 串行(Android)
   │                                          ├─ AppDelegate 中的同步操作(iOS)
   │                                          └─ DOMContentLoaded 中阻塞 JS(Web)
   │
   ├── 阶段 C. Activity / 首屏控制器 > 0.3s ─▶ UI 创建慢
   │                                          ├─ 布局过深(详见卷三·渲染篇)
   │                                          ├─ 首屏数据同步加载
   │                                          └─ View Inflate 慢
   │
   └── 阶段 D. 首帧之后到可交互 > 1s ────────▶ 数据加载慢
                                               ├─ 网络请求串行
                                               ├─ 缓存未命中
                                               └─ 大列表初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

反直觉问题 ③ 答案:CPU 不饱和但启动慢,几乎一定是 B 类(IO / Binder / 锁等待)或资源竞争。

▶▶ 回扣 §02 案例:金融 App 套用决策树:

  • 启动 P50 = 4.2s(中端机预算 1.5s)
  • Application 阶段 1850ms(超 4×)→ 必查
    • 关键路径? = 710ms
    • 主线程实际跑了? = 1850ms(多 1140ms 是不该串行的任务)
    • 根因 A:非关键任务挤进主线程
  • 首屏 → 数据稳态 1100ms(行情数据)
    • 行情连接初始化在主线程? Yes
    • 行情数据请求在 onResume 后? Yes(串行链)
    • 根因 B:数据预取没在启动并行

# 6.2 关键路径分析法

步骤:

  1. 建任务清单:列出启动期所有任务及其耗时。
  2. 建依赖图:标注每个任务依赖什么。
  3. 求关键路径:DAG 上的最长路径。
  4. 重排 / 异步化:把非关键任务移出,缩短关键任务。

关键路径求解(简化算法):

对每个任务 t,计算:

  • EarliestStart(t) = max(EarliestEnd of all predecessors)
  • EarliestEnd(t) = EarliestStart(t) + Duration(t)

最终:TotalTime = max(EarliestEnd of all tasks)。关键路径 = 任何 EarliestEnd = TotalTime 的链路。

工程上的两个简化工具:

  • Trace 时序图(Perfetto / Instruments / Chrome Performance)直接展示每个任务的开始 / 结束 / 关系。
  • Gantt 图导出(自定义脚本从打点数据生成)便于离线分析。

# 6.3 阶段拆解归因法

按 §6.1 决策树的四阶段,逐阶段定位:

阶段 A:进程 / 运行时

  • 工具:Android ApplicationStartInfo + Trace;iOS Instruments dyld + libObjc;Web Network panel。
  • 典型问题:包体积 > 100MB 慢、静态初始化多、JNI_OnLoad 串行。

阶段 B:Application

  • 工具:Trace + 应用埋点。
  • 典型问题(按耗时排序):ContentProvider.onCreate 串行(Android 特有);数据库 / SP / NSUserDefaults 同步读;网络初始化(建立 SSL / WebSocket);大量 SDK 同步 init。

阶段 C:Activity / 首屏控制器

  • 工具:Choreographer / FrameMetrics(详见卷三·渲染篇)。
  • 典型问题:布局过深、ViewStub 滥用、主题 windowBackground 加载大图。

阶段 D:首帧到可交互

  • 工具:业务埋点 + 网络监控。
  • 典型问题:首页数据网络请求慢、大列表 onBindViewHolder 中同步 IO、异步任务回调到主线程后立即触发卡顿。

# 6.4 任务依赖归因法

反直觉问题 ⑦ 的回应:异步化后总耗时不一定减少。

为什么:

  • A 任务依赖 B:A 异步无法跳过 B 的耗时。
  • B 占用 CPU:异步只是把 B 移到子线程,CPU 总量不变。
  • 异步开销:线程创建、上下文切换本身有成本(详见 §17.2)。

正确的"任务依赖归因"步骤:

  1. 列出每个任务的真实依赖(不是想当然)。
  2. 找出"伪依赖"(误以为依赖,实际可解耦)。
  3. 异步化只对真正无依赖的任务有效。
  4. 异步化必须验证总耗时减少(A/B 测试),否则可能反向收益。

反直觉问题 ④ 的回应:开多少线程最快?

由 卷二·CPU 篇 §5.2 已证明:CPU bound 任务的最优线程数 = CPU 核数(拐点)。超过后上下文切换开销超过并发收益。

启动期推荐线程数:

  • 计算密集任务池:CPU 大核数 + 1(4–6 个)。
  • IO 密集任务池:可更大(10–20 个),但要看磁盘并发能力。
  • 总线程数不要超过 CPU 总核数 × 2,否则启动期线程争用反而拖慢。

探索性思考:为什么"伪依赖"是隐藏陷阱? 工程师写代码时常说"A 必须在 B 后"——但仔细问"为什么必须?"往往得到"因为代码这样写的"。这不是真依赖,是实现依赖。比如 PushSDK init 调用 LogSDK,看起来依赖;但 PushSDK 完全可以延迟 LogSDK 调用到首次需要时。伪依赖的本质是"代码耦合"——重构后可以解开。这是 P 系数(可异步化部分)能从 0.5 提到 0.8 的最大空间。


# 07.Android 全链路 ⭐

本章把 Android 启动从"用户点击图标"一路拆到"首帧渲染完成",回答四个核心问题:Zygote fork 是什么 / Application 阶段为什么这么重 / ContentProvider 为何串行 / Activity 首帧怎么算"完成"。

# 7.1 进程创建阶段

Android 应用进程不是从零创建,而是 Zygote fork——Android 启动时已经创建一个"模板进程"Zygote,加载好了 ART 运行时和 Framework 类。每次启动应用都从 Zygote fork 出来,省掉了 ART 初始化的几百 ms。

   用户点击图标
       │
       ▼
   Launcher → Binder → AMS(system_server)
       │
       ▼
   AMS 检查目标进程是否存在
       │
       ├─ 存在 → 复用(warm/hot 启动)
       │
       └─ 不存在 → 通知 Zygote fork
                      │
                      ▼
                   fork() 系统调用
                      │
                      ▼
                   子进程 = 应用进程(共享父进程地址空间)
                      │
                      ▼
                   子进程执行 ActivityThread.main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

关键事实:

  1. fork 几乎是零成本(COW 写时复制),但进程创建后还要做 OS 资源分配(PID、虚拟内存、文件描述符),约 50-100ms。
  2. 共享 ART:Zygote 已加载的类直接共享(不需要重新加载),这是 Android 启动相对快的最大优势。
  3. 应用代码看不到这一段——只能用 Android 12+ 的 ApplicationStartInfo 拿到。

# 7.2 ART 加载与 DEX 阶段

   ActivityThread.main()
       │
       ▼
   Looper.prepareMainLooper()
       │
       ▼
   ActivityThread.attach()
       │
       ▼
   AMS 通过 Binder 通知"绑定应用"
       │
       ▼
   handleBindApplication
       │
       ├─ 加载 APK 的 dex(如果不在 Zygote 共享类里)
       │
       ├─ ART 解析 / verify / 编译(AOT 已编译则跳过)
       │
       ├─ 加载 Native 库(System.loadLibrary)
       │
       ▼
   ContentProvider.onCreate(所有 Provider 串行)
       │
       ▼
   Application.onCreate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

两个性能要点:

  1. AOT 编译产物(.oat)让 ART 启动接近 Native——但首次安装后第一次启动仍可能因 ART JIT 编译热路径而较慢(一次性)。
  2. Multi-DEX 在 Android 5.0+ 几乎无影响(§17.3 实验)——但 Android 4.x 仍是关键瓶颈。

探索性思考:为什么 Baseline Profiles 在 Android 12+ 这么重要? 因为它告诉 ART"哪些类是热路径"——ART 在安装期对这些类做完整 AOT 编译,启动时直接用编译产物。没有 Profile 时 ART 用"启动后慢慢 JIT"策略——首次启动会更慢。Baseline Profiles 让 Google Play 自动收集线上热路径数据反哺,首次启动加速 30%——这是 Android 启动优化领域近年最重要的进展。

# 7.3 ContentProvider 串行陷阱

Android 独有的"启动地雷":每个 ContentProvider 在 Application.onCreate 之前就执行 onCreate()——而且是主线程串行。

   handleBindApplication
       │
       ▼
   installContentProviders()
       │
       ├─ Provider A.onCreate()(同步等待)
       │
       ├─ Provider B.onCreate()(同步等待)
       │
       ├─ ... 几十个 Provider 串行 ...
       │
       ▼
   Application.onCreate()
1
2
3
4
5
6
7
8
9
10
11
12
13

关键事实:

  • 第三方 SDK 喜欢用 ContentProvider 实现"自动初始化"(不用应用调 init)。
  • 每个 Provider 执行 50-200ms,几十个加起来就是 1-3s。
  • ContentProvider.onCreate 不能异步——是 Framework 强制串行调用。

应对:

  • Jetpack App Startup:把所有第三方 ContentProvider 整合到一个 InitializationProvider,内部用 DAG 调度。
  • 删除可去除的 SDK ContentProvider:很多 SDK 提供"手动 init"选项。

# 7.4 Application.onCreate 阶段

   Application.onCreate
       │
       ▼
   一般业务做的事:
   ├─ 各种 SDK 初始化(统计/推送/广告/日志/崩溃...)
   ├─ 全局配置加载(SP / 数据库)
   ├─ 网络框架初始化
   ├─ 注册 Activity 生命周期监听
   ├─ 路由表 / DI 容器构建
   ├─ ...
       │
       ▼
   返回,Framework 创建启动 Activity
1
2
3
4
5
6
7
8
9
10
11
12
13

这一阶段是 Android 启动优化的"主战场"——80% 的可优化空间都在这里。

典型问题:

问题 占比 解决思路
28+ SDK 同步初始化 30-50% 分级 + IdleHandler 推迟
SP 同步读阻塞 5-15% MMKV / DataStore
数据库初始化 5-10% 异步 + 懒加载
Native 库加载(.so) 5-10% 按需加载
网络框架建连 5-10% 预热 + 并行

# 7.5 Activity 创建到首帧

   Application.onCreate 返回
       │
       ▼
   ActivityThread 创建 LauncherActivity
       │
       ▼
   Activity.onCreate
       │
       ├─ setContentView(XML 解析 + View Inflate)
       ├─ findViewById(逐层遍历)
       ├─ 业务初始化
       │
       ▼
   Activity.onStart → onResume
       │
       ▼
   ViewRootImpl.performTraversals
       │
       ├─ Measure / Layout / Draw
       │
       ▼
   首帧绘制完成 → logcat "Displayed"
       │
       ▼
   首屏数据请求 → onBindViewHolder → ...
       │
       ▼
   Activity.reportFullyDrawn → TTFD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

关键差异:TTID(首帧)≠ TTFD(可交互)——首帧可能只是骨架屏 / 占位图,用户看到但点不动。

探索性思考:为什么"白屏"在 Android 上特别常见? 因为 Android 启动时先显示 windowBackground(主题里的占位色 / 图)——直到 Activity 首帧绘制完成才切换。如果 Application 阶段慢 1s,用户就盯着白屏 1s。应对方法:把 windowBackground 设置成与首屏色调一致的图片,让用户视觉上感觉应用已经"开始加载"。这叫"启动屏感知优化"——不缩短启动时长,但缩短感知启动时长。


# 08.iOS 全链路 ⭐

本章把 iOS 启动从"用户点击图标"一路拆到"viewDidAppear",回答:dyld 在做什么 / +load 为什么是禁忌 / Swift 启动比 OC 慢的原因。

# 8.1 iOS 启动五阶段

iOS 启动的官方五阶段模型(Apple WWDC 2019 提出):

   ┌──────────┬──────────┬─────────┬──────────┬──────────┐
   │  dyld    │ libObjc  │ Static  │  UI      │  Initial │
   │  Linking │  Init    │ Init    │  Init    │  Frame   │
   └──────────┴──────────┴─────────┴──────────┴──────────┘
        │           │          │          │          │
   动态链接库     ObjC 类      静态变量    AppDelegate  首帧渲染
   加载 + 链接    注册        构造函数    + Window
1
2
3
4
5
6
7

关键事实:前三阶段(dyld / libObjc / Static Init)应用代码完全无法干预——你只能通过"减少代码量、减少动态库、减少 +load"来缩短。

# 8.2 dyld 链接阶段

   exec() 加载主二进制
       │
       ▼
   dyld 启动
       │
       ├─ 加载所有依赖的 dylib(递归)
       │
       ├─ Rebase(修正基址)
       │
       ├─ Bind(链接外部符号)
       │
       ├─ ObjC 类注册
       │
       ▼
   dyld 完成 → main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

dyld 性能要点:

因素 影响 优化
动态库数量 每个 dylib 加载 5-20ms 合并 + 减少
符号绑定 每个外部符号 1-10us 减少跨库调用
ObjC 类数 每个类注册 1-5us 减少 OC 类

Apple 推荐:动态库 ≤ 6 个(含系统)——超过会被 watchdog 杀死风险高。

# 8.3 +load 与 Static Init 禁忌

OC 的 +load 方法和 C++ 静态变量构造函数都在 dyld 之后、main() 之前执行——完全在主线程:

@implementation MyClass
+ (void)load {
    // 启动期就执行,无法异步
    [SomeSDK init];     // ❌ 在这里 init SDK 直接拖慢启动
}
@end
1
2
3
4
5
6
static MyService g_service;  // 构造函数在 dyld 后执行
1

为什么是禁忌:

  1. 执行时机不可控——所有 +load 串行执行,顺序由 dyld 决定。
  2. 错误处理困难——抛异常 / 阻塞会让进程直接死。
  3. 测量困难——Instruments 看到的是"main 之前的黑盒"。

应对:

  • 用 +initialize 替代 +load(懒加载,首次发消息时执行)。
  • C++ 全局变量改成函数局部 static(首次调用时构造)。
  • 把"Pod 的初始化"挪到 AppDelegate 显式调用。

探索性思考:为什么 Swift 启动比 OC 慢 100-300ms? Swift 标准库 + 运行时本身较大(dyld 加载 20-50ms),且 Swift 类的元数据注册比 OC 类复杂。对纯 Swift 应用,dyld 阶段比纯 OC 应用多 100-300ms。这是不可消除的"语言税"——选 Swift 就要接受。Apple 的应对是:iOS 13+ 的 dyld3 做了显著优化(启动时使用预生成的"closure"跳过部分链接),让 Swift 应用追平 OC。

# 8.4 main() 与 AppDelegate

   main()
       │
       ▼
   UIApplicationMain
       │
       ▼
   UIApplication 初始化 + AppDelegate 创建
       │
       ▼
   application(_:didFinishLaunchingWithOptions:)
       │
       ├─ 业务做的事:
       │  ├─ 启动 Crashlytics / 各种 SDK
       │  ├─ 设置 Window + RootViewController
       │  ├─ 加载持久化数据(NSUserDefaults / Core Data)
       │  ├─ 注册推送
       │  └─ ...
       │
       ▼
   返回,UIKit 调用 RootVC.viewDidLoad
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

关键认知:iOS 的 didFinishLaunching 类似 Android 的 Application.onCreate + Activity.onCreate 合并——这里慢,启动整体就慢。

优化思想:

  • 同步初始化 < 5 个(关键路径)。
  • 其他 SDK 用 dispatch_async 推迟到首帧后。
  • 用 os_signpost 标注每个阶段,方便 Instruments 分析。

# 8.5 viewDidLoad 到 viewDidAppear

   RootVC.viewDidLoad
       │
       ├─ Storyboard / XIB 解析(如果用)
       ├─ 子视图创建
       ├─ 数据准备
       │
       ▼
   viewWillAppear
       │
       ▼
   首次 layoutSubviews + draw
       │
       ▼
   viewDidAppear(系统认为"显示完成")
       │
       ▼
   首屏数据请求结束 → 业务声明 TTFD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

iOS 的"启动结束"判定:

  • TTID:viewDidAppear 调用时刻。
  • TTFD:业务自己用 os_signpost_interval_end 标注(首屏数据稳态)。

iOS 启动优化的特殊优势:

  • LaunchScreen 自动衔接:iOS 强制 LaunchScreen 与首屏视觉一致——避免 Android 的"白屏"问题。
  • dyld3 closure 缓存:iOS 13+ 自动加速二次启动。
  • Order Files:编译期指定符号顺序,让 dyld 加载更快。

探索性思考:为什么 iOS 启动天然比 Android 快? 因为 iOS 没有 ContentProvider 这种"启动地雷"——AppDelegate 是唯一的启动入口。这是"严格生态"的好处——Apple 不允许应用偷偷在启动期跑代码(除了 +load,且有限制)。反观 Android 生态——SDK 用 ContentProvider 偷启动、Application 阶段被无数 SDK 挤占——这是"开放生态"的代价。


# 09.Web 全链路 ⭐

本章把 Web 启动从"输入 URL"一路拆到"页面可交互",回答:HTML 解析 vs JS 执行的关系 / 为什么 LCP 不等于 TTI / Service Worker 怎么"秒开"。

# 9.1 Web 启动的物理本质

Web 启动比 Native 复杂——它涉及网络往返:

   用户输入 URL
       │
       ▼
   DNS 解析 + TCP 握手 + TLS 握手(首次访问)
       │
       ▼
   下载 HTML(首字节 TTFB)
       │
       ▼
   HTML 解析 → 发现 <script> / <link> / <img>
       │
       ├─ 同步 <script>:阻塞 HTML 解析 → 必须下载 + 执行
       │
       ├─ <link rel="stylesheet">:阻塞渲染(不阻塞解析)
       │
       └─ <img>:异步加载
       │
       ▼
   DOM 解析完成(DOMContentLoaded)
       │
       ▼
   CSS 解析完成 → 首次渲染(FCP / First Contentful Paint)
       │
       ▼
   主要内容渲染完成(LCP / Largest Contentful Paint)
       │
       ▼
   JS 执行完成 + 事件绑定 → 可交互(TTI / Time To Interactive)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

关键差异:Native 启动是"本地任务",Web 启动是"网络任务"。这意味着 Web 启动的物理上限是网络往返延迟——不可能比一次 RTT 更快(除非用缓存)。

# 9.2 关键渲染路径(CRP)

Web 的"关键路径"叫 Critical Rendering Path:

   HTML ──▶ DOM
              │
              ├─▶ Render Tree ──▶ Layout ──▶ Paint
              │
   CSS ──▶ CSSOM
   
   JS(阻塞)──▶ 暂停 DOM 构建
1
2
3
4
5
6
7

关键事实:

上次更新: 2026/06/07, 10:26:12
动画交互响应优化
网络性能分析优化

← 动画交互响应优化 网络性能分析优化→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式