性能分析

YVTU

性能分析-工具

开源工具

  • FLEX (Flipboard Explorer) 是一个非常受欢迎的开源 iOS 内部工具,提供了强大的 UI,允许开发者在运行时检查应用的视图层次结构、网络请求、数据库、用户偏好设置等。适合开发者在应用内实时调试和检查性能问题。
  • XCTest XCTest 是 Apple 官方的单元测试框架,支持性能测试。开发者可以通过 measure 方法来衡量代码块的执行时间,从而发现性能瓶颈。适合需要在单元测试中添加性能测试的场景。
  • KSCrash KSCrash 是一个强大的崩溃报告框架,它不仅能够捕获崩溃信息,还能提供应用程序的性能数据,例如内存使用和 CPU 使用情况。适合需要深入了解崩溃原因并监控相关性能数据的场景。
  • GT (GDT, GodEye) GodEye 是一个开源的 iOS 性能监控工具包,提供了多种监控功能,包括 FPS、内存使用、CPU 使用率、网络请求、崩溃日志等。它有一个方便的 UI,可以实时显示性能数据。适合在开发过程中嵌入应用进行实时性能监控。
  • libimobiledevice libimobiledevice 是一个开源的库,提供了与 iOS 设备交互的 API,可以用来监控设备状态和性能,特别是对非越狱设备进行操作。

Instruments

Instruments 是 Xcode 自带的一款强大的性能分析和调试工具,广泛用于 iOS 应用的性能监测和问题排查。

打开 Xcode 项目,选择 Product > Profile,或者使用快捷键 Command + I。这会编译并启动应用,然后打开 Instruments 界面。你也可以直接从 Xcode 中选择 Xcode > Open Developer Tool > Instruments 启动 Instruments。

Instruments 提供了多种模板,每个模板用于不同类型的性能分析。常用的模板有:

  • Time Profiler:用于分析 CPU 使用情况,找出性能瓶颈。
  • Allocations:监控内存分配,帮助识别内存泄漏和过度使用。
  • Leaks:检测内存泄漏。
  • Core Animation:分析应用的动画性能,找出导致掉帧的原因。
  • Energy Log:评估应用的能耗表现。

当应用运行时,正常使用它或执行你需要分析的特定操作。Instruments 将记录这些操作期间的性能数据。点击 Record 按钮(通常是红色圆点),开始记录数据。完成后,点击 Stop 按钮停止记录。Instruments 会生成一个时间轴,展示应用在运行期间的各种性能数据。你可以通过拖动时间轴,放大或缩小某段时间范围内的数据。

细节分析:

  • CPU 使用情况:使用 Time Profiler 查看函数调用栈,识别性能瓶颈。
  • 内存使用情况:使用 Allocations 或 Leaks 查看对象分配情况,分析内存泄漏和不当的内存使用。
  • 帧率分析:使用 Core Animation 检查应用的帧率,查找导致掉帧的操作。

你可以在关键点添加标记(Mark),帮助更容易地定位和分析问题。

System Trace

System Trace 是一个强大的工具,可以帮助你深入分析和解决性能问题。System Trace 是 Xcode Instruments 中的一个模板,它提供了关于系统性能的详细信息,包括 CPU 使用情况、线程活动、系统调用、分页错误、I/O 接口信息等,甚至包括进程内以及进程间的情况。以下是 System Trace 的使用方法:

步骤一:准备工作

  1. 确保 Xcode 和设备准备好:确保你已经安装了最新版本的 Xcode,并将 iOS 设备连接到你的 Mac。
  2. 打开 Xcode Instruments:在 Xcode 中选择 Product > Profile,或者直接在 Finder 中打开 Instruments 应用。

步骤二:选择 System Trace 模板

  1. 选择模板:在 Instruments 中,打开“选择模板”窗口,选择 System Trace 模板。
  2. 连接设备:选择你要分析的 iOS 设备,确保设备处于调试模式并已连接。

