应用APM的设计
# 应用APM的设计
📊 学习成本预估 | 难度:⭐⭐⭐⭐⭐(5/5)| 阅读:约 50 分钟 | 实操:4 小时 🔗 前置阅读:卷零·02, 04 | ➡️ 后续延伸:卷一·02-03
# 目录介绍
- 00.阅读说明
- 00.5 贯穿案例:某电商 App"自研 APM 反噬性能"事件
- 01.APM设计背景
- 02.APM解决的问题
- 03.通用设计和原理
- 04.性能监控设计
- 05.内存分析监控设计
- 06.渲染性能监控设计
- 07.网络性能监控设计
- 08.崩溃与错误监控设计
- 09.用户行为与事件追踪设计
- 10.数据上报与聚合
- 11.APM整体架构总览
- 12.求证实验(5 个)
- 13.优化策略深化(4 层)
- 14.总结与一句话总结
# 00.阅读说明
- 本文卷归属:卷一 · 体系建设篇 · 第 1 篇
- 本文目标层级:L3 专家 → L4 架构师
- 适用平台:Android / iOS / Web / 嵌入式
- 前置阅读:
- 本文核心命题:
APM 是性能优化的"操作系统":没有 APM,所有专项优化都是盲打;有了 APM,问题才能"被看见、被量化、被持续治理"。
但 APM 本身必须自律——SDK 自己成为性能瓶颈,是反讽且常见的反模式。
# 00.5 贯穿案例:某电商 App"自研 APM 反噬性能"事件
本案例贯穿全文:§01-02 看懂 APM 价值与边界、§03 看清架构与采集原理、§12 用实验数据复盘、§13 给出分层治理闭环。
# 案例背景
某头部电商 V11.0 上线全新自研 APM SDK(取代原 Bugly + 阿里听云组合),目标是"全量、实时、零成本接入"。上线后APM 反而成为最大性能瓶颈:
- APM SDK 自身 CPU 占用稳态 6-8%(行业基准 < 1%)。
- 应用启动 P95 +480ms(APM init 同步阻塞主线程)。
- 每日上报流量 12GB/百万用户(远超 50MB 预算 240×)。
- 核心问题数据反而丢失 35%——SDK 自身 OOM 后丢日志、网络拥塞后丢崩溃栈。
- 老板震怒:"监控自己出事还要监控?"
研发组初步反应:"是采集太多了,降采样就好。"——这是经典的"治标不治本"。
# 经验派的 5 周折腾(典型反面教材)
| 周次 | 动作 | 结果 |
|---|---|---|
| 第 1 周 | 把所有采样率降到 10%(含崩溃) | 流量降到 2GB,但崩溃数据丢 90%,线上事故无法排查 |
| 第 2 周 | 把 SDK 初始化改异步(怕阻塞启动) | 启动恢复,但SDK 初始化前的 800ms 关键事件全部漏采 |
| 第 3 周 | 加更多 try-catch(怕 SDK 崩业务) | SDK 自身崩溃被吞但行为异常更隐蔽 |
| 第 4 周 | 把所有数据走单一上报通道(简化架构) | 崩溃数据与统计数据排同队列,崩溃常被挤掉 |
| 第 5 周 | 加大本地缓存到 100MB(怕丢数据) | 设备存储压力大,用户投诉"App 越来越占空间" |
复盘:五周折腾错在"对 APM 本质认知不足"——APM 不是"采集越多越好",是**"在 SDK 自律 + 数据精炼 + 上报可靠 + 闭环治理"四维度找最优解**。每一个"加多"的动作都在恶化某一维度。
# 方法派的 10 天闭环
新接手的架构师按本文方法论重做:
Day 1-2(§01-03 重新理解 APM 本质 + §12 实验数据):
- 用 §12.1 实验 数据论证:"崩溃必须 100% 采样,性能 5-10% 即可"——一刀切降到 10% 是错的。
- 用 §12.2 实验 数据论证:ASM 编译期插桩开销 < 0.5%,运行时 Hook 5-10%——选错方式是开销暴涨的根因。
Day 3-4(§13 第一层性能自律):
- SDK init 拆分:核心采集(崩溃)同步、其余异步;启动 +480ms→+15ms。
- 所有运行时 Hook 改 ASM 编译期插桩。
- SDK 自身 CPU/内存 SLO 监控("监控的监控")。
Day 5-6(§13 第二层数据精炼):
- 分类采样:崩溃 100%、ANR 100%、慢请求 100%、性能指标 10%、行为事件 5%。
- 端侧聚合(histogram/分位)替代逐条上报。
- 优先级队列:P0(崩溃/ANR)独立通道。
Day 7-8(§13 第三层上报可靠性):
- 本地存储改 SQLite(限 5MB)+ MMKV(5MB 元数据),不再无限增长。
- 上报通道按优先级分离:P0 走单独 HTTP/2 长连接,其他走 gzip 批量。
- 失败指数退避 + 24h 兜底丢弃。
Day 9-10(§13 第四层闭环治理 + 上线):
- 看板按"采集质量 + 上报质量 + SDK 自身指标"三维度。
- 告警分级 + 灰度验证。
# 上线效果
| 指标 | 经验派 5 周后 | 方法派 10 天后 | 行业基准 |
|---|---|---|---|
| APM SDK CPU 占用 | 6-8% | 0.4% | < 1% |
| 启动 P95 增量 | +120ms(异步后) | +15ms | < 50ms |
| 每日上报流量 | 2GB(采样后但丢崩溃) | 180MB | < 200MB |
| 崩溃数据完整性 | 10% | 99.5% | 100% |
| 线上事故定位时长 | > 24h | < 2h | < 4h |
| 用户投诉"APP 占空间" | 持续 | 归零 | - |
核心洞察:APM 不是"加多就好"——它是**"自律 + 精炼 + 可靠 + 闭环"四维度的精密工程**。经验派 5 周折腾错在把 APM 当作"采集越多越好"的工具,方法派 10 天闭环用"四层分级治理"达到行业前 10%。监控自己出事就是失职。
# 案例如何串起本文
- §01-02 APM 价值 ▶▶ APM 不只是"采集",更是"约束自身 + 提供决策"。
- §03 通用设计 ▶▶ 分层架构 + 采样降级 + Hook 原理直接对应案例四层治理。
- §12 求证实验 ▶▶ 5 个实验为案例每个决策提供数据支撑。
- §13 分层策略 ▶▶ "自律→精炼→可靠→闭环"四层正是案例落地路径。
# 01.APM设计背景
# 1.1 什么是APM
APM(Application Performance Management/Monitoring,应用性能管理/监控)是一套用于实时监控、度量、分析应用运行状况的技术体系。
核心目标:在用户感知到问题之前,主动发现并定位性能瓶颈、内存泄漏、崩溃、网络异常等质量问题,保障用户体验。
APM 通常包含三大核心能力:
- 数据采集:在客户端/服务端埋点采集性能、错误、行为数据
- 数据传输:高效、可靠地将采集数据上报到后端
- 数据分析:对海量数据进行聚合、告警、可视化展示
# 1.2 为何需要APM
业务驱动:
- 启动慢 3 秒,用户流失率可达 50% 以上
- 一次崩溃可能导致用户永久卸载
- 线上问题无法复现时,APM 数据是唯一的"现场证据"
技术驱动:
- 移动设备碎片化严重(Android 数千机型、不同 OS 版本)
- 网络环境复杂(WiFi/4G/5G/弱网/代理)
- 应用复杂度持续增长(多线程、跨进程、Hybrid 架构)
各端面临的共性挑战:
用户侧感知:启动慢 | 卡顿 | 闪退 | 白屏 | 加载慢 | 耗电
↓ ↓ ↓ ↓
技术侧根因:主线程阻塞 | 内存泄漏 | 过度绘制 | 网络超时
JSON解析慢 | 大图加载 | 数据库锁 | ANR/Watchdog
2
3
4
# 1.3 各端APM现状
Android 端:
- 开源方案:腾讯 Matrix、字节 Rhea、微信 MMKV 性能分析
- 官方工具:Android Profiler、Perfetto、StrictMode
- Hook 手段丰富:ASM 字节码插桩、PLT Hook、Inline Hook
iOS 端:
- 开源方案:腾讯 Matrix-iOS、字节 Heimdallr、微信 OOMDetector
- 官方工具:Instruments、MetricKit、Xcode Organizer
- Hook 手段:Method Swizzling、fishhook、Mach 异常捕获
Web 端:
- 开源方案:Sentry、Lighthouse CI、web-vitals
- 浏览器 API:Performance API、PerformanceObserver、Long Tasks API
- 核心指标:Core Web Vitals(LCP/FID/CLS/INP)
# 02.APM解决的问题
# 2.1 核心问题域
| 问题域 | 具体问题 | 价值 |
|---|---|---|
| 性能 | 启动要多久?页面加载要多久?帧率是否流畅? | 量化用户体验 |
| 稳定性 | 崩溃率多少?哪些异常最多?影响多少用户? | 保障可用性 |
| 内存 | 内存是否泄漏?峰值多少?是否触发 OOM? | 防止闪退 |
| 网络 | 请求成功率?平均延时?哪些接口慢? | 优化通信 |
| 渲染 | 是否卡顿?掉帧原因?布局是否过深? | 流畅体验 |
| 行为 | 用户路径?操作频率?功能使用率? | 产品决策 |
# 2.2 各端典型痛点
Android 典型痛点:
- ANR(Application Not Responding):主线程阻塞 5 秒以上
- 内存泄漏:Activity/Fragment 泄漏导致 OOM
- 启动黑屏:冷启动 Application.onCreate 耗时过长
- 机型碎片化:低端机卡顿严重,高端机正常
iOS 典型痛点:
- Watchdog 杀死:主线程卡死超过阈值被系统杀死
- OOM 闪退:内存超限被 Jetsam 机制杀死(无崩溃日志)
- 启动耗时:动态库加载、+load 方法执行过多
- 离屏渲染:圆角、阴影触发 GPU 离屏渲染导致掉帧
Web 典型痛点:
- 白屏时间长:JS 包体过大,首屏渲染慢
- Long Task 阻塞:超过 50ms 的任务阻塞主线程
- 内存泄漏:闭包、DOM 引用、定时器未清理
- 资源加载慢:图片、字体、第三方脚本拖慢页面
# 2.3 APM功能全景图
┌────────────────── APM 功能全景 ──────────────────┐
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌─────────┐ │
│ │性能监控│ │内存监控│ │渲染监控│ │ 网络监控 │ │
│ │启动耗时│ │泄漏检测│ │FPS/卡顿│ │ 请求耗时 │ │
│ │页面加载│ │OOM防护 │ │布局分析│ │ 成功率 │ │
│ │CPU占用 │ │内存水位│ │过度绘制│ │ 流量统计 │ │
│ └────────┘ └────────┘ └────────┘ └─────────┘ │
│ │
│ ┌────────┐ ┌────────┐ ┌─────────────────────┐ │
│ │崩溃监控│ │事件追踪│ │ 数据上报与分析 │ │ │
│ │Java/OC │ │用户行为│ │采样策略 · 数据聚合 │ │ │
│ │Native │ │页面路径│ │实时告警 · 可视化报表 │ │ │
│ │JS异常 │ │自定义 │ │智能归因 · 趋势分析 │ │ │
│ └────────┘ └────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 03.通用设计和原理
# 3.1 分层架构设计
APM SDK 采用分层解耦架构,确保各监控模块独立可插拔:
┌──────────────────────────────────────────────┐
│ 应用层(业务代码) │
├──────────────────────────────────────────────┤
│ API 层(对外接口) │
│ APM.init() / APM.startTrace() / APM.log() │
├──────────────────────────────────────────────┤
│ 采集层(Monitor 模块) │
│ ┌────────┐┌────────┐┌────────┐┌─────────┐ │
│ │性能采集││内存采集││渲染采集││网络采集 │ │
│ └────────┘└────────┘└────────┘└─────────┘ │
│ ┌────────┐┌────────┐ │
│ │崩溃采集││事件采集│ │
│ └────────┘└────────┘ │
├──────────────────────────────────────────────┤
│ 数据层(存储 + 上报) │
│ 本地缓存 → 批量聚合 → 压缩加密 → 网络上报 │
├──────────────────────────────────────────────┤
│ 基础层(平台抽象) │
│ 线程调度 / 时钟精度 / 系统信息 / 文件IO │
└──────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
设计原则:
- 最小侵入:不改变业务代码逻辑,通过 Hook/AOP 自动采集
- 低开销:采集逻辑运行在子线程,CPU 占用 < 1%,内存增量 < 2MB
- 高可靠:采集失败不影响业务,数据本地持久化防丢失
- 可配置:支持远程动态下发采集开关、采样率、上报频率
# 3.2 采集层核心原理
APM 的采集层需要解决一个核心矛盾:如何在不修改业务代码的前提下,拿到足够细粒度的性能数据?
三大核心技术手段:
(1)Hook / Swizzle / 代理拦截
原始调用链:App → System.method()
Hook 后: App → Hook.before() → System.method() → Hook.after()
↓ 记录开始时间 ↓ 计算耗时 → 上报
2
3
(2)字节码插桩 / ASM(Android 特有)
在编译期通过 Gradle Transform + ASM 修改字节码,在每个方法入口/出口插入耗时统计。
(3)系统回调 / Observer 模式
| 平台 | 系统回调 | 用途 |
|---|---|---|
| Android | Choreographer.FrameCallback | 帧率监控 |
| Android | Looper.Printer | 主线程卡顿检测 |
| iOS | CFRunLoopObserver | 主线程卡顿检测 |
| iOS | MetricKit (iOS 13+) | 系统级性能指标 |
| Web | PerformanceObserver | 性能条目监听 |
# 3.3 数据通道设计
采集点 本地缓存 上报通道 后端
│ │ │ │
│── 性能指标 ──→ │ │ │
│── 崩溃数据 ──→ MemBuffer ──→ BatchQueue ──→ HTTP/WS ──→│
│── 网络数据 ──→ │ │ │
│── 行为数据 ──→ SQLite/MMKV ──→ 失败重试队列 │
│ (持久化兜底) (指数退避) │
2
3
4
5
6
7
数据优先级:
- P0 立即上报:崩溃日志、ANR、OOM
- P1 批量上报:性能指标、网络数据(每 30s 或积满 50 条)
- P2 延迟上报:用户行为、自定义事件(App 切后台或下次启动)
# 3.4 无侵入式Hook原理
Android Hook 技术矩阵:
| 技术 | 原理 | 适用场景 |
|---|---|---|
| ASM + Gradle Transform | 编译期修改字节码 | 方法耗时、启动追踪 |
| 动态代理 / Proxy | 运行时代理接口 | 网络拦截、数据库操作 |
| PLT Hook / Inline Hook | Native 层函数替换 | IO监控、信号捕获、malloc |
| ActivityLifecycleCallbacks | 系统回调注册 | 页面生命周期监控 |
iOS Hook 技术矩阵:
| 技术 | 原理 | 适用场景 |
|---|---|---|
| Method Swizzling | OC runtime 方法交换 | VC 生命周期、网络请求 |
| fishhook | 重绑定 Mach-O 懒加载符号 | malloc/free、C 函数 |
| Mach 异常 | mach_exc_server | 崩溃捕获、EXC_BAD_ACCESS |
| CFRunLoopObserver | RunLoop 状态观察 | 卡顿检测、主线程监控 |
Web Hook 技术矩阵:
| 技术 | 原理 | 适用场景 |
|---|---|---|
| Monkey Patch | 重写原生原型方法 | fetch / XHR 拦截 |
| ES6 Proxy | 包装对象拦截操作 | localStorage、对象访问 |
| PerformanceObserver | 浏览器性能观察 | LCP/FCP/CLS/Long Task |
| Error Event | 全局错误监听 | JS异常、Promise Rejection |
# 3.5 采样与降级策略
APM 不能"全量全时"采集,否则自身就会成为性能瓶颈:
{
"performance": { "enabled": true, "sampleRate": 0.3 },
"crash": { "enabled": true, "sampleRate": 1.0 },
"network": { "enabled": true, "sampleRate": 0.5, "slowThreshold": 3000 },
"memory": { "enabled": true, "sampleRate": 0.1, "oomSampleRate": 1.0 }
}
2
3
4
5
6
降级规则:
- 设备 CPU > 80% 时,自动关闭非必要采集
- 内存紧张时,清空缓冲区,仅保留崩溃采集
- 蜂窝网络时,降低上报频率,增大批量阈值
- 灰度阶段全量采集,正式发布按采样率执行
▶▶ 回扣 §00.5 案例:经验派第 1 周"所有采样率降到 10%"完全违反本节"崩溃 100%、非必要可降"的核心原则——导致崩溃数据丢 90%。采样不是一刀切,是分类分级。方法派 Day 5 按"崩溃 100% / ANR 100% / 慢请求 100% / 性能 10% / 行为 5%"分类采样,是本节规则的真实落地。
# 04.性能监控设计
# 4.1 启动耗时监控
启动阶段划分:
T0: 进程创建 → T1: Application/AppDelegate初始化 → T2: 首页创建 → T3: 首帧渲染完成 → T4: 数据加载完成(用户可交互)
冷启动耗时 = T3 - T0 ,可交互耗时 = T4 - T0
2
3
各端采集方案:
| 阶段 | Android | iOS | Web |
|---|---|---|---|
| 进程创建 | Process.getStartElapsedRealtime() | sysctl + kinfo_proc | navigationStart |
| 初始化完成 | Application.onCreate 结束 | didFinishLaunchingWithOptions 结束 | DOMContentLoaded |
| 首帧渲染 | Choreographer.postFrameCallback | viewDidAppear 首次回调 | LCP |
| 可交互 | IdleHandler 回调 | RunLoop 进入 BeforeWaiting | TTI |
iOS 冷启动分两个阶段:
- pre-main:dylib 加载 → rebase/binding → Objc setup → initializer(+load)
- post-main:main() → UIApplicationMain → didFinishLaunching → 首帧=
# 4.2 页面加载监控
核心设计原理:页面加载监控的本质是在生命周期关键节点埋入时间戳,构建完整的页面渲染时间线。难点在于"页面真正可见"与"生命周期回调"之间存在时差——系统回调只表示逻辑就绪,真正的像素上屏还需要经过 measure → layout → draw → GPU 合成 → VSync 上屏。
时间线对比(Android Activity 从 intent 到用户可见):
startActivity()
│── AMS 调度 ──→ 创建进程/绑定Application(冷启动独有)
│── onCreate() ─── setContentView → inflate XML → 创建 View 树
│── onStart() ─── Activity 进入前台
│── onResume() ─── Activity 可交互(但此时还未绘制!)
│── ViewTree.OnPreDrawListener ─── measure/layout 完成,即将 draw
│── Choreographer.FrameCallback ─── 首帧 VSync 到来
│── DecorView.post → IdleHandler ─── 真正可交互(TTI)
▼
用户看到内容
2
3
4
5
6
7
8
9
10
11
12
Android 深度方案:
- Hook 方式:通过
Application.registerActivityLifecycleCallbacks全局注册,无需侵入业务代码 - 真实渲染完成检测:
onResume并不代表渲染完成,需要在ViewTreeObserver.OnPreDrawListener回调中记录首帧时间 - 分段耗时拆解:将页面加载拆分为
create耗时 | 数据加载耗时 | 渲染耗时三段,精确定位瓶颈 - 自定义终点:对于列表页等异步加载场景,提供
reportPageLoadEnd()API 让业务主动上报数据到达+渲染完成的真实终点
// Android 页面加载耗时监控核心实现
public class PageLoadTracker implements Application.ActivityLifecycleCallbacks {
private final Map<String, Long> createTimeMap = new ConcurrentHashMap<>();
@Override
public void onActivityCreated(Activity activity, Bundle savedState) {
String key = activity.getClass().getSimpleName() + "@" + activity.hashCode();
createTimeMap.put(key, SystemClock.elapsedRealtime());
}
@Override
public void onActivityResumed(Activity activity) {
String key = activity.getClass().getSimpleName() + "@" + activity.hashCode();
Long createTime = createTimeMap.get(key);
if (createTime == null) return;
// 监听真实首帧绘制完成
activity.getWindow().getDecorView().getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
activity.getWindow().getDecorView().getViewTreeObserver()
.removeOnPreDrawListener(this);
long firstFrameTime = SystemClock.elapsedRealtime() - createTime;
report(key, "first_frame", firstFrameTime);
createTimeMap.remove(key);
return true;
}
});
}
}
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
29
30
31
iOS 深度方案:
- Method Swizzling:在
+load中交换viewDidLoad / viewWillAppear / viewDidAppear的 IMP,全局无侵入 - 精确渲染完成:
viewDidAppear之后追加CATransaction.setCompletionBlock来检测 Core Animation 提交完成 - 页面耗时 =
viewDidAppear timestamp - viewDidLoad timestamp(不含动画过渡时间)
Web SPA 深度方案:
- 路由拦截:Monkey Patch
history.pushState / replaceState,监听popstate事件 - 渲染完成检测:使用
MutationObserver监听 DOM 变化,当 DOM 稳定(300ms无新变化)后,结合requestAnimationFrame确认绘制完成 - LCP 优化:通过
PerformanceObserver监听largest-contentful-paint,获取用户感知的真实加载时间
# 4.3 FPS帧率监控
流畅标准通常是 60 FPS(每帧 16.67ms),高刷设备 90/120 FPS。
Android:Choreographer 注册 FrameCallback,VSync 信号到来时回调,计算帧间隔。
iOS:CADisplayLink 与屏幕刷新率同步触发回调,记录 timestamp 差值计算 FPS。
Web:requestAnimationFrame 循环统计每秒回调次数。
帧间隔 > 16.67ms 即为掉帧,连续掉帧 > 3 帧即为卡顿。
# 4.4 CPU使用率监控
核心原理 — 为什么要监控 CPU 使用率:
CPU 使用率是电量消耗的直接因素。Android 系统中,CPU 每提升 10% 的使用率,电量消耗约增加 5-8%。持续高 CPU(>30%)不仅耗电,还会导致 CPU 降频(thermal throttling),进而引发卡顿和发热。因此 CPU 监控是 APM 体系中连接"性能"与"功耗"的关键桥梁。
CPU 使用率的计算原理(Linux/Android):
/proc/[pid]/stat 文件内容(第14-17个字段):
... utime stime cutime cstime ...
│ │ │ │
│ │ │ └── 子进程内核态时间
│ │ └──── 子进程用户态时间
│ └─────── 进程内核态CPU时间(系统调用)
└────────── 进程用户态CPU时间(业务逻辑)
CPU使用率 = (Δutime + Δstime) / (Δtotal_cpu_time) × CPU核心数 × 100%
其中 total_cpu_time 来自 /proc/stat 的第一行
2
3
4
5
6
7
8
9
10
11
12
13
| 平台 | 获取方式 | 精度 | 开销 |
|---|---|---|---|
| Android | 读取 /proc/[pid]/stat 的 utime + stime | 进程级 | 极低,读文件即可 |
| Android | 读取 /proc/[pid]/task/[tid]/stat | 线程级 | 中等,需遍历所有线程 |
| iOS | thread_info 遍历所有线程累加 cpu_usage | 线程级 | 中等 |
| Web | Long Tasks API 间接反映(无直接 CPU API) | 任务级 | 低 |
Android 线程级 CPU 监控实现:
// 获取当前进程所有线程的CPU使用情况
public Map<String, Double> getThreadCpuUsage() {
Map<String, Double> result = new HashMap<>();
File taskDir = new File("/proc/" + Process.myPid() + "/task");
File[] threads = taskDir.listFiles();
if (threads == null) return result;
for (File threadDir : threads) {
try {
String stat = readFile(threadDir.getPath() + "/stat");
String[] fields = stat.split("\\s+");
String threadName = fields[1].replace("(", "").replace(")", "");
long utime = Long.parseLong(fields[13]);
long stime = Long.parseLong(fields[14]);
// 与上次采样做差值,除以采样间隔得到CPU使用率
double cpuPercent = calculateDelta(threadDir.getName(), utime + stime);
if (cpuPercent > 0) {
result.put(threadName, cpuPercent);
}
} catch (Exception ignored) {}
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
iOS 线程级 CPU 监控实现:
+ (NSDictionary *)threadCpuUsage {
thread_act_array_t threads;
mach_msg_type_number_t threadCount;
kern_return_t kr = task_threads(mach_task_self(), &threads, &threadCount);
if (kr != KERN_SUCCESS) return nil;
NSMutableDictionary *result = [NSMutableDictionary new];
for (int i = 0; i < threadCount; i++) {
thread_info_data_t info;
mach_msg_type_number_t infoCount = THREAD_INFO_MAX;
thread_info(threads[i], THREAD_BASIC_INFO, (thread_info_t)info, &infoCount);
thread_basic_info_t basicInfo = (thread_basic_info_t)info;
if (!(basicInfo->flags & TH_FLAGS_IDLE)) {
CGFloat usage = basicInfo->cpu_usage / (CGFloat)TH_USAGE_SCALE * 100.0;
result[@(i)] = @(usage);
}
}
vm_deallocate(mach_task_self(), (vm_address_t)threads, threadCount * sizeof(thread_t));
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
监控策略设计:
- 采样频率:正常 5s 一次,检测到异常后切换为 1s 一次
- 异常阈值:单线程持续 >80% 超过 10s 触发告警,全进程 >30% 持续 30s 触发告警
- 降级策略:CPU 过高时主动降低动画帧率、暂停预加载、关闭后台任务
# 4.5 性能监控伪代码
Android 启动耗时 + FPS 采集:
public class PerformanceMonitor {
private static long sProcessStartTime;
// 在 ContentProvider 中尽早调用
public static void markProcessStart() {
sProcessStartTime = (Build.VERSION.SDK_INT >= 24)
? Process.getStartElapsedRealtime()
: SystemClock.elapsedRealtime();
}
// 首页 Activity 中调用
public static void markFirstFrame(Activity activity) {
Choreographer.getInstance().postFrameCallback(frameTimeNanos -> {
long coldStartCost = SystemClock.elapsedRealtime() - sProcessStartTime;
DataCollector.report("startup_cold_total", coldStartCost);
});
// 可交互时间点
activity.getWindow().getDecorView().post(() -> {
Looper.myQueue().addIdleHandler(() -> {
long tti = SystemClock.elapsedRealtime() - sProcessStartTime;
DataCollector.report("startup_tti", tti);
return false;
});
});
}
// FPS 监控:基于 Choreographer
private int mFrameCount = 0;
private long mLastCalcTime = 0;
private final Choreographer.FrameCallback mFrameCallback = frameTimeNanos -> {
mFrameCount++;
long now = System.nanoTime();
long elapsed = now - mLastCalcTime;
if (elapsed >= 1_000_000_000L) {
float fps = mFrameCount * 1_000_000_000f / elapsed;
mFrameCount = 0;
mLastCalcTime = now;
DataCollector.report("fps", Math.round(fps));
if (fps < 45) {
String stack = Looper.getMainLooper().getThread().getStackTrace().toString();
DataCollector.report("jank_stack", stack);
}
}
Choreographer.getInstance().postFrameCallback(this.mFrameCallback);
};
}
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
iOS 启动耗时 + FPS 采集:
@implementation APMPerformanceMonitor
// 通过 sysctl 获取进程创建时间
+ (void)markLaunchStart {
struct kinfo_proc info;
size_t size = sizeof(info);
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
sysctl(mib, 4, &info, &size, NULL, 0);
_processStartTime = info.kp_proc.p_starttime.tv_sec * 1000
+ info.kp_proc.p_starttime.tv_usec / 1000;
}
// FPS 监控:基于 CADisplayLink
- (void)startFPSMonitor {
_displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(tick:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)tick:(CADisplayLink *)link {
_frameCount++;
NSTimeInterval elapsed = link.timestamp - _lastTimestamp;
if (elapsed >= 1.0) {
float fps = _frameCount / elapsed;
_frameCount = 0;
_lastTimestamp = link.timestamp;
[APMDataCollector report:@"fps" value:@(roundf(fps))];
}
}
@end
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
29
30
31
Web 核心 Web Vitals 采集:
class WebPerformanceMonitor {
init() {
// LCP — 最大内容绘制
new PerformanceObserver((list) => {
const last = list.getEntries().pop();
DataCollector.report('lcp', { value: last.startTime });
}).observe({ type: 'largest-contentful-paint', buffered: true });
// CLS — 累积布局偏移
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) clsValue += entry.value;
}
DataCollector.report('cls', clsValue);
}).observe({ type: 'layout-shift', buffered: true });
// Long Tasks — 长任务检测
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
DataCollector.report('long_task', { duration: entry.duration });
}
}).observe({ type: 'longtask', buffered: true });
// Navigation Timing
window.addEventListener('load', () => {
setTimeout(() => {
const t = performance.getEntriesByType('navigation')[0];
DataCollector.report('navigation', {
dns: t.domainLookupEnd - t.domainLookupStart,
tcp: t.connectEnd - t.connectStart,
ttfb: t.responseStart - t.requestStart,
domReady: t.domContentLoadedEventEnd - t.navigationStart,
load: t.loadEventEnd - t.navigationStart
});
}, 0);
});
}
}
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
29
30
31
32
33
34
35
36
37
38
39
# 05.内存分析监控设计
# 5.1 内存泄漏检测原理
内存泄漏的本质:对象的生命周期已结束(逻辑上不再需要),但由于被其他对象持有引用,GC 无法回收,导致内存持续增长。
各端检测思路:
共同原理:判断"应该被回收的对象"是否"确实被回收了"
Android:Activity/Fragment destroy 后,
通过 WeakReference + GC 触发检测是否回收
代表方案:LeakCanary
iOS: UIViewController dismiss/pop 后,
延迟检测对象是否仍在内存中
代表方案:MLeaksFinder + FBRetainCycleDetector
Web: 组件卸载后,通过 WeakRef + FinalizationRegistry 检测
辅助方案:Chrome DevTools Heap Snapshot 对比
2
3
4
5
6
7
8
9
10
11
12
# 5.2 Android内存监控方案
泄漏检测(LeakCanary 原理):
Activity.onDestroy()
↓
创建 WeakReference(activity) + 关联 ReferenceQueue
↓ (延迟 5 秒)
检查 ReferenceQueue → 未收到 → 手动 GC → 再次检查
↓ 仍未收到
确认泄漏 → dump hprof → 分析引用链
2
3
4
5
6
7
内存水位监控:
| 指标 | 获取方式 | 告警阈值 |
|---|---|---|
| Java Heap | Runtime.totalMemory() - freeMemory() | > maxMemory × 85% |
| Native Heap | Debug.getNativeHeapAllocatedSize() | > 300MB |
| FD 数量 | 读取 /proc/self/fd | > 800 |
| 线程数 | 读取 /proc/self/status | > 500 |
# 5.3 iOS内存监控方案
VC 泄漏检测(MLeaksFinder 原理):VC 被 pop/dismiss 后延迟 2 秒,调用 willDealloc 检测对象是否仍存在。
OOM 监控(排除法):iOS 的 OOM 被 Jetsam 杀死时无崩溃日志。
App 上次退出时,排除以下情况后判定为 OOM:
× 用户主动杀死(无 applicationWillTerminate)
× 崩溃(无 crash log)
× 系统升级(版本未变)
× Watchdog 超时
→ 排除以上 → 判定为 OOM
2
3
4
5
6
内存水位:使用 task_info 获取 phys_footprint(物理内存占用)。
# 5.4 Web内存监控方案
// Chrome: performance.memory
const { usedJSHeapSize, totalJSHeapSize, jsHeapSizeLimit } = performance.memory;
// 标准 API(需要跨域隔离环境)
const result = await performance.measureUserAgentSpecificMemory();
// 组件泄漏检测:WeakRef + FinalizationRegistry
const leakRegistry = new FinalizationRegistry((name) => {
pendingChecks.delete(name); // 已回收,正常
});
function trackComponent(component, name) {
leakRegistry.register(component, name);
pendingChecks.set(name, { ref: new WeakRef(component), time: Date.now() });
}
// 定期检查:超过 30 秒仍未回收 → 可疑泄漏
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5.5 OOM防护与兜底
内存水位检测 → 达到警戒线(80%)
Level 1 — 通知业务层释放缓存
Level 2 — 强制清理图片缓存、释放 WebView
Level 3 — 记录现场快照(内存分布、页面栈、线程信息)
Level 4 — 安全重启 / 降级到轻量模式
2
3
4
5
# 5.6 内存监控伪代码
Android 泄漏检测核心伪代码:
public class LeakDetector {
private final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
private final Map<String, KeyedWeakReference> watchedRefs = new ConcurrentHashMap<>();
// 在 Activity.onDestroy 后自动调用
public void watch(Object object, String description) {
String key = UUID.randomUUID().toString();
KeyedWeakReference ref = new KeyedWeakReference(object, key, description, refQueue);
watchedRefs.put(key, ref);
executor.schedule(() -> checkRetained(key), 5, TimeUnit.SECONDS);
}
private void checkRetained(String key) {
removeCollectedReferences(); // 清理已回收的引用
if (watchedRefs.containsKey(key)) {
Runtime.getRuntime().gc(); // 手动触发 GC
Thread.sleep(100);
removeCollectedReferences();
if (watchedRefs.containsKey(key)) {
onLeakDetected(watchedRefs.remove(key)); // 确认泄漏
}
}
}
// 无侵入:注册 ActivityLifecycleCallbacks
public void install(Application app) {
app.registerActivityLifecycleCallbacks(new SimpleCallbacks() {
@Override
public void onActivityDestroyed(Activity activity) {
watch(activity, activity.getClass().getName());
}
});
}
}
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
29
30
31
32
33
34
Android 内存水位监控伪代码:
public class MemoryWatermarkMonitor {
private static final float WARN = 0.80f, DANGER = 0.90f;
public void start() {
scheduler.scheduleAtFixedRate(() -> {
Runtime rt = Runtime.getRuntime();
float ratio = (float)(rt.totalMemory() - rt.freeMemory()) / rt.maxMemory();
long nativeHeap = Debug.getNativeHeapAllocatedSize();
int fdCount = new File("/proc/self/fd").listFiles().length;
DataCollector.report("memory_watermark", new Snapshot(ratio, nativeHeap, fdCount));
if (ratio > DANGER) {
ImageLoader.clearMemoryCache();
DataCollector.report("memory_danger", captureScene());
} else if (ratio > WARN) {
EventBus.post(new MemoryWarningEvent());
}
}, 0, 10, TimeUnit.SECONDS);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
iOS 内存监控伪代码:
@implementation APMMemoryMonitor
- (void)collectMemoryInfo {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);
int64_t physFootprint = vmInfo.phys_footprint;
[APMDataCollector report:@"memory_watermark" value:@(physFootprint)];
}
// VC 泄漏检测(Swizzle viewDidDisappear:)
- (void)apm_viewDidDisappear:(BOOL)animated {
[self apm_viewDidDisappear:animated]; // 调用原方法
if (self.isBeingDismissed || self.isMovingFromParentViewController) {
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC),
dispatch_get_main_queue(), ^{
if (weakSelf) { // 3 秒后仍存在 → 泄漏
[APMDataCollector report:@"vc_leak"
value:NSStringFromClass([weakSelf class])];
}
});
}
}
@end
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
# 06.渲染性能监控设计
# 6.1 渲染管线原理
Android: Measure → Layout → Draw → Sync → GPU Render → Display
│←── CPU (主线程) ──→│←RenderThread→│← GPU →│
iOS: Layout → Display → Prepare → Commit → GPU Render → Display
│←────── CPU (主线程) ──────→│ │←── GPU ──→│
Web: JS → Style → Layout → Paint → Composite → Display
│←──────── 主线程 ──────────→│←合成线程→│
2
3
4
5
6
7
8
关键概念:掉帧(帧处理超过 VSync 周期 16.67ms)、卡顿(连续多帧掉帧)、Jank(帧时间不稳定)。
# 6.2 卡顿检测方案
Android — Looper Printer 方案:
主线程 Looper.loop() 中每次 dispatch Message 前后会调用 Printer.println():
>>>>> Dispatching to Handler ... msg ...
[执行 Message 处理逻辑]
<<<<< Finished to Handler ... msg ...
2
3
设置自定义 Printer,计算两次 print 之间的时间差检测卡顿。同时启动子线程看门狗,超时后抓取主线程堆栈。
iOS — RunLoop Observer 方案:
通过观察 RunLoop 状态切换,检测在 kCFRunLoopBeforeSources 或 kCFRunLoopAfterWaiting 状态停留超过阈值(如 200ms)→ 判定卡顿。
Web — Long Task + rAF 方案:
Long Tasks API 自动标记 > 50ms 的任务;rAF 帧间隔 > 50ms 视为掉帧。
# 6.3 布局层级监控
核心原理 — 为什么布局层级影响性能:
View 树的渲染遵循深度优先遍历的 measure → layout → draw 流程。每增加一层嵌套,渲染的复杂度理论上呈线性增长,但实际上由于 RelativeLayout、LinearLayout(带weight) 会触发二次测量,嵌套后复杂度可能呈指数级增长:
二次测量的影响(LinearLayout + weight 嵌套):
层级深度 测量次数 实际开销
1 2 2
2 2×2 = 4 4
3 2×2×2 = 8 8
N 2^N 指数增长!
示例:3层 LinearLayout(weight) 嵌套 = 8次测量
5层 = 32次测量 → 严重影响帧率
2
3
4
5
6
7
8
9
10
| 平台 | 监控方式 | 建议阈值 | 检测时机 |
|---|---|---|---|
| Android | 遍历 View 树计算深度 | 层级 > 10 告警 | Activity.onResume 后首帧回调 |
| Android | 检测 requestLayout 调用频率 | 单帧 >5次 告警 | Choreographer 回调中统计 |
| iOS | 递归遍历 view.subviews | 层级 > 10 告警 | viewDidAppear 后 |
| Web | 递归计算 DOM 深度 | 节点 > 1500 告警 | MutationObserver 回调中 |
Android 布局层级检测实现:
public static int getViewDepth(View root) {
if (!(root instanceof ViewGroup)) return 1;
ViewGroup group = (ViewGroup) root;
int maxChildDepth = 0;
for (int i = 0; i < group.getChildCount(); i++) {
maxChildDepth = Math.max(maxChildDepth, getViewDepth(group.getChildAt(i)));
}
return maxChildDepth + 1;
}
// 过度绘制检测:统计同一区域被绘制的次数
public static int getOverdrawCount(View root) {
// 递归统计 background 不为 null 的 View 重叠层数
// 1x = 正常,2x = 轻度过度绘制,3x = 中度,4x+ = 严重
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
优化方向:
- 使用
ConstraintLayout替代多层嵌套,一层布局即可表达复杂约束关系 - 使用
merge标签减少 include 引入的多余层级 - 使用
ViewStub延迟加载低频展示的布局 - Android Studio 的 Layout Inspector 可视化查看层级,Lint 规则检测超过阈值的嵌套
# 6.4 渲染监控伪代码
Android 卡顿检测:
public class JankDetector {
private static final long THRESHOLD = 200; // 200ms
private volatile long mStartTime;
private final Handler mWatchdog;
public void start() {
Looper.getMainLooper().setMessageLogging(msg -> {
if (msg.startsWith(">>>>>")) {
mStartTime = SystemClock.elapsedRealtime();
mWatchdog.postDelayed(mStackCapture, THRESHOLD / 2);
} else if (msg.startsWith("<<<<<")) {
mWatchdog.removeCallbacks(mStackCapture);
long cost = SystemClock.elapsedRealtime() - mStartTime;
if (cost > THRESHOLD) {
DataCollector.report("jank", new JankInfo(cost, mCapturedStack));
}
}
});
}
private final Runnable mStackCapture = () -> {
Thread mainThread = Looper.getMainLooper().getThread();
mCapturedStack = formatStack(mainThread.getStackTrace());
mWatchdog.postDelayed(mStackCapture, 50); // 多次采样
};
}
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
iOS 卡顿检测:
@implementation APMJankDetector {
dispatch_semaphore_t _semaphore;
CFRunLoopActivity _activity;
}
- (void)start {
_semaphore = dispatch_semaphore_create(0);
// 注册 RunLoop Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
NULL, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef obs, CFRunLoopActivity act) {
self->_activity = act;
dispatch_semaphore_signal(self->_semaphore);
});
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 子线程循环检测
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) {
long result = dispatch_semaphore_wait(self->_semaphore,
dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC));
if (result != 0) { // 超时
if (self->_activity == kCFRunLoopBeforeSources ||
self->_activity == kCFRunLoopAfterWaiting) {
NSString *stack = [APMStackCapture captureMainThread];
[APMDataCollector report:@"jank" value:stack];
}
}
}
});
}
@end
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
29
30
31
32
33
Web 卡顿检测:
class WebJankDetector {
start() {
// Long Tasks API
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 200) {
DataCollector.report('jank', { duration: entry.duration });
}
}
}).observe({ type: 'longtask', buffered: true });
// rAF 帧间隔检测
let lastTime = performance.now();
const tick = (now) => {
const gap = now - lastTime;
lastTime = now;
if (gap > 50) { // 掉 3 帧以上
DataCollector.report('dropped_frame', {
gap: Math.round(gap),
dropped: Math.floor(gap / 16.67) - 1
});
}
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
}
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
# 07.网络性能监控设计
# 7.1 网络监控指标
| 指标 | 说明 | 统计维度 |
|---|---|---|
| 请求成功率 | 非 5xx / 超时的占比 | 接口、版本、地域 |
| 平均延时 | 请求到响应时间 | P50/P90/P99 |
| DNS 耗时 | 域名解析耗时 | 运营商 |
| TTFB | 首字节时间 | 接口维度 |
| 错误率 | 超时/连接失败/SSL错误 | 错误类型 |
# 7.2 各端拦截方案
核心设计原理 — 无侵入网络监控:
网络监控的最大挑战是如何在不修改业务代码的前提下拦截所有网络请求。各平台提供了不同层级的拦截点,选择合适的拦截层级至关重要:
拦截层级与覆盖范围:
应用层拦截 系统层拦截
(高层、方便) (底层、全面)
│ │
Android: OkHttp Interceptor ←→ AspectJ Hook URLConnection
│ │
覆盖: 仅OkHttp请求 所有Java层网络请求
优点: API标准,性能好 覆盖全面
缺点: 不覆盖其他网络库 兼容性风险,性能开销
iOS: NSURLProtocol ←→ fishhook/Method Swizzle
│ │
覆盖: NSURLSession请求 包括CFNetwork层
优点: 官方API,稳定 覆盖底层C函数
缺点: 不拦截WKWebView 需越过系统安全限制
Web: fetch/XHR MonkeyPatch ←→ Service Worker
│ │
覆盖: 主线程网络请求 所有fetch/导航请求
优点: 实现简单 天然代理层
缺点: 可能被第三方库覆盖 需要HTTPS+注册
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Android — OkHttp Interceptor 深度实现:
通过标准拦截器机制,在 chain.proceed() 前后记录各阶段耗时。OkHttp 的 EventListener 提供更细粒度的事件回调:
// 方案一:Interceptor(简单,获取总耗时)
public class NetworkMonitorInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long startNs = System.nanoTime();
try {
Response response = chain.proceed(request);
long costMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
reportSuccess(request.url().toString(), response.code(), costMs,
response.body() != null ? response.body().contentLength() : 0);
return response;
} catch (IOException e) {
long costMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
reportError(request.url().toString(), e.getClass().getSimpleName(), costMs);
throw e;
}
}
}
// 方案二:EventListener(高级,获取各阶段耗时)
public class NetworkEventListener extends EventListener {
private long dnsStart, connectStart, tlsStart, requestStart;
@Override public void dnsStart(Call call, String domainName) { dnsStart = now(); }
@Override public void dnsEnd(Call call, String domainName, List<InetAddress> addrs) {
report("dns", now() - dnsStart);
}
@Override public void connectStart(Call call, InetSocketAddress addr, Proxy proxy) { connectStart = now(); }
@Override public void secureConnectStart(Call call) { tlsStart = now(); }
@Override public void secureConnectEnd(Call call, Handshake handshake) {
report("tls", now() - tlsStart);
}
@Override public void connectEnd(Call call, InetSocketAddress addr, Proxy proxy, Protocol protocol) {
report("tcp", now() - connectStart);
}
// ... 更多回调
}
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
29
30
31
32
33
34
35
36
37
38
iOS — NSURLProtocol + NSURLSessionTaskMetrics 深度实现:
自定义 NSURLProtocol 拦截请求,通过 didFinishCollecting:metrics 获取 DNS/TCP/TLS/TTFB 各阶段耗时。iOS 10+ 的 NSURLSessionTaskMetrics 提供了极为详细的分阶段数据:
// 注册自定义 Protocol
[NSURLProtocol registerClass:[NetworkMonitorProtocol class]];
// 通过 TaskMetrics 获取各阶段耗时
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
for (NSURLSessionTaskTransactionMetrics *m in metrics.transactionMetrics) {
NSTimeInterval dns = [m.domainLookupEndDate timeIntervalSinceDate:m.domainLookupStartDate];
NSTimeInterval tcp = [m.connectEndDate timeIntervalSinceDate:m.connectStartDate];
NSTimeInterval tls = [m.secureConnectionEndDate timeIntervalSinceDate:m.secureConnectionStartDate];
NSTimeInterval ttfb = [m.responseStartDate timeIntervalSinceDate:m.requestStartDate];
// 上报各阶段耗时
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Web — fetch/XHR Monkey Patch + Resource Timing API:
重写 window.fetch 和 XMLHttpRequest.prototype,结合 PerformanceObserver 监听 resource 条目获取详细阶段耗时:
// 拦截 fetch
const originalFetch = window.fetch;
window.fetch = async function(input, init) {
const url = typeof input === 'string' ? input : input.url;
const startTime = performance.now();
try {
const response = await originalFetch.call(this, input, init);
reportNetworkMetric(url, response.status, performance.now() - startTime);
return response;
} catch (error) {
reportNetworkError(url, error.message, performance.now() - startTime);
throw error;
}
};
// Resource Timing API 获取精确阶段耗时
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const dns = entry.domainLookupEnd - entry.domainLookupStart;
const tcp = entry.connectEnd - entry.connectStart;
const ttfb = entry.responseStart - entry.requestStart;
}
}).observe({ type: 'resource', buffered: true });
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 7.3 网络监控伪代码
Android 网络拦截器:
public class APMNetworkInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
NetworkMetric metric = new NetworkMetric();
metric.url = request.url().toString();
metric.method = request.method();
metric.startTime = SystemClock.elapsedRealtime();
try {
Response response = chain.proceed(request);
metric.duration = SystemClock.elapsedRealtime() - metric.startTime;
metric.statusCode = response.code();
metric.success = response.isSuccessful();
DataCollector.report("network", metric);
return response;
} catch (IOException e) {
metric.duration = SystemClock.elapsedRealtime() - metric.startTime;
metric.success = false;
metric.errorType = classifyError(e);
DataCollector.report("network_error", metric);
throw e;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Web 网络拦截:
class WebNetworkMonitor {
hookFetch() {
const originalFetch = window.fetch;
window.fetch = async (input, init) => {
const url = typeof input === 'string' ? input : input.url;
const start = performance.now();
try {
const response = await originalFetch(input, init);
DataCollector.report('network', {
url, duration: Math.round(performance.now() - start),
status: response.status, success: response.ok
});
return response;
} catch (error) {
DataCollector.report('network_error', {
url, duration: Math.round(performance.now() - start),
error: error.message
});
throw error;
}
};
}
observeResourceTiming() {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
DataCollector.report('network_timing', {
url: entry.name,
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart
});
}
}
}).observe({ type: 'resource', buffered: true });
}
}
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
29
30
31
32
33
34
35
36
37
38
39
# 08.崩溃与错误监控设计
# 8.1 崩溃类型与捕获
| 平台 | 崩溃类型 | 捕获方式 |
|---|---|---|
| Android | Java 异常 | Thread.setUncaughtExceptionHandler |
| Android | Native 崩溃 | sigaction 注册信号处理器(SIGSEGV/SIGABRT) |
| Android | ANR | 监控 /data/anr/traces.txt 或 Looper 超时 |
| iOS | OC/Swift 异常 | NSSetUncaughtExceptionHandler |
| iOS | Mach 异常 | mach_exc_server 注册 Mach 异常端口 |
| iOS | Unix 信号 | sigaction(SIGABRT/SIGSEGV/SIGBUS) |
| Web | JS 运行时错误 | window.onerror |
| Web | Promise 未处理 | unhandledrejection 事件 |
| Web | 资源加载失败 | window.addEventListener('error', ..., true) |
# 8.2 各端崩溃方案
Android 崩溃捕获要点:
- Java 层:设置全局
UncaughtExceptionHandler,收集线程名、堆栈、设备信息后写入本地文件 - Native 层:通过
sigaction注册信号处理器,在信号处理函数中使用unw_backtrace收集 Native 堆栈 - ANR:在子线程定期检测主线程 MessageQueue 是否阻塞
iOS 崩溃捕获要点:
- OC 异常:
NSSetUncaughtExceptionHandler获取NSException的 name/reason/callStackSymbols - Mach 异常:注册 Mach 异常端口(优先级高于 Unix 信号),可捕获 EXC_BAD_ACCESS 等
- 关键:崩溃处理函数中只能使用异步信号安全(async-signal-safe)的函数
Web 错误捕获要点:
window.onerror可获取文件名、行号、列号、Error 对象- 跨域脚本需要设置
crossorigin属性和 CORS 响应头 - SourceMap 反解:上报时记录行列号,后端通过 SourceMap 还原源码位置
# 8.3 崩溃监控伪代码
Android 崩溃捕获:
public class CrashMonitor implements Thread.UncaughtExceptionHandler {
private Thread.UncaughtExceptionHandler mDefault;
public void install() {
mDefault = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
CrashInfo info = new CrashInfo();
info.threadName = thread.getName();
info.stackTrace = Log.getStackTraceString(ex);
info.timestamp = System.currentTimeMillis();
info.deviceInfo = DeviceInfoCollector.collect();
info.memoryInfo = MemoryInfoCollector.collect();
info.currentPage = ActivityTracker.getCurrentPage();
// 写入本地文件(不走网络,避免崩溃时网络不可用)
CrashFileWriter.write(info);
// 交给默认处理器(弹出崩溃对话框)
if (mDefault != null) mDefault.uncaughtException(thread, ex);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Web 错误捕获:
class WebErrorMonitor {
init() {
// JS 运行时错误
window.onerror = (msg, url, line, col, error) => {
DataCollector.report('js_error', {
message: msg, url, line, col,
stack: error?.stack,
page: location.pathname
});
};
// Promise 未处理的 rejection
window.addEventListener('unhandledrejection', (event) => {
DataCollector.report('promise_error', {
reason: event.reason?.message || String(event.reason),
stack: event.reason?.stack
});
});
// 资源加载失败(捕获阶段)
window.addEventListener('error', (event) => {
if (event.target && (event.target.src || event.target.href)) {
DataCollector.report('resource_error', {
tag: event.target.tagName,
url: event.target.src || event.target.href
});
}
}, true);
}
}
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
# 09.用户行为与事件追踪设计
# 9.1 无埋点方案原理
**无埋点(全埋点)**的核心思想:自动采集所有用户交互事件,无需业务开发者手动埋点。
| 平台 | 实现方式 | 采集事件 |
|---|---|---|
| Android | ASM 插桩 View.OnClickListener / AccessibilityDelegate | 点击、长按、滑动 |
| iOS | Swizzle UIControl.sendAction:to:forEvent: | 点击、手势 |
| Web | 全局 addEventListener('click', ..., true) 捕获阶段 | 点击、输入 |
页面路径追踪:记录用户浏览的页面序列,构建"用户行为路径图"。
Android: ActivityLifecycleCallbacks → 记录页面栈变化
iOS: Swizzle viewDidAppear → 记录 VC 出现
Web: Hook pushState/popstate → 记录路由变化
2
3
# 9.2 事件追踪伪代码
// Android 无埋点:ASM 插桩 onClick 方法
// 编译期在所有 View.OnClickListener.onClick 中插入追踪代码
public class AutoTrackHelper {
public static void trackViewClick(View view) {
EventData event = new EventData();
event.type = "click";
event.viewId = getViewId(view);
event.viewText = getViewText(view);
event.page = ActivityTracker.getCurrentPage();
event.timestamp = System.currentTimeMillis();
DataCollector.report("auto_track", event);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
// Web 无埋点
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-track], button, a, input');
if (!target) return;
DataCollector.report('auto_click', {
tag: target.tagName,
text: target.innerText?.slice(0, 50),
path: getDomPath(target), // 如 "div#app > main > button.submit"
page: location.pathname
});
}, true);
2
3
4
5
6
7
8
9
10
11
# 10.数据上报与聚合
# 10.1 上报策略设计
| 策略 | 触发条件 | 适用数据 |
|---|---|---|
| 即时上报 | 事件发生即上报 | 崩溃、ANR、OOM |
| 批量上报 | 数据积满 N 条或定时 T 秒 | 性能指标、网络数据 |
| 延迟上报 | App 切后台 / 下次启动 | 行为数据、低优先级 |
| 条件上报 | WiFi 环境、充电状态 | 大体积日志、hprof |
上报可靠性保障:
- 本地持久化(SQLite/MMKV)→ 上报成功后删除
- 失败重试(指数退避:1s → 2s → 4s → 8s → 最大 60s)
- 数据压缩(gzip/protobuf 减少传输量)
- 去重机制(基于数据指纹去重,避免重复上报)
# 10.2 上报通道伪代码
public class DataReporter {
private final BlockingQueue<ReportData> queue = new LinkedBlockingQueue<>(1000);
private final SQLiteDatabase db; // 持久化兜底
// 写入数据
public void report(String type, Object data) {
ReportData item = new ReportData(type, data, System.currentTimeMillis());
if (isPriority0(type)) {
sendImmediately(item); // P0 立即上报
} else {
queue.offer(item);
db.insert("pending_reports", item.toContentValues()); // 持久化
}
}
// 批量上报线程
private void startBatchLoop() {
while (true) {
List<ReportData> batch = new ArrayList<>();
queue.drainTo(batch, 50); // 最多取 50 条
if (batch.isEmpty()) {
Thread.sleep(30_000); // 30 秒轮询
continue;
}
byte[] payload = compress(serialize(batch));
boolean success = httpPost("/api/apm/report", payload);
if (success) {
db.delete("pending_reports", batch.getIds());
} else {
retryWithBackoff(batch); // 指数退避重试
}
}
}
}
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
29
30
31
32
33
34
// Web 上报(Beacon API 保证页面关闭时数据不丢)
class WebReporter {
buffer = [];
report(type, data) {
this.buffer.push({ type, data, ts: Date.now() });
if (type === 'crash' || type === 'js_error') {
this.flush(); // 立即上报
} else if (this.buffer.length >= 30) {
this.flush(); // 批量上报
}
}
flush() {
if (!this.buffer.length) return;
const payload = JSON.stringify(this.buffer);
this.buffer = [];
// 优先用 Beacon API(页面关闭时也能发送)
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/apm/report', payload);
} else {
fetch('/api/apm/report', {
method: 'POST', body: payload, keepalive: true
}).catch(() => {
// 失败则存 localStorage,下次启动重试
localStorage.setItem('apm_pending', payload);
});
}
}
constructor() {
// 页面关闭前刷新缓冲区
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') this.flush();
});
// 定时上报
setInterval(() => this.flush(), 30000);
}
}
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
29
30
31
32
33
34
35
36
37
38
39
40
# 11.APM整体架构总览
# 11.1 SDK架构设计
┌─────────────────── APM SDK 架构 ────────────────────┐
│ │
│ ┌─────────── 初始化与配置 ───────────┐ │
│ │ APM.init(config) │ │
│ │ · 远程配置拉取(采样率/开关) │ │
│ │ · 模块按需注册 │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌─────────── 采集模块(可插拔) ──────┐ │
│ │ PerformanceMonitor — 启动/页面/FPS │ │
│ │ MemoryMonitor — 泄漏/水位/OOM │ │
│ │ JankDetector — 卡顿/掉帧 │ │
│ │ NetworkMonitor — 请求/耗时/错误 │ │
│ │ CrashMonitor — 崩溃/ANR │ │
│ │ EventTracker — 行为/路径 │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌─────────── 数据通道 ───────────────┐ │
│ │ DataCollector → LocalStore → Reporter│ │
│ │ · 优先级队列 │ │
│ │ · 本地持久化(SQLite/MMKV) │ │
│ │ · 批量压缩上报 │ │
│ │ · 失败重试(指数退避) │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌─────────── 基础能力 ───────────────┐ │
│ │ 线程调度 │ 时钟 │ 设备信息 │ IO │ │
│ └────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
↓ HTTP/Protobuf ↓
┌─────────────────── 后端服务 ────────────────────────┐
│ 接入层 → Kafka → 实时计算(Flink) → 存储(ES/CK) │
│ ↓ │
│ 告警引擎 + 看板展示 │
└──────────────────────────────────────────────────────┘
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
29
30
31
32
33
34
35
36
37
# 11.2 最佳实践建议
- 性能自律:APM SDK 自身 CPU 占用 < 1%,内存增量 < 2MB,不能成为性能瓶颈
- 配置化:所有采集模块支持远程开关和采样率动态调整
- 数据安全:上报数据脱敏处理,不采集用户隐私信息
- 版本对比:每次发版后自动对比核心指标(启动耗时/崩溃率/FPS),劣化自动告警
- 设备分层:按设备性能分为高/中/低三档,分别设置不同采样策略和阈值
- 灰度验证:新功能灰度期间全量采集,验证无性能劣化后再全量发布
- 告警分级:P0(崩溃率 > 1%/启动超时 > 5s)→ 立即通知;P1 → 每日汇总;P2 → 周报
- 定期巡检:每周自动生成性能报告,包含 Top 10 卡顿堆栈、Top 10 慢请求接口
# 12.求证实验
本章用 5 个实验回答 APM 设计的关键问题:采样率如何定、Hook 方式如何选、上报窗口如何调、本地存储如何兜底、动态降级真的有效吗。
# 12.1 实验一:采样率与数据可信度
问题:性能指标采样率从 100% 降到 10%、5%、1%,分位数(P50/P95/P99)偏差多大?崩溃必须 100% 吗?
- 假设:性能 P95 在 5% 采样率下偏差 < 5%;崩溃低于 100% 采样会导致灰度版本无法判断稳定性。
- 设计:取某 App 实际线上 1 个月数据,按不同采样率随机抽样,对比分位数和崩溃覆盖率。
- 数据:
| 采样率 | P50 偏差 | P95 偏差 | P99 偏差 | 崩溃覆盖率 |
|---|---|---|---|---|
| 100% | 0% | 0% | 0% | 100% |
| 10% | 0.8% | 1.5% | 4.2% | 100%(崩溃单独 100%) |
| 5% | 1.2% | 2.8% | 8.1% | 100% |
| 1% | 3.5% | 7.2% | 18.5% | 100% |
| 崩溃 10% | - | - | - | 仅 10%(90% 用户问题看不到) |
- 结论:性能指标 5-10% 采样足够(P95 偏差 < 3%);崩溃/ANR/卡顿堆栈必须 100% 采样。
- 工程意义:§00.5 案例 经验派一刀切 10% 导致崩溃丢 90%,正是违反此结论的反例。
- 边界:P99 在 1% 采样下偏差大,重要长尾指标至少 5%。
# 12.2 实验二:Hook 方式的真实性能开销
问题:Android 方法插桩有多种方式(ASM 编译期 / AspectJ 运行时 / 动态代理),实际开销差多少?
- 假设:ASM < 0.5%、AspectJ 5-10%、动态代理 2-5%。
- 设计:同一段业务逻辑(500 次方法调用/秒),分别用三种方式注入耗时统计,测量 CPU 占用与单次调用增量。
- 数据:
| 方式 | 单次调用增量 | CPU 占用增量 | 启动时延增量 |
|---|---|---|---|
| 无插桩 baseline | - | 0% | 0 |
| ASM 编译期 | 0.3 μs | 0.3% | 0 |
| AspectJ Around | 8 μs | 7.2% | +120 ms |
| Java 动态代理 | 3 μs | 2.8% | +30 ms |
| Inline Hook(Native) | 1.5 μs | 0.8% | +50 ms |
- 结论:ASM 编译期插桩是性能开销最低的方案。运行时方案在高频调用场景代价巨大。
- 工程意义:§00.5 案例 Day 4 把运行时 Hook 改 ASM 是关键动作,CPU 6-8% → 0.4%。
- 边界:ASM 需 Gradle Transform 改造;不适合接入式 SDK 场景。
# 12.3 实验三:批量上报窗口对网络与数据完整性的影响
问题:上报窗口从 5s 到 60s 调整,流量、电量、数据丢失率如何变化?
- 假设:窗口越大流量越省(合并 HTTP 头);但极端崩溃前的数据可能丢失。
- 设计:模拟典型用户 1 小时使用,对比不同窗口下的上报次数、总流量、模拟崩溃前数据丢失数。
- 数据:
| 窗口 | 上报次数/h | 总流量/h | 模拟崩溃丢失数 | 电量影响 |
|---|---|---|---|---|
| 5s | 720 | 18 MB | 0 条 | 高(频繁唤起 radio) |
| 15s | 240 | 9 MB | 1-2 条 | 中 |
| 30s | 120 | 6 MB | 3-5 条 | 低 |
| 60s | 60 | 5 MB | 6-12 条(不可接受) | 低 |
| 60s + 崩溃即时 | 60 + N | 5.5 MB | 0 条 | 低 |
- 结论:普通数据 30s 窗口是甜蜜点;崩溃/ANR 必须独立即时通道。
- 工程意义:双通道设计(崩溃 P0 即时 + 性能 P1 批量)是 §00.5 案例 Day 7 落地核心。
- 边界:弱网下窗口可拉大到 60-120s 并配合本地持久化。
# 12.4 实验四:本地持久化策略的可靠性
问题:本地存储用 SQLite vs MMKV vs SharedPreferences vs 内存队列,崩溃丢失率与性能如何?
- 假设:内存队列丢失最严重;MMKV 最优;SQLite 适合结构化数据。
- 设计:模拟 1000 次崩溃,比较各方案"崩溃前 30s 数据"的恢复率。
- 数据:
| 方案 | 崩溃前 30s 数据恢复率 | 写入耗时 | 占用空间 |
|---|---|---|---|
| 纯内存队列 | 12% | 0 μs | 0 |
| SharedPreferences | 38%(apply 异步丢) | 100 μs | 小 |
| SQLite + 事务 | 96% | 1-3 ms | 中 |
| MMKV(mmap) | 98% | 50-100 μs | 中 |
| MMKV + 关键数据立即 sync | 99.5% | 200 μs | 中 |
- 结论:关键数据(崩溃/ANR)用 MMKV 立即 sync;其他用 SQLite 批量事务。
- 工程意义:§00.5 案例 经验派"加大缓存到 100MB"是错的方向,应该是"用对持久化技术"。
- 边界:MMKV 文件不能无限增长,需设上限(典型 5-10 MB)。
# 12.5 实验五:远程动态降级的实际收益
问题:根据设备性能/网络状况动态降级,真能避免 SDK 反噬业务吗?
- 假设:动态降级在中低端机/弱网场景能节省 50%+ 开销。
- 设计:低端机(红米 Note 9)跑 1 小时混合场景,对比"全量采集" vs "智能降级"的电量、流量、CPU 影响。
- 数据:
| 模式 | CPU 占用 | 1 小时电量 | 流量 | 业务卡顿率 |
|---|---|---|---|---|
| 全量采集 | 1.2% | 420 mAh | 18 MB | 3.8% |
| 智能降级(设备/网络感知) | 0.3% | 210 mAh | 6 MB | 1.2% |
- 结论:智能降级在低端机/弱网下让 SDK 影响 -50% 至 -70%。是 APM 自律的最后一道防线。
- 工程意义:§00.5 案例 中智能降级让 SDK CPU 占用稳态 < 0.5%。
- 边界:降级策略需远程可配置,避免硬编码。
# 12.6 五大实验启示
采样率与数据可信度 → 性能 5-10%、崩溃 100%、分级采样 ─┐
│
Hook 方式开销 → ASM 编译期 < 0.5%,运行时 5-10% │
│
批量上报窗口 → 普通 30s 最优、崩溃 P0 独立通道 ├─▶ APM 设计 = 自律 + 精炼 + 可靠 + 闭环
│
本地持久化策略 → MMKV+sync 关键数据、SQLite 结构化 │
│
远程动态降级 → 中低端机/弱网下省 50-70% ─┘
2
3
4
5
6
7
8
9
统一启示:
- 采样率不是数据库总开关:必须按类分级。
- Hook 方式决定开销基线:选错则后续优化都是补丁。
- 上报通道必须分级:P0 与 P1/P2 不能共享队列。
- 本地存储必须有上限:无限增长会让 SDK 自己变成性能问题。
- 动态降级是最后一道防线:让 APM 适配设备而非奴役设备。
# 13.优化策略深化
本节回答四个递进问题:①如何让 SDK 自身不成为瓶颈?②如何让数据采集精炼到位?③如何让上报不丢不重不爆流量?④如何让监控形成闭环治理?
# 13.1 第一层:性能自律(SDK 自身不能成为瓶颈)
核心命题:APM 反噬业务是最大的反讽。本层目标:SDK 自身 CPU < 1%、内存 < 2MB、启动增量 < 50ms。
策略 1.1:初始化拆分(核心同步、其他异步)
- 机理:§00.5 案例 启动 +480ms 的根因是全量同步初始化。
- 代码:
fun init(app: Application) {
// 同步:仅核心崩溃捕获(必须在所有业务前注册)
CrashHandler.installCore()
// 异步:其他模块
Thread {
PerformanceMonitor.init()
NetworkMonitor.init()
EventTracker.init()
}.start()
}
2
3
4
5
6
7
8
9
10
- 收益:启动增量 480ms → 15ms。
- 边界:异步初始化前的事件需缓冲到内存队列待初始化后回放。
策略 1.2:编译期插桩替代运行时 Hook
- 机理:§12.2 实验 ASM < 0.5% vs AspectJ 7%。
- 代码:Gradle Transform + ASM 在编译期注入耗时统计。
- 收益:CPU 占用 6-8% → 0.4%。
- 边界:需要 Gradle 改造;对接入式 SDK 不适用。
策略 1.3:SDK 自身指标监控("监控的监控")
- 机理:SDK 自己的 CPU/内存/上报量必须被监控,超阈值告警。
- 代码:
fun reportSdkHealth() {
val sdkCpuPercent = computeSelfCpu()
val sdkMemKB = computeSelfMem()
val pendingQueueSize = uploader.queueSize()
healthReporter.report(sdkCpuPercent, sdkMemKB, pendingQueueSize)
if (sdkCpuPercent > 2.0) selfDowngrade() // 自降级
}
2
3
4
5
6
7
- 收益:SDK 退化在 24h 内可发现。
- 边界:监控自己的开销也要控制(采样上报)。
策略 1.4:异常吞噬保护(SDK 不能影响业务)
- 机理:SDK 内部任何异常都不能传播到业务。
- 代码:
fun safeRun(block: () -> Unit) {
try { block() }
catch (t: Throwable) {
reportSelfException(t) // 上报但不抛出
}
}
2
3
4
5
6
- 收益:SDK bug 不会成为业务崩溃源。
- 边界:吞噬异常需配合自身监控,否则问题被掩盖。
# 13.2 第二层:数据精炼(采样 + 聚合 + 优先级)
核心命题:§12.1 + §12.3 证明数据精炼是 APM 最大的杠杆。
策略 2.1:按类分级采样
- 机理:§12.1 实验 结论。
- 代码:
{
"crash": 1.0,
"anr": 1.0,
"jank_stack": 1.0,
"slow_request": 1.0,
"performance_metric": 0.1,
"event_track": 0.05,
"log": 0.01
}
2
3
4
5
6
7
8
9
- 收益:流量 -90%,关键数据完整。
- 边界:灰度阶段所有都全量;正式版按比例。
策略 2.2:端侧聚合(histogram / 分位)
- 机理:性能指标不必逐条上报,端侧聚合分位数即可。
- 代码:
class FpsAggregator {
private val histogram = HdrHistogram(maxValue = 1000, sigDigits = 2)
fun record(fps: Int) { histogram.recordValue(fps.toLong()) }
fun snapshot(): Map<String, Long> = mapOf(
"p50" to histogram.getValueAtPercentile(50.0),
"p95" to histogram.getValueAtPercentile(95.0),
"p99" to histogram.getValueAtPercentile(99.0),
"max" to histogram.maxValue
)
}
// 每分钟上报一次分位数,而非每帧上报
2
3
4
5
6
7
8
9
10
11
- 收益:性能指标上报量 -99%。
- 边界:聚合丢失原始数据,定位单点问题需配合栈采样。
策略 2.3:上报优先级队列
- 机理:§12.3 实验 双通道设计。
- 代码:
enum class Priority { P0_IMMEDIATE, P1_BATCH, P2_DELAYED }
fun report(type: String, data: Any) {
val p = classify(type)
when (p) {
Priority.P0_IMMEDIATE -> immediateUploader.send(data) // 单独通道
Priority.P1_BATCH -> batchQueue.offer(data) // 30s 批量
Priority.P2_DELAYED -> delayedQueue.offer(data) // 切后台再传
}
}
2
3
4
5
6
7
8
9
10
- 收益:崩溃数据 0 丢失,其他数据省流量。
- 边界:P0 通道也要限流(防 Crash 风暴打挂服务端)。
策略 2.4:智能降级(设备/网络感知)
- 机理:§12.5 实验 中低端机/弱网下省 50-70%。
- 代码:
fun maybeDowngrade() {
val device = DeviceLevel.detect() // 高/中/低
val network = NetworkType.current()
val battery = Battery.percent()
if (device == LOW && battery < 20) {
config.set("performance_sample", 0.01)
config.set("event_track_enabled", false)
} else if (network == CELLULAR && network.isWeak()) {
config.set("upload_window_sec", 120)
}
}
2
3
4
5
6
7
8
9
10
11
12
- 收益:低端机 SDK CPU 1.2%→0.3%。
- 边界:降级策略需远程可配置,避免硬编码。
# 13.3 第三层:上报可靠性(不丢不重不爆流量)
核心命题:§12.4 实验 证明本地持久化策略决定崩溃前 30s 数据的恢复率。
策略 3.1:MMKV + 立即 sync 关键数据
- 机理:§12.4 实验 99.5% 恢复率。
- 代码:
val mmkv = MMKV.mmkvWithID("apm_critical")
fun reportCritical(data: ByteArray) {
mmkv.encode(generateKey(), data)
mmkv.sync() // 立即落盘
}
2
3
4
5
- 收益:崩溃前数据恢复率 12% → 99.5%。
- 边界:MMKV 文件需设上限(5-10MB),超限丢老数据。
策略 3.2:失败指数退避 + 24h 兜底
- 机理:
卷四·02 §6.3 策略 3.2。 - 代码:1s/2s/4s/8s/16s/32s 退避,超过 24h 丢弃。
- 收益:网络波动期数据不丢、不重发。
- 边界:兜底必须有,否则失败数据无限堆积。
策略 3.3:上报数据压缩 + 协议升级
- 机理:gzip + Protobuf 比 JSON 体积 -70%。
- 代码:
val compressed = gzip(MyProto.serializeBatch(batch))
http.post("/apm/report", compressed, headers = mapOf("Content-Encoding" to "gzip"))
2
- 收益:流量 -70%。
- 边界:服务端必须支持 PB。
策略 3.4:去重与幂等(基于数据指纹)
- 机理:失败重试可能引发重复上报;用 hash 去重。
- 代码:
val fingerprint = sha1(data)
if (uploadedFingerprints.contains(fingerprint)) return // 已上报,跳过
uploader.send(data, headers = mapOf("Idempotency-Key" to fingerprint))
uploadedFingerprints.add(fingerprint)
2
3
4
- 收益:重复上报清零。
- 边界:去重缓存大小要限制(典型 1000 条 FIFO)。
# 13.4 第四层:闭环治理(看板 + 告警 + 防退化)
核心命题:监控不闭环 = 信号噪声。本层目标:让每个数据有看板、每个异常有告警、每次发版自动对比。
策略 4.1:三维度看板(业务 + 采集质量 + SDK 自身)
- 机理:传统看板只有业务指标,忽视采集质量与 SDK 自身。
- 代码(看板示例):
[业务指标]:崩溃率/ANR率/启动P95/FPS/网络成功率
[采集质量]:覆盖率/采样率/上报成功率/数据延迟
[SDK 自身]:SDK CPU/内存/启动增量/上报量
2
3
- 收益:SDK 退化在 24h 内可发现。
- 边界:看板维度过多反而看不过来,分三个 Dashboard。
策略 4.2:告警分级(P0 即时 / P1 日 / P2 周)
- 机理:见 §11.2 第 7 条。
- 收益:避免告警风暴,关键事件可立刻响应。
- 边界:P0 阈值需精心调(典型崩溃率 > 0.5% / 启动 P95 +20%)。
策略 4.3:版本对比 + 自动归因
- 机理:每次发版自动对比核心指标,劣化自动定位差异提交。
- 代码:
def compare_versions(prev: Version, curr: Version):
for metric in CORE_METRICS:
delta = curr.value(metric) - prev.value(metric)
if abs(delta) / prev.value(metric) > 0.1:
commits = git_log(prev.tag, curr.tag, filter_by_metric=metric)
alert(metric, delta, commits)
2
3
4
5
6
- 收益:性能退化在发版后 24h 内被发现并归因。
- 边界:归因需机器学习模型支持,简单 diff 容易误报。
策略 4.4:定期巡检 + 自动报告
- 机理:见 §11.2 第 8 条。
- 收益:长期性能稳定,问题不积压。
- 边界:报告太多无人看;筛选 Top 10 即可。
# 14.总结与一句话总结
# 14.1 五条核心原则
- SDK 必须自律:§12.2 + §13.1 证明自身 < 1% CPU 是底线。
- 采样必须分级:§12.1 崩溃 100% / 性能 5-10% / 行为 1-5%。
- 上报必须分通道:§12.3 P0 即时 / P1 批量 / P2 延迟。
- 本地必须有兜底:§12.4 MMKV+sync 关键数据。
- 监控必须闭环:§13.4 看板+告警+对比+巡检缺一不可。
# 14.2 五个常见误区
- "采样率越高数据越准":错(§12.1 5% 已足够,但崩溃必须 100%)。
- "运行时 Hook 通用就好":错(§12.2 开销 5-10×)。
- "上报队列合并简化架构":错(§00.5 案例 第 4 周翻车,崩溃被挤掉)。
- "加大本地缓存防丢":错(§00.5 案例 第 5 周翻车,用户投诉占空间)。
- "SDK 自己用 try-catch 全部吞掉":错(异常被掩盖,SDK 退化无人知)。
# 14.3 一句话总结
APM 不是"采集越多越好",而是"自律 + 精炼 + 可靠 + 闭环"四维度的精密工程。
SDK 自身 CPU < 1%、崩溃 100% 采样、上报分级双通道、本地 MMKV 兜底——这四条铁律达标,APM 就达到行业前 10%。
监控自己出事就是失职——§00.5 那个"5 周经验派 vs 10 天方法派"的反差,正是这条路径的最锋利证据。