如何做性能监控 + 包体/组件瘦身(冷启动、页面 Load、动态/静态库画像)

目标:用“线上可观测数据 + 构建期静态分析”两条腿走路,定位 启动/页面加载慢动态库过多组件体积过大/不可控,并形成可重复 的“组件瘦身”闭环。

本文结合当前工程里能看到的关键接入点与典型实现思路说明(可以忽略细枝末节代码,但逻辑链路尽量完整)。


0. 背景:为什么海神能帮助“组件瘦身”?

“组件瘦身”在 iOS 里通常同时包含三类问题:

  1. 启动慢 常见成因:+load/initialize 过多、启动阶段初始化链太长、主线程 I/O、动态库加载/符号绑定成本高、首页渲染链过长等。

  2. 运行时开销大(卡顿/内存/崩溃) 这类常常与“引入过重组件”“初始化不当”“页面渲染链冗余”相关。

  3. 包体大/组件体积不可控 需要做到:知道“哪个 pod/模块占了多少”,“是静态链接变大还是动态 Framework 太多”,“是资源还是代码”,“是否有重复引入/重复资源/无用符 号”等。

海神(Poseidon)在本项目里的定位是:

  • 线上/灰度环境:采集冷启动、页面加载、帧率、OOM/Watchdog、日志回捞等指标并上报;
  • 构建期(CI/本地):通过 fastlane 插件做包体与符号归因,输出“组件体积画像”。

1. 接入总览:从 main 到 Poseidon 模块加载