步骤三:开始记录

  1. 启动记录:点击红色的记录按钮,开始采集数据。运行你的应用程序,并重现卡顿或性能问题。
  2. 结束记录:当你捕获到问题或完成测试后,点击停止按钮停止记录。

步骤四:分析数据

  1. 查看时间线:System Trace 会生成一个时间线视图,其中包括 CPU 使用情况、线程活动、硬件中断等。你可以通过缩放和滚动来查看不同时间点的详细信息。
  2. 分析线程活动:查看哪些线程在特定时间段内占用了大量 CPU 资源,找出是否存在长时间运行的任务或死锁。
  3. 查看内核事件:System Trace 还会显示内核相关的事件,如中断处理和内存管理,这对于深入分析系统级问题非常有帮助。
  4. 标记和过滤:使用标记和过滤功能,专注于特定时间段或线程,以更容易地识别问题。

步骤五:优化和调试

  1. 识别问题代码:根据分析结果,找到可能导致卡顿的代码段,查看是否有可以优化的部分。
  2. 修改代码并重新测试:修改代码后,重新运行 System Trace 来验证优化是否解决了性能问题。

步骤六:生成报告

  1. 保存和分享:你可以将 System Trace 生成的分析数据导出为 .trace 文件,方便共享或进一步分析。
  2. 对比分析:可以多次运行 System Trace,比较优化前后的数据,确保性能得到改善。

MetricKit

基本使用

MetricKit 允许开发者收集和分析与应用程序性能、崩溃、功耗等相关的关键指标。以下是使用 MetricKit 的方法:

首先,在你的项目中导入 MetricKit 框架:

1
import MetricKit

创建一个 MXMetricManager 的实例,它是收集指标数据的主要接口。

1
let metricManager = MXMetricManager.shared

注册 Metrics 管理器

你需要让你的类(通常是 AppDelegate 或其他适合的单例类)遵循 MXMetricManagerSubscriber 协议,并注册为 MXMetricManager 的订阅者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AppDelegate: UIResponder, UIApplicationDelegate, MXMetricManagerSubscriber {

func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
MXMetricManager.shared.add(self)
return true
}

// 处理 Metrics 数据
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
// 处理接收到的 Metrics 数据
print("Received metrics payload: \(payload)")
}
}

// 处理崩溃诊断数据
func didReceive(_ payloads: [MXCrashDiagnosticPayload]) {
for payload in payloads {
// 处理接收到的崩溃诊断数据
print("Received crash diagnostics payload: \(payload)")
}
}
}

分析和存储指标数据

MXMetricPayload 包含了多种指标数据,如 CPU 使用率、内存使用情况、网络流量等。你可以遍历这些数据,存储或发送到你的后端进行进一步分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
if let cpuMetrics = payload.cpuMetrics {
print("CPU Usage: \(cpuMetrics)")
}

if let memoryMetrics = payload.memoryMetrics {
print("Memory Usage: \(memoryMetrics)")
}

if let networkMetrics = payload.networkTransferMetrics {
print("Network Usage: \(networkMetrics)")
}

// 你可以将这些数据保存到文件或发送到服务器
}
}

处理崩溃诊断数据

MetricKit 也可以收集应用的崩溃诊断信息,方便你追踪和解决崩溃问题。

1
2
3
4
5
6
7
8
9
10
func didReceive(_ payloads: [MXCrashDiagnosticPayload]) {
for payload in payloads {
// 处理崩溃诊断数据
if let diagnostics = payload.crashDiagnostics {
for diagnostic in diagnostics {
print("Crash Diagnostic: \(diagnostic)")
}
}
}
}

从 Metric Manager 移除订阅者

如果你不再需要收集指标数据,可以从 MXMetricManager 中移除你的订阅者。

1
MXMetricManager.shared.remove(self)

MXMetricManager 只在实际设备上运行时才会收集数据,模拟器中不会生成有效的数据。MetricKit 数据的收集是异步的,且系统会自动决定何时发送数据给你的应用,所以你不会立即收到数据。数据的保留期较短,Apple 建议尽快处理和存储接收到的指标数据。

