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,数据完全不代表真实设备表现。

打开方式

  1. Xcode → Xcode 菜单 → Open Developer Tool → Instruments。
  2. 或者在 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)中最重的函数。

分析步骤

  1. 看时间线,找到 CPU 峰值的时间段。
  2. 点击峰值区域,缩小分析范围。
  3. 在 Call Tree 中展开主线程,找占用时间最长的函数。
  4. 双击函数,在扩展详情面板看具体代码行。
  5. 优化耗时操作(放到子线程、减少计算、缓存结果等)。

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 秒自动快照一次,扫描内存中的泄漏。

使用技巧

  1. 选择 Leaks 模板启动。
  2. 操作 App,重点操作你怀疑有泄漏的功能。
  3. 观察时间线,如果出现红色的 Leaks 柱状图,说明有泄漏。
  4. 点击泄漏区域,在详细面板看泄漏对象的类型和大小。
  5. 展开泄漏对象,查看分配时的调用栈,定位代码。

循环引用检测

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 音频播放/录制

使用技巧

  1. 选择 Energy 模板启动。
  2. 正常使用 App 一段时间。
  3. 看时间线上的能耗曲线,找到高能耗时段。
  4. 展开详细面板,看各模块的能耗占比。
  5. 针对性优化:减少网络请求频率、降低定位精度、避免持续 CPU 计算等。

6. Zombie(僵尸对象检测)

当对象被释放后,不真正回收内存,而是标记为"僵尸",如果后续有代码访问这个已释放的对象,就会捕获并显示访问僵尸对象的位置,帮你定位 EXC_BAD_ACCESS 崩溃。

适用场景

  • 偶现的 EXC_BAD_ACCESS 崩溃。
  • 怀疑访问了已释放的对象。
  • 难以复现的野指针问题。

使用方式

Zombie 有两种使用方式:

方式一:Instruments 的 Zombie 模板

  1. 选择 Zombie 模板启动。
  2. 操作 App 复现崩溃。
  3. 如果访问了僵尸对象,Instruments 会弹出提示,显示对象的类型、分配位置、释放位置和访问位置。

方式二:Xcode Scheme 中开启 Zombie Objects

  1. Xcode → Product → Scheme → Edit Scheme → Run → Diagnostics。
  2. 勾选 Zombie Objects。
  3. 正常 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 内置的内存调试工具,运行时点击可以生成当前所有对象的引用关系图,直观看到哪个对象被谁持有,快速定位循环引用。

使用方法

  1. 在 Xcode 中 Run App。
  2. 点击调试栏底部的 Debug Memory Graph 按钮(三个圆圈的图标)。
  3. Xcode 会暂停 App,显示所有存活对象的列表。
  4. 点击某个对象,右侧显示它的引用关系图。
  5. 循环引用会以高亮显示(两个对象互相引用形成环)。

与 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?

检查:

  1. 设备是否连接(真机)或模拟器是否启动。
  2. App 是否已安装到设备上。
  3. 左上角的目标选择器是否选对了设备和 App。
  4. 如果是自定义的 Instruments 包,需要先安装。

Q2:Time Profiler 看不到自己的代码?

在 Call Tree 中勾选 Hide System Libraries,这样会隐藏系统框架的调用,只显示你自己的代码。如果还是看不到,检查是否勾选了 Separate by Thread,展开主线程(通常是 Thread 1)。

Q3:Leaks 检测不到泄漏?

可能的原因:

  1. 泄漏是偶现的,需要操作足够长的时间。
  2. 循环引用导致的泄漏,Leaks 可以检测,但需要等自动快照(默认 10 秒一次)。
  3. 有些泄漏是 Core Foundation 对象(非 OC 对象),需要手动管理。
  4. 可以用 Memory Graph Debugger 手动检查某一时刻的引用关系。

Q4:Zombie 模式下 App 内存暴涨?

正常现象。Zombie 模式下对象被释放后不会真正回收内存(保留为僵尸对象),所以内存会持续增长。这只用于调试,不要在发布版本中开启。

Q5:Core Animation 显示 FPS 正常但还是感觉卡?

可能的原因:

  1. FPS 是平均值,某几帧掉帧但平均下来正常。看时间线上的 FPS 曲线,找突然下降的点。
  2. 主线程阻塞导致响应慢(不是渲染问题,是 CPU 问题)。配合 Time Profiler 看主线程。
  3. 触摸事件延迟(手势处理耗时)。

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 个常用模板:
    1. Time Profiler:CPU 性能,找耗时函数(Call Tree + Hide System Libraries)。
    2. Allocations:内存分配,Generations 对比内存增长。
    3. Leaks:内存泄漏,自动快照检测泄漏对象和循环引用。
    4. Core Animation:FPS/渲染,颜色标记离屏渲染、颜色混合。
    5. Energy:能耗,各模块耗电占比。
    6. Zombie:僵尸对象,定位 EXC_BAD_ACCESS。
    7. Network:网络请求,慢请求和不必要调用。
    8. App Launch:启动性能,拆解启动各阶段耗时。
  • 高级技巧:os_signpost 自定义标记、Memory Graph Debugger、多 Instrument 组合分析。
  • 关键原则:必须用真机测试、先定位再优化、优化后重新 Profile 验证。
  • 排查流程:明确现象 → 选择模板 → 复现问题 → 定位时段 → 找到代码 → 分析原因 → 修复 → 验证。

posted @ 2024-07-16 17:05  Mr.陳  阅读(1013)  评论(0)    收藏  举报