1.1 应用启动链路

  • main 中在 UIApplicationMain 之前记录 main 起点: JGWorkflow/main.m:16

    这里调用 ljuplaunch_appendMainStamp(),目的是把“main 函数起点”纳入冷启动时间线(后面会解释这个点如何被上报)。

  • AppDelegate 中启动海神监控集合: JGWorkflow/AppDelegate.m:108didFinishLaunchingWithOptions 内) JGWorkflow/AppDelegate.m:111(调用 startMonitorSet

1.2 Poseidon 聚合器:DWPoseidonLauncher

真正“把各监控模块挂到 Poseidon 上”的总入口在:

  • Pods/DWBaseCommon/DWBaseCommon/Classes/DWPlatform/DWPoseidonLauncher.m:82

核心逻辑是 LJPOSEIDON_LAUNCH(...),把一串子模块加载进去(上下文/性能/崩溃/内存/日志等)。并且注意它默认 在非 DEBUG 环境启用(#if DEBUG ... #else ... #endif),也就是更偏向“线上可观测”。

此外,它还把业务用户信息通过 LJCONTEXT.delegate 提供给平台,用于在海神后台按用户维度检索与关联(见 Pods/DWBaseCommon/DWBaseCommon/ Classes/DWPlatform/DWPoseidonLauncher.m:85 起)。


2. 冷启动监控:如何统计“启动各阶段耗时”

2.1 关键思想:用 “Dot/Timeline” 描述启动阶段

冷启动不是一个点,而是一段时间线。典型会包含:

  • main 起点(main 函数刚开始)
  • UIApplicationMain / didFinishLaunching
  • window/rootVC 设置完成
  • 首页(home)首次可见/首次出现
  • 首次可交互(有些体系会统计 TTI)

在本项目里,冷启动的“main 起点”由 ljuplaunch_appendMainStamp() 提供:

  • 调用点:JGWorkflow/main.m:17
  • 实现:Pods/LJBaseUIProber/LJBaseUIProber/Classes/AppLaunch/LJUPAppLaunchManager.m:89

它会创建一个 Dot(类似时间戳节点)并追加进“冷启动对象”的时间线中。

2.2 “首页是谁”如何确定?

DWPoseidonLauncher 在加载 UIProber 时指定了首页类(home page class):

  • Pods/DWBaseCommon/DWBaseCommon/Classes/DWPlatform/DWPoseidonLauncher.m:100

这里传了 DWMainViewController,也就是冷启动监控会把“首页出现”作为关键节点之一(具体“出现”的时机由 UIProber 的控制器状态回调决定)。

2.3 何时触发上报?

UIProber 的冷启动管理器在第一次首页出现时绑定 homepage 名称与 pageLoad 对象,并在某个时机触发上报:

  • Pods/LJBaseUIProber/LJBaseUIProber/Classes/AppLaunch/LJUPAppLaunchManager.m:59(记录 homepage/pageLoad)
  • Pods/LJBaseUIProber/LJBaseUIProber/Classes/AppLaunch/LJUPAppLaunchManager.m:72(触发上传)

简化理解:

  1. main 最顶部记一个“main stamp”;
  2. 在 Poseidon 启动后,UIProber 开始监听控制器生命周期;
  3. DWMainViewController 第一次出现时,补齐“首页节点”;
  4. 在约定的结束点(例如首次页面消失等)把冷启动时间线打包上传。

3. 页面 Load / TTI:如何监控“页面各阶段耗时”

冷启动只解决“进 App 慢”,还需要回答“页面打开慢”。

3.1 页面加载监控的核心:拦截 VC 生命周期 + 打点

LJUPPageLoadManager 负责把控制器生命周期拆成阶段并记录 begin/end:

  • Pods/LJBaseUIProber/LJBaseUIProber/Classes/PageLoad/LJUPPageLoadManager.m:54(init begin)
  • 同文件里还能看到 loadViewviewDidLoadlayoutappear 等节点连续打点

这些点通常会形成如下结构(示意):

  • Init begin/end
  • LoadView begin/end
  • ViewDidLoad begin/end
  • WillLayout/DidLayout begin/end
  • WillAppear/DidAppear begin/end
  • (可选)TTI:首帧渲染完成/首次可交互时间

3.2 TTI 与“二次渲染/手动上报”的处理

页面加载监控里往往会遇到:

  • 列表页:第一次出现只是骨架屏,真正内容稍后异步回来;
  • Flutter/Hybrid:生命周期与渲染链不完全等价。

LJUPPageLoadManager 里能看到对 TTI 监控开关以及“rerender”/“manual rerender”的逻辑分支(例如同文件中 ljpr_controllerEndRerender... 一 段)。

这类机制对瘦身的意义是: 你不仅能知道“页面总耗时”,还能拆出“哪一段最慢”,从而把瘦身/优化动作落到具体模块(网络、解析、布局、图片、Web/Flutter容器等)。


4. FPS / 卡顿:如何监控“渲染性能”

LJUPFrameManagerCADisplayLink 采样帧率,并统计:

  • FPS 分布(每秒帧数)
  • 严重卡顿(单帧耗时超过阈值)
  • 阈值支持云控动态调整

关键实现位置:

  • Pods/LJBaseUIProber/LJBaseUIProber/Classes/Frame/LJUPFrameManager.m:33

为什么这与“组件瘦身”有关? 很多“组件过重”的后果不是包体,而是 页面渲染链变重(复杂 UI、过多监听、频繁刷新、Web/Flutter桥接过度),卡顿数据可以反向证明“某模块引入 后导致性能回退”。


5. 动态库数量:运行时如何统计、如何用于瘦身

5.1 动态库的定义(工程视角)

iOS 里“动态库数量”通常指:

  • App 进程运行时由 dyld 加载的 images 数量 包括:主可执行文件、动态 frameworks、dylibs、系统库等。

动态 frameworks 数量过多会带来:

  • 启动时 dyld 加载/重定位成本上升
  • 可能增加内存占用(尤其是符号/段映射开销)
  • 影响冷启动指标

5.2 本工程里现成的 dyld 枚举用法

工程中已有遍历 dyld images 并解析每个 image 的 LC_UUID 的实现:

  • Pods/LJBaseCustomReporter/LJBaseCustomReporter/Classes/Flutter/LJDwarfdumpUUIDTool.m:19

它的流程是:

  1. imageCount = _dyld_image_count()
  2. for each image:
    • _dyld_get_image_header(i) 取 Mach-O header
    • 遍历 load command 找 LC_UUID
    • _dyld_get_image_name(i) 拿路径并提取 imageName
  3. 最终得到 { imageName -> uuid } 字典

这个字典本质上就是“进程加载二进制清单”。 拿到清单后,你可以:

  • 统计动态库总数imageCount(可再过滤系统库/只保留 app bundle 内)
  • 统计 app 自带 frameworks 数:过滤 path 包含 /*.app/Frameworks/
  • 找异常引入:同名不同路径、重复加载、debug-only frameworks 泄漏等
  • 把“动态库数量变化”与“冷启动变化”关联:形成瘦身 KPI

注意:运行时看到的“image 数量”≠“Pod 数量”,静态库不会作为独立 image 出现(下一节解释)。


6. 静态库数量/体积:为什么运行时看不到?应该怎么做归因?

6.1 静态库为什么无法用 dyld image 统计?

静态库(*.a)在链接阶段被“拆成目标文件(.o)并合并进主二进制或某个静态 framework”:

  • 运行时它不再是单独的 Mach-O image
  • dyld 无法把它当作“一个库”计数

因此:静态库数量/体积归因基本属于构建期问题,要靠 LinkMap / 符号 / Mach-O 静态分析来完成。

6.2 典型构建期归因方法(平台无关思路)

常见做法有三类(从易到难):

  1. LinkMap 分析(最常用) Xcode 可生成 LinkMap(需开启设置)。LinkMap 里会列:

    • Object files 列表:每个 .o 来自哪个 libXXX.a(...) 或某个 Pod target
    • Symbols 列表:每个符号占用大小
    • Sections 汇总:TEXT/DATA 等段大小

    你可以聚合:

    • 按 Pod / 静态库名汇总 size(得到“哪个 pod 贡献了多少代码体积”)
    • 统计静态库/对象文件数量
    • 找“重复符号/重复实现/重复资源”的线索
  2. Mach-O / dSYM / nm / otool 组合分析nm 看符号,otool 看依赖、段、load commands,dwarfdump 看 UUID 等; 更适合做“深入诊断/自动化工具”。

  3. 构建系统侧(Pods 工程/Swift Package)结构化统计 例如 CocoaPods/SwiftPM 的 target graph + build products 报表,做更加结构化的“组件画像”。


7. 本项目的构建期瘦身工具链:fastlane lanes + 私有插件

本仓库里有两个关键 lane(但实现依赖外部插件,不在 repo 内):

  • podbymbolinfofastlane/Fastfile:311 调用 getpodsymbolinfo(...)(通常用于 按 Pod/组件归因符号与体积

  • getpackageanalysefastlane/Fastfile:325 调用 packageanalyse(...)(通常用于 包体结构分析:可执行文件、Frameworks、资源、插件等)

这些能力来自 fastlane 私有插件(不在本仓库):

  • fastlane/Pluginfile:5

实务经验:

  • packageanalyse 更像“包体剖析/目录级统计”(Frameworks/资源/可执行文件大小等);
  • getpodsymbolinfo 更像“代码体积归因”(LinkMap/符号级聚合到 pod)。

8. 启动优化的“order file”:如何与瘦身结合

“组件瘦身”不只是删依赖,很多时候是:

  • 让启动路径更短(少初始化/延迟初始化)
  • 让启动路径更快(减少指令/缓存失效率)

8.1 本项目的启动符号采集:DWAppMethodHook

工程里有一个典型的“按实际执行路径生成 order file”的实现:

  • Pods/DWBaseCommon/DWBaseCommon/Classes/Utils/DWAppMethodHook.m:14

它使用 sanitizer coverage 回调:

  • __sanitizer_cov_trace_pc_guard 在函数被执行时拿到返回地址 PC
  • 通过 dladdr 解析成符号名
  • 去重、反转后写到 Documents/launch.order

这份 launch.order 的用途通常是:

  • 作为链接器 -order_file 输入,使“启动路径上的函数”在二进制中更集中,
  • 从而提升启动时的指令局部性(减少 page fault / i-cache miss),带来冷启动收益。

8.2 仓库中的 link.order

仓库根目录有一份 link.order(例如 link.order:1),看起来就是一份预生成/预维护的 order 文件。

是否真正生效取决于 Xcode Build Settings 是否配置了 -order_file(当前从 JinggongWorkflow.xcodeproj/project.pbxproj 片段里未直接看到 ORDER_FILE 字段命中;也可能是通过 xcconfig/脚本注入,需结合实际构建环境确认)。

结论:

  • 海神负责“冷启动耗时可观测”;
  • DWAppMethodHook/link.order 提供“启动路径优化手段”;
  • 两者结合就是“可测量 → 可优化 → 可验证”。

9. CocoaPods 侧的“组件形态管理”:源码/二进制切换与 Flutter 集成

瘦身实践里常见一类需求是: “同一个组件,在不同环境以不同形态引入(源码/二进制)”,从而平衡:

  • 开发效率(编译速度)
  • 包体/启动性能(动态库数量、符号表、无用代码)
  • 可控性(排查问题)

本项目 Podfile 中启用了:

  • cocoapods-cafswitcherPodfile:10(常见用途:在源码与二进制之间切换)
  • cocoapods-ljflutterPodfile:11(Flutter 容器/产物集成相关)
  • 并且当前打开了 all_source!Podfile:14(意味着偏向全源码形态)

此外项目里还有用于安装本地 Flutter 插件 podspec 的脚本辅助:

  • script/podInstallFlutter.rb:1

这些属于“组件管理/工程效率”层面的能力,和“包体瘦身”不完全等价,但在大型工程里通常会配合使用。


10. 把这些串成“完整瘦身闭环”:你应该如何使用这些数据?

下面是一条可落地的流程(更像团队实践 SOP):

10.1 建立基线指标(必须有对照)

  • 冷启动:main → 首页出现的关键分段耗时(海神 UIProber)
  • 页面加载:核心页面的 init/loadView/viewDidLoad/layout/TTI 分段耗时(海神 PageLoad)
  • 动态库:dyld images 数(至少统计 app 自带 frameworks/dylibs 子集)
  • 包体:可执行文件大小、Frameworks 目录大小、资源目录大小(packageanalyse)
  • 代码体积归因:按 Pod/模块聚合的体积 TopN(getpodsymbolinfo)

10.2 定位“瘦身方向”

  • 冷启动慢且动态库多:优先减少动态 frameworks 数量、合并/静态化部分组件、清理不必要的动态依赖。
  • 冷启动慢但动态库不多:重点排查启动阶段初始化链(大量 +load、主线程 I/O、网络/配置拉取、日志/监控初始化过重),用启动阶段打点分段去 定位。
  • 页面慢:看页面加载分段,定位是数据/布局/图片/列表/Flutter bridge。
  • 包体大:先做“目录级结构分析”(packageanalyse),再做“代码体积归因”(getpodsymbolinfo/LinkMap),区分是资源还是代码。
  • 体积归因不清晰:补齐 LinkMap 产物与符号聚合逻辑(通常由 getpodsymbolinfo 插件完成)。

10.3 实施优化并验证

  • 减动态库:合并 framework、减少动态组件、审计 Pods 引入方式
  • 启动优化:延迟初始化、拆分启动任务、减少主线程工作、必要时使用 order file
  • 删除/替换重组件:用“体积归因 TopN + 业务必要性”推动决策
  • 回归验证:对照海神数据看冷启动/页面加载是否真实下降,同时监控崩溃/OOM/卡顿是否回退

11. 关键点总结(对应你关心的三类问题)

  1. 监控如何接入 JGWorkflow/AppDelegate.m:111Pods/DWBaseCommon/.../DWPoseidonLauncher.m:82LJPOSEIDON_LAUNCH 聚合加载。

  2. 冷启动 / 页面 Load 耗时如何采集

    • main 起点:JGWorkflow/main.m:17 / Pods/LJBaseUIProber/.../LJUPAppLaunchManager.m:89
    • 首页/冷启动对象:Pods/DWBaseCommon/.../DWPoseidonLauncher.m:100
    • 页面分段打点:Pods/LJBaseUIProber/.../LJUPPageLoadManager.m:54
  3. 动态库/静态库数量与体积如何用于瘦身

    • 动态库(images)运行时可数:Pods/LJBaseCustomReporter/.../LJDwarfdumpUUIDTool.m:19
    • 静态库运行时不可数,必须构建期归因:本项目通过 fastlane/Fastfile:311fastlane/Fastfile:325 对接外部分析插件(fastlane/ Pluginfile:5)。