自定义和收集特定的应用指标

虽然 MetricKit 本身主要提供了一组预定义的指标,如启动时间、CPU 使用率、内存使用情况等,但你也可以通过结合其他技术手段来自定义和收集特定的应用指标。

思路

  1. 利用现有的 MetricKit 指标: 如果你的自定义指标能够与 MetricKit 提供的现有指标关联,最好优先利用这些指标。例如,你可以利用 MXAppLaunchMetric 来测量特定场景下的启动时间。
  2. 结合日志系统(如 os_log): 你可以通过 os_log 系统记录特定的自定义事件和数据,然后在 MXLogHandle 提供的日志中分析这些自定义指标。虽然这不是直接通过 MetricKit 记录,但你可以通过这些日志生成自定义报告。
  3. 使用第三方工具或库: 如果 MetricKit 无法满足需求,可以使用其他分析工具或库来收集和发送自定义指标,例如 Firebase Analytics 或 Amplitude。
  4. 自定义代码实现: 你可以在应用中自行编写代码,定期收集并记录自定义指标数据,然后通过网络请求或其他方式将其发送到后端服务器进行分析。
    假设你想自定义一个指标,用于监测用户在某个特定功能页面的停留时间。你可以通过以下方式实现:

在 ViewController 中记录开始时间和结束时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import UIKit
import os

class FeatureViewController: UIViewController {
private var startTime: Date?
private var osLog = OSLog(subsystem: "com.yourapp.feature", category: "Performance")

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startTime = Date()
os_log("Feature page appeared", log: osLog, type: .info)
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if let startTime = startTime {
let elapsedTime = Date().timeIntervalSince(startTime)
os_log("Feature page disappeared, duration: %{public}.2f seconds", log: osLog, type: .info, elapsedTime)
// 你可以在这里将 elapsedTime 发送到服务器或其他存储系统
}
}
}

定期发送收集到的数据。你可以通过定时任务或应用进入后台时,将收集到的自定义指标发送到服务器或分析系统。

1
2
3
func applicationDidEnterBackground(_ application: UIApplication) {
// 发送数据到服务器
}

将自定义指标与 MetricKit 集成。虽然 MetricKit 没有直接支持自定义指标的功能,但你可以通过 MXMetricManager 的代理方法捕获系统提供的指标数据,并结合自定义指标进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import MetricKit

class MetricManager: NSObject, MXMetricManagerSubscriber {
override init() {
super.init()
MXMetricManager.shared.add(self)
}

func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
// 处理 MetricKit 提供的系统指标
let cpuMetrics = payload.cpuMetrics
// 结合你的自定义指标进行综合分析
}
}
}

InApp分析工具

iOS 平台上常用的 In-app Debug 工具有以下几种:

  • Flex 是一个功能强大的 In-app Debug 工具,允许开发者在应用内实时查看和修改视图层次结构、网络请求、用户默认设置等。它还支持动态调整 UI 以及调试其他 app 内部逻辑。无需重新编译代码即可直接调试;可以修改内存中的值来观察变化。
  • Chisel 是 Facebook 开发的一组 LLDB 命令集,专门用于在调试时提供更方便的操作。它能帮助开发者快速检查视图层次结构、查看控件信息等。与 Xcode LLDB 无缝集成,通过命令行调试视图、打印出布局相关信息等。
  • Reveal 是一个图形化的 In-app Debug 工具,它允许开发者在运行中的应用中实时查看和编辑视图层次结构,支持 2D 和 3D 的视图展示。提供直观的 UI 调试界面,可以轻松地查看和修改视图属性;支持 iOS 和 tvOS。
  • Lookin 是一个开源的 iOS 视觉调试工具,专门用于分析和检查 iOS 应用的界面结构。它提供类似于 Xcode 的 View Debugging 功能,但更加灵活和强大,尤其是在复杂 UI 布局的分析上。通过 Lookin,你可以轻松地获取 iOS 应用中的界面层级、布局信息,并进行实时的 UI 调试和调整。可以称之为开源版的 Reveal。

