如何做性能监控 + 包体/组件瘦身(冷启动、页面 Load、动态/静态库画像)
目标:用“线上可观测数据 + 构建期静态分析”两条腿走路,定位 启动/页面加载慢、动态库过多、组件体积过大/不可控,并形成可重复 的“组件瘦身”闭环。
本文结合当前工程里能看到的关键接入点与典型实现思路说明(可以忽略细枝末节代码,但逻辑链路尽量完整)。
0. 背景:为什么海神能帮助“组件瘦身”?
“组件瘦身”在 iOS 里通常同时包含三类问题:
-
启动慢 常见成因:
+load/initialize过多、启动阶段初始化链太长、主线程 I/O、动态库加载/符号绑定成本高、首页渲染链过长等。 -
运行时开销大(卡顿/内存/崩溃) 这类常常与“引入过重组件”“初始化不当”“页面渲染链冗余”相关。
-
包体大/组件体积不可控 需要做到:知道“哪个 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:108(didFinishLaunchingWithOptions内)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(触发上传)
简化理解:
- main 最顶部记一个“main stamp”;
- 在 Poseidon 启动后,UIProber 开始监听控制器生命周期;
DWMainViewController第一次出现时,补齐“首页节点”;- 在约定的结束点(例如首次页面消失等)把冷启动时间线打包上传。
3. 页面 Load / TTI:如何监控“页面各阶段耗时”
冷启动只解决“进 App 慢”,还需要回答“页面打开慢”。
3.1 页面加载监控的核心:拦截 VC 生命周期 + 打点
LJUPPageLoadManager 负责把控制器生命周期拆成阶段并记录 begin/end:
Pods/LJBaseUIProber/LJBaseUIProber/Classes/PageLoad/LJUPPageLoadManager.m:54(init begin)- 同文件里还能看到
loadView、viewDidLoad、layout、appear等节点连续打点
这些点通常会形成如下结构(示意):
- 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 / 卡顿:如何监控“渲染性能”
LJUPFrameManager 用 CADisplayLink 采样帧率,并统计:
- 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
它的流程是:
imageCount = _dyld_image_count()- for each image:
_dyld_get_image_header(i)取 Mach-O header- 遍历 load command 找
LC_UUID _dyld_get_image_name(i)拿路径并提取 imageName
- 最终得到
{ 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 典型构建期归因方法(平台无关思路)
常见做法有三类(从易到难):
-
LinkMap 分析(最常用) Xcode 可生成 LinkMap(需开启设置)。LinkMap 里会列:
- Object files 列表:每个
.o来自哪个libXXX.a(...)或某个 Pod target - Symbols 列表:每个符号占用大小
- Sections 汇总:TEXT/DATA 等段大小
你可以聚合:
- 按 Pod / 静态库名汇总 size(得到“哪个 pod 贡献了多少代码体积”)
- 统计静态库/对象文件数量
- 找“重复符号/重复实现/重复资源”的线索
- Object files 列表:每个
-
Mach-O / dSYM / nm / otool 组合分析 用
nm看符号,otool看依赖、段、load commands,dwarfdump看 UUID 等; 更适合做“深入诊断/自动化工具”。 -
构建系统侧(Pods 工程/Swift Package)结构化统计 例如 CocoaPods/SwiftPM 的 target graph + build products 报表,做更加结构化的“组件画像”。
7. 本项目的构建期瘦身工具链:fastlane lanes + 私有插件
本仓库里有两个关键 lane(但实现依赖外部插件,不在 repo 内):
-
podbymbolinfo:fastlane/Fastfile:311调用getpodsymbolinfo(...)(通常用于 按 Pod/组件归因符号与体积) -
getpackageanalyse:fastlane/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-cafswitcher:Podfile:10(常见用途:在源码与二进制之间切换)cocoapods-ljflutter:Podfile: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. 关键点总结(对应你关心的三类问题)
-
监控如何接入
JGWorkflow/AppDelegate.m:111→Pods/DWBaseCommon/.../DWPoseidonLauncher.m:82用LJPOSEIDON_LAUNCH聚合加载。 -
冷启动 / 页面 Load 耗时如何采集
- main 起点:
JGWorkflow/main.m:17/Pods/LJBaseUIProber/.../LJUPAppLaunchManager.m:89 - 首页/冷启动对象:
Pods/DWBaseCommon/.../DWPoseidonLauncher.m:100 - 页面分段打点:
Pods/LJBaseUIProber/.../LJUPPageLoadManager.m:54
- main 起点:
-
动态库/静态库数量与体积如何用于瘦身
- 动态库(images)运行时可数:
Pods/LJBaseCustomReporter/.../LJDwarfdumpUUIDTool.m:19 - 静态库运行时不可数,必须构建期归因:本项目通过
fastlane/Fastfile:311、fastlane/Fastfile:325对接外部分析插件(fastlane/ Pluginfile:5)。
- 动态库(images)运行时可数: