iOS开发基础106 - Instruments 性能分析
iOS Instruments 性能分析
Instruments 是 Xcode 内置的性能分析神器,能帮你找到 CPU 瓶颈、内存泄漏、卡顿掉帧、耗电过快等各种性能问题。本文从 Instruments 的工作原理讲起,系统梳理 8 个常用模板的使用方法和技巧,以及完整的性能问题排查流程。
一、Instruments 概述
Instruments 是一个基于时间线的性能记录仪——在 App 运行时持续采集 CPU、内存、GPU、网络等数据,记录在时间轴上,停止后通过各种分析视图帮你定位性能问题。
工作原理
Instruments 采用两种分析方式:
| 方式 | 说明 | 代表模板 |
|---|---|---|
| 采样式(Sampling) | 每隔几毫秒抓取一次线程调用栈,统计各函数占用时间比例 | Time Profiler |
| 插桩式(Instrumentation) | 在代码/系统中插入钩子,精确记录每个事件 | Allocations、Leaks、Network |
重要提醒:性能测试必须用真机!模拟器用的是 Mac 的 CPU/GPU,数据完全不代表真实设备表现。
打开方式
- Xcode → Xcode 菜单 → Open Developer Tool → Instruments。
- 或者在 Xcode 中长按 Run 按钮旁边的 Profile 按钮(⌘+I),直接用当前项目启动 Instruments。
二、界面组成(Xcode 13+ 新版)
┌─────────────────────────────────────────────────────────┐
│ 工具栏:选择目标 App、Record/Stop、模板选择 │
├──────────┬──────────────────────────────────────────────┤
│ │ 时间线面板(Timeline): │
│ 仪器列表 │ 每个 Instrument 一行,显示数据随时间变化的曲线 │
│ │ │
│ (Instrument│ │
│ List) │ │
│ │ │
├──────────┴──────────────────────────────────────────────┤
│ 详细面板(Detail Panel): │
│ 根据选中的 Instrument 显示 Call Tree / 内存列表 / FPS 等 │
├─────────────────────────────────────────────────────────┤
│ 扩展详情面板(Extended Detail): │
│ 显示选中函数的源码和具体行号 │
└─────────────────────────────────────────────────────────┘
三、基本使用流程
1. 打开 Instruments
↓
2. 选择分析模板(如 Time Profiler)
↓
3. 选择目标设备和 App
↓
4. 点击 Record 开始记录(App 自动启动)
↓
5. 在设备上操作 App,复现性能问题
↓
6. 点击 Stop 停止记录
↓
7. 分析时间线和详细面板,定位问题
↓
8. 修复代码,重新 Profile 验证
四、常用模板详解
1. Time Profiler(CPU 性能分析)
每隔 1 毫秒抓取一次所有线程的调用栈,统计每个函数出现在栈顶的次数,出现次数越多说明占用 CPU 时间越长,从而找到性能瓶颈。
适用场景
- App 卡顿、掉帧。
- 某个操作响应慢。
- CPU 使用率过高。
使用技巧
Call Tree 选项(详细面板底部):
| 选项 | 作用 |
|---|---|
| Separate by Thread | 按线程分开显示,方便定位主线程问题 |
| Invert Call Tree | 反转调用树,从叶子节点(最底层函数)开始显示 |
| Hide System Libraries | 隐藏系统库,只显示你自己的代码 |
| Show Obj-C Only | 只显示 Objective-C 方法 |
推荐配置:勾选 Separate by Thread + Hide System Libraries,然后看主线程(Thread 1)中最重的函数。
分析步骤
- 看时间线,找到 CPU 峰值的时间段。
- 点击峰值区域,缩小分析范围。
- 在 Call Tree 中展开主线程,找占用时间最长的函数。
- 双击函数,在扩展详情面板看具体代码行。
- 优化耗时操作(放到子线程、减少计算、缓存结果等)。
2. Allocations(内存分配分析)
记录每一次内存分配和释放,统计每个类创建了多少对象、占用多少内存、存活了多久,帮你找到内存浪费和异常增长。
适用场景
- 内存占用过高。
- 操作某个功能后内存持续增长不下降。
- 想优化内存使用。
核心视图
| 视图 | 说明 |
|---|---|
| Allocation Summary | 按类名统计:对象数量、占用内存、活跃数量 |
| Allocation Timeline | 内存随时间变化的曲线 |
| Generations | 标记多个时间点,对比各阶段的内存增长 |
Generations(代际分析)技巧
这是 Allocations 最强大的功能,用于检测"操作一次就涨一次内存"的问题:
1. 进入一个页面,点击 Mark Generation(标记 A)
↓
2. 操作一些功能,然后返回
↓
3. 再次点击 Mark Generation(标记 B)
↓
4. 对比 A 和 B 之间的内存增长
↓
5. 如果有对象应该释放但还存活,就是问题所在
分析要点
- 看 Persistent(持久)列:应该释放但还存在的对象。
- 看 # Persistent:持续存在的对象数量。
- 展开对象,可以看到分配时的调用栈。
3. Leaks(内存泄漏检测)
定期扫描内存,找到那些已经没有任何指针指向、但没有被释放的内存块(泄漏对象),并显示泄漏发生的位置。
适用场景
- 怀疑有内存泄漏。
- 内存持续增长导致 App 被系统杀死。
- 验证修复后是否还有泄漏。
工作原理
Leaks 模板 = Allocations + Leaks 两个 Instrument:
- Allocations 记录所有内存分配。
- Leaks 每隔 10 秒自动快照一次,扫描内存中的泄漏。
使用技巧
- 选择 Leaks 模板启动。
- 操作 App,重点操作你怀疑有泄漏的功能。
- 观察时间线,如果出现红色的 Leaks 柱状图,说明有泄漏。
- 点击泄漏区域,在详细面板看泄漏对象的类型和大小。
- 展开泄漏对象,查看分配时的调用栈,定位代码。
循环引用检测
Leaks 可以检测循环引用(retain cycle):
- 在详细面板中,泄漏对象会显示引用关系图。
- 两个对象互相强引用,形成循环,都无法释放。
- 常见原因:delegate 用 strong、block 中强引用 self、NSTimer 未 invalidate。
4. Core Animation(渲染性能 / FPS)
监测屏幕的帧率(FPS)和渲染性能,通过颜色标记找出离屏渲染、颜色混合、图片复制等导致卡顿的渲染问题。
适用场景
- 列表滑动卡顿、掉帧。
- 界面动画不流畅。
- GPU 使用率过高。
核心指标
- FPS:帧率,正常 60fps(iPhone 13 Pro 等支持 120fps),低于 60 就会感觉卡顿。
- GPU 使用率:渲染消耗的 GPU 资源。
诊断选项(Recording 时可开启)
| 选项 | 颜色标记 | 说明 |
|---|---|---|
| Color Offscreen-Rendered Yellow | 黄色 | 离屏渲染(需要先在离屏缓冲区渲染再合成,非常耗性能) |
| Color Blended Layers | 红色 | 颜色混合(透明层需要 GPU 计算混合,红色越多越耗性能) |
| Color Hits Green and Misses Red | 绿/红 | 缓存命中(绿)或未命中(红) |
| Color Copied Images | 蓝色 | 图片被复制(图片格式不被 GPU 直接支持,需要 CPU 转换) |
| Color Immediately | - | 移除颜色刷新的 10ms 延迟,更实时显示 |
常见优化方向
- 离屏渲染(黄色):避免 cornerRadius + masksToBounds 同时使用,用贝塞尔曲线绘制圆角,或用后台预渲染。
- 颜色混合(红色):尽量使用不透明视图(opaque = YES),避免透明背景。
- 图片复制(蓝色):使用 GPU 支持的图片格式(如 JPEG/PNG 解码后的位图),避免特殊格式。
5. Energy(能耗分析)
监测 App 的电量消耗,统计 CPU、GPU、网络、定位、蓝牙等各模块的能耗占比,帮你找到耗电大户。
适用场景
- 用户反馈耗电快。
- 后台运行时耗电。
- 想优化电池使用。
能耗来源
| 来源 | 说明 |
|---|---|
| CPU | 计算消耗的电量 |
| GPU | 渲染消耗的电量 |
| Network | 网络请求(蜂窝网络比 WiFi 更耗电) |
| Location | GPS 定位 |
| Bluetooth | 蓝牙通信 |
| Audio | 音频播放/录制 |
使用技巧
- 选择 Energy 模板启动。
- 正常使用 App 一段时间。
- 看时间线上的能耗曲线,找到高能耗时段。
- 展开详细面板,看各模块的能耗占比。
- 针对性优化:减少网络请求频率、降低定位精度、避免持续 CPU 计算等。
6. Zombie(僵尸对象检测)
当对象被释放后,不真正回收内存,而是标记为"僵尸",如果后续有代码访问这个已释放的对象,就会捕获并显示访问僵尸对象的位置,帮你定位 EXC_BAD_ACCESS 崩溃。
适用场景
- 偶现的 EXC_BAD_ACCESS 崩溃。
- 怀疑访问了已释放的对象。
- 难以复现的野指针问题。
使用方式
Zombie 有两种使用方式:
方式一:Instruments 的 Zombie 模板
- 选择 Zombie 模板启动。
- 操作 App 复现崩溃。
- 如果访问了僵尸对象,Instruments 会弹出提示,显示对象的类型、分配位置、释放位置和访问位置。
方式二:Xcode Scheme 中开启 Zombie Objects
- Xcode → Product → Scheme → Edit Scheme → Run → Diagnostics。
- 勾选 Zombie Objects。
- 正常 Run,访问僵尸对象时会在控制台打印
*** -[ClassName method]: message sent to deallocated instance。
注意:Zombie 模式会让对象不真正释放,内存会持续增长,只用于调试,不要在发布版本中开启。
7. Network(网络分析)
记录 App 的所有网络请求,显示请求 URL、方法、状态码、请求/响应大小、耗时,帮你找到慢请求和不必要的网络调用。
适用场景
- 网络请求慢。
- 想优化 API 调用。
- 排查网络相关的性能问题。
分析要点
- 看每个请求的耗时:找到最慢的请求。
- 看请求大小:请求/响应数据是否过大。
- 看请求频率:是否有重复或不必要的请求。
- 看并发数:同时发起的请求是否过多。
8. App Launch(启动性能)
专门分析 App 的启动过程,从点击图标到首屏渲染完成,拆解每个阶段的耗时,帮你优化启动速度。
适用场景
- App 启动慢。
- 想优化启动时间(冷启动 < 2 秒为优秀)。
启动阶段拆解
| 阶段 | 说明 |
|---|---|
| dyld 加载 | 加载动态库、可执行文件 |
| ObjC 运行时初始化 | 加载类、Category、+load 方法 |
| UIApplication 初始化 | didFinishLaunchingWithOptions |
| 首屏渲染 | 第一个界面的 viewDidLoad、viewWillAppear、布局、绘制 |
优化方向
- 减少
+load方法中的耗时操作。 didFinishLaunchingWithOptions中只做必要的初始化,其他延后。- 首屏控制器不要做复杂计算和网络请求(可以用占位图 + 异步加载)。
- 减少动态库数量(每个动态库都有加载开销)。
五、高级技巧
1. os_signpost 自定义性能标记
在代码中插入 os_signpost 标记,告诉 Instruments"这段代码从这里开始、到这里结束",Instruments 会在时间线上显示你自定义的性能区间,方便精确测量特定功能的耗时。
OC 代码示例
#import <os/signpost.h>
// 创建一个 log 实例(通常作为静态变量)
static os_log_t _signpostLog;
+ (void)initialize {
if (self == [MyClass class]) {
_signpostLog = os_log_create("com.myapp.performance", OS_LOG_CATEGORY_POINTS_OF_INTEREST);
}
}
// 测量一个函数的耗时
- (void)doSomeExpensiveWork {
// 开始标记
os_signpost_id_t signpostID = os_signpost_id_generate(_signpostLog);
os_signpost_interval_begin(_signpostLog, signpostID, "ExpensiveWork");
// 执行耗时操作
[self processLargeData];
// 结束标记
os_signpost_interval_end(_signpostLog, signpostID, "ExpensiveWork");
}
// 标记一个事件(瞬间发生,不是区间)
- (void)userTappedButton {
os_signpost_event_emit(_signpostLog, OS_SIGNPOST_ID_INVALID, "UserTappedButton");
}
Swift 代码示例
import os.signpost
class PerformanceMonitor {
static let shared = PerformanceMonitor()
let log = OSLog(subsystem: "com.myapp.performance", category: .pointsOfInterest)
func measure<T>(_ name: String, block: () -> T) -> T {
let signpostID = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: name, signpostID: signpostID)
let result = block()
os_signpost(.end, log: log, name: name, signpostID: signpostID)
return result
}
}
// 使用
PerformanceMonitor.shared.measure("加载数据") {
self.loadData()
}
在 Instruments 中查看
使用 Points of Interest 模板(或在任意模板中添加 Points of Interest Instrument),你标记的区间会显示在时间线上,方便和 CPU/内存数据对比分析。
2. Memory Graph Debugger(内存图调试器)
Xcode 内置的内存调试工具,运行时点击可以生成当前所有对象的引用关系图,直观看到哪个对象被谁持有,快速定位循环引用。
使用方法
- 在 Xcode 中 Run App。
- 点击调试栏底部的 Debug Memory Graph 按钮(三个圆圈的图标)。
- Xcode 会暂停 App,显示所有存活对象的列表。
- 点击某个对象,右侧显示它的引用关系图。
- 循环引用会以高亮显示(两个对象互相引用形成环)。
与 Instruments 的配合
- Instruments 的 Leaks 用于运行时自动检测泄漏。
- Memory Graph Debugger 用于某一时刻的静态快照,手动分析引用关系。
- 两者配合使用:Leaks 发现有泄漏 → Memory Graph 查看具体引用关系。
3. 多 Instrument 组合分析
Instruments 允许在一个会话中同时使用多个 Instrument:
- Time Profiler + Allocations:同时看 CPU 和内存,分析某个操作的性能和内存影响。
- Core Animation + Time Profiler:卡顿的时候同时看 FPS 和 CPU 调用栈。
- Network + Energy:分析网络请求对电量的影响。
添加方法:在 Instruments 界面右上角点击 + 按钮,从库中拖拽 Instrument 到时间线面板。
六、常见性能问题排查流程
1. 明确问题现象
├─ 卡顿掉帧 → Core Animation + Time Profiler
├─ 内存过高 → Allocations + Leaks
├─ 启动慢 → App Launch
├─ 耗电快 → Energy
└─ 网络慢 → Network
↓
2. 真机 Profile,复现问题
↓
3. 在时间线上定位问题时段
↓
4. 在详细面板找到具体函数/对象
↓
5. 双击查看源码,定位代码行
↓
6. 分析原因
├─ CPU:主线程耗时操作 → 移到子线程
├─ 内存:循环引用 → 用 weak/weak self
├─ 渲染:离屏渲染 → 优化圆角/阴影
├─ 启动:+load 耗时 → 延后初始化
└─ 网络:请求过多 → 合并/缓存
↓
7. 修复代码
↓
8. 重新 Profile 验证问题是否解决
七、Swift 版本对照
import os.signpost
import UIKit
// MARK: - os_signpost 封装
class PerformanceMonitor {
static let shared = PerformanceMonitor()
private let log = OSLog(subsystem: "com.myapp.performance", category: .pointsOfInterest)
/// 测量代码块耗时
func measure<T>(_ name: String, block: () -> T) -> T {
let signpostID = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: name, signpostID: signpostID)
let result = block()
os_signpost(.end, log: log, name: name, signpostID: signpostID)
return result
}
/// 标记事件
func event(_ name: String) {
os_signpost(.event, log: log, name: name)
}
}
// MARK: - 使用示例
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 测量 viewDidLoad 耗时
PerformanceMonitor.shared.measure("viewDidLoad") {
self.setupUI()
self.loadData()
}
}
func setupUI() {
// UI 初始化
}
func loadData() {
PerformanceMonitor.shared.measure("加载数据") {
// 模拟耗时操作
Thread.sleep(forTimeInterval: 0.5)
}
PerformanceMonitor.shared.event("数据加载完成")
}
}
八、常见问题与坑
Q1:Instruments 打开后没有我的 App?
检查:
- 设备是否连接(真机)或模拟器是否启动。
- App 是否已安装到设备上。
- 左上角的目标选择器是否选对了设备和 App。
- 如果是自定义的 Instruments 包,需要先安装。
Q2:Time Profiler 看不到自己的代码?
在 Call Tree 中勾选 Hide System Libraries,这样会隐藏系统框架的调用,只显示你自己的代码。如果还是看不到,检查是否勾选了 Separate by Thread,展开主线程(通常是 Thread 1)。
Q3:Leaks 检测不到泄漏?
可能的原因:
- 泄漏是偶现的,需要操作足够长的时间。
- 循环引用导致的泄漏,Leaks 可以检测,但需要等自动快照(默认 10 秒一次)。
- 有些泄漏是 Core Foundation 对象(非 OC 对象),需要手动管理。
- 可以用 Memory Graph Debugger 手动检查某一时刻的引用关系。
Q4:Zombie 模式下 App 内存暴涨?
正常现象。Zombie 模式下对象被释放后不会真正回收内存(保留为僵尸对象),所以内存会持续增长。这只用于调试,不要在发布版本中开启。
Q5:Core Animation 显示 FPS 正常但还是感觉卡?
可能的原因:
- FPS 是平均值,某几帧掉帧但平均下来正常。看时间线上的 FPS 曲线,找突然下降的点。
- 主线程阻塞导致响应慢(不是渲染问题,是 CPU 问题)。配合 Time Profiler 看主线程。
- 触摸事件延迟(手势处理耗时)。
Q6:模拟器测试性能可以吗?
不可以。模拟器用的是 Mac 的 CPU/GPU,性能远强于手机,数据完全不代表真实设备表现。性能测试必须用真机,最好用最低端的目标设备测试(性能最差的设备最容易暴露问题)。
Q7:Instruments 记录的数据可以保存吗?
可以。停止记录后,File → Save,保存为 .trace 文件。以后可以随时打开查看,也可以分享给团队成员分析。
Q8:如何分析启动时间?
用 App Launch 模板(Xcode 14+),或者用 Time Profiler 启动后立即操作。App Launch 模板会自动拆解启动阶段(dyld 加载、ObjC 初始化、didFinishLaunching、首屏渲染),显示每个阶段的耗时。
Q9:os_signpost 会影响性能吗?
影响极小。os_signpost 是非常轻量级的标记,Release 版本中也可以保留(只有在 Instruments 记录时才会真正输出数据)。但不要在高频循环中大量使用(如每帧调用),累积起来会有一定开销。
Q10:Instruments 和 Xcode Memory Graph 有什么区别?
| 对比 | Instruments | Xcode Memory Graph |
|---|---|---|
| 分析方式 | 运行时持续记录 | 某一时刻的静态快照 |
| 检测泄漏 | 自动检测(Leaks 模板) | 手动查看引用关系 |
| 性能数据 | CPU/内存/GPU/网络等 | 只有内存引用关系 |
| 使用时机 | 复现问题过程中 | 怀疑有泄漏时暂停查看 |
九、总结
- Instruments 是什么:基于时间线的性能分析工具,采样式 + 插桩式两种分析方式。
- 8 个常用模板:
- Time Profiler:CPU 性能,找耗时函数(Call Tree + Hide System Libraries)。
- Allocations:内存分配,Generations 对比内存增长。
- Leaks:内存泄漏,自动快照检测泄漏对象和循环引用。
- Core Animation:FPS/渲染,颜色标记离屏渲染、颜色混合。
- Energy:能耗,各模块耗电占比。
- Zombie:僵尸对象,定位 EXC_BAD_ACCESS。
- Network:网络请求,慢请求和不必要调用。
- App Launch:启动性能,拆解启动各阶段耗时。
- 高级技巧:os_signpost 自定义标记、Memory Graph Debugger、多 Instrument 组合分析。
- 关键原则:必须用真机测试、先定位再优化、优化后重新 Profile 验证。
- 排查流程:明确现象 → 选择模板 → 复现问题 → 定位时段 → 找到代码 → 分析原因 → 修复 → 验证。

浙公网安备 33010602011771号