fishhook

fishhook 是一个用于在 iOS 和 macOS 环境中进行函数替换的开源库,主要用于动态链接库(dylib)中的符号重写。

fishhook 通过操作 Mach-O 文件的符号表来找到需要替换的函数。Mach-O 文件的符号表包含了所有函数和变量的符号(名称和地址)信息。fishhook 使用 dyld(动态链接器)的相关函数来进行符号重定向。它首先使用 dyld 提供的接口找到符号的地址,然后将这些符号的地址替换为新的地址。通过修改 Mach-O 文件中的符号表来完成函数替换。这包括修改动态链接库中的 __dyld 的符号信息,使得在运行时调用这些符号时会跳转到新的实现。

fishhook 允许在程序运行时动态替换函数实现,这对于调试和修改第三方库的行为非常有用。

以下是一个简单的示例,展示了如何使用 fishhook 来替换函数实现:

1
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
#include <stdio.h>
#include <dlfcn.h>
#include "fishhook.h"

// 需要被替换的原始函数
static int (*orig_printf)(const char *, ...) = NULL;

// 新的函数实现
int new_printf(const char *format, ...) {
printf("Intercepted: ");
va_list args;
va_start(args, format);
int result = orig_printf(format, args);
va_end(args);
return result;
}

// 替换函数实现
void replace_printf() {
// 确保在替换之前已经加载了动态库
rebind_symbols((struct rebinding[1]){{"printf", new_printf, (void *)&orig_printf}}, 1);
}

int main() {
replace_printf();
printf("Hello, World!\n");
return 0;
}

在这个示例中,replace_printf 函数将 printf 函数的实现替换为 new_printf,并在控制台上输出 “Intercepted: “。

Frida

使用 Frida 来进行动态分析和调试

  1. 准备工作
  • Frida 工具链: 安装 Frida 的命令行工具。在 macOS 上可以通过 pip 安装: bash pip install frida-tools
  • 开发者证书: 需要一个有效的苹果开发者账号,用于签名自定义的 Frida 服务器应用。
  1. 编译并签名 Frida Server
  • 下载 Frida 源码: 从 Frida 的 GitHub 仓库克隆源码。
1
2
git clone https://github.com/frida/frida.git
cd frida
  • 编译 Frida Server: 使用 Frida 提供的构建脚本来编译 Frida Server。这个步骤可能需要一些配置,具体过程可以参考 Frida 官方文档。编-译完成后,你会得到一个 frida-server 可执行文件。
  • 使用开发者证书签名: 将编译好的 frida-server 文件进行签名,确保它可以在非越狱的设备上运行。
1
codesign -s "iPhone Developer: Your Name (Team ID)" --entitlements entitlements.plist frida-server

entitlements.plist 文件需要包括允许调试和 taskforpid 权限的配置。

  1. 部署 Frida Server 到设备
  • 使用 Xcode 部署: 将 frida-server 文件打包成一个应用,然后通过 Xcode 部署到目标设备上。
  • 或者通过 SSH 部署: 如果你已经有其他方式在非越狱设备上运行自签名应用,可以通过 SSH 将 frida-server 直接拷贝到设备上。
  1. 启动 Frida Server
  • 在设备上启动: 打开你部署的应用,确保 frida-server 在后台运行。你可以通过 Xcode 的调试控制台或终端命令来启动它。
  • 连接 Frida Server: 在你的电脑上通过 USB 连接设备,并使用 Frida CLI 连接到运行中的 Frida Server。
1
frida-ps -U
  • 开始调试: 你可以使用 Frida 的各种命令来分析和调试目标应用。
  1. Frida CLI 使用示例
  • 列出所有进程:
1
frida-ps -U
  • 附加到目标进程:
1
frida -U -n <AppName>
  • 运行脚本:
1
frida -U -n <AppName> -l script.js