本文是<
通常情况下,App 的性能问题虽然不会导致 App 不可用,但依然会影响到用户体验。
如果这个性能问题不断累积,达到临界点以后,问题就会爆发出来。这时,影响到的就不仅仅是用户了,还有负责 App 开发的我们。
为了能够主动、高效地发现性能问题,避免 App 质量进入无人监管的失控状态,我们就需要对 App 的性能进行监控。
对 App 的性能监控,主要是从线下和线上两个维度展开。
Instruments
Instruments 是苹果公司官方的性能监控工具。被集成在 Xcode 里,专门用来在线下进行性能分析。
Instruments 的功能非常强大,
- Energy Log 就是用来监控耗电量的,
- Leaks 就是专门用来监控内存泄露问题的,
- Network 就是用来专门检查网络情况的,
- Time Profiler 就是通过时间采样来分析页面卡顿问题的。
除了对各种性能问题进行监控外,还有以下两大优势:
- Instruments 基于 os_signpost 架构,可以支持所有平台。
- Instruments 由于标准界面(Standard UI)和分析核心(Analysis Core)技术,使得我们可以非常方便地进行自定义性能监测工具的开发。当你想要给 Instruments 内置的工具换个交互界面,或者新创建一个工具的时候,都可以通过自定义工具这个功能来实现。
从整体架构来看,Instruments 包括 Standard UI
和 Analysis Core
两个组件,它的所有工具都是基于这两个组件开发的。而且,你如果要开发自定义的性能分析工具的话,完全基于这两个组件就可以实现。
线上性能监控
对于线上性能监控,有两个原则:
- 监控代码不要侵入到业务代码中;
- 采用性能消耗最小的监控方案。
线上性能监控,主要集中在 CPU 使用率、FPS 的帧率和内存这三个方面。
CPU 使用率的线上监控方法
App 作为进程运行起来后会有多个线程,每个线程对 CPU 的使用率不同。各个线程对 CPU 使用率的总和,就是当前 App 对 CPU 的使用率。
在 iOS 系统中,你可以在 usr/include/mach/thread_info.h 里看到线程基本信息的结构体,其中的 cpu_usage 就是 CPU 使用率。结构体的完整代码如下所示:
struct thread_basic_info {
time_value_t user_time; // 用户运行时长
time_value_t system_time; // 系统运行时长
integer_t cpu_usage; // CPU 使用率
policy_t policy; // 调度策略
integer_t run_state; // 运行状态
integer_t flags; // 各种标记
integer_t suspend_count; // 暂停线程的计数
integer_t sleep_time; // 休眠的时间
};
因为每个线程都会有这个 thread_basic_info 结构体,只需要定时(比如,将定时间隔设置为 2s)去遍历每个线程,累加每个线程的 cpu_usage 字段的值,就能够得到当前 App 所在进程的 CPU 使用率了。实现代码如下:
+ (integer_t)cpuUsage {
thread_act_array_t threads; //int 组成的数组比如 thread[1] = 5635
mach_msg_type_number_t threadCount = 0; //mach_msg_type_number_t 是 int 类型
const task_t thisTask = mach_task_self();
//根据当前 task 获取所有线程
// task_threads 方法能够取到当前进程中的线程总数 threadCount 和所有线程的数组 threads。
kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
if (kr != KERN_SUCCESS) {
return 0;
}
integer_t cpuUsage = 0;
// 我们就可以通过遍历这个数组来获取单个线程的基本信息。其中,线程基本信息的结构体是 thread_basic_info_t,这个结构体里就包含了我们需要的 CPU 使用率的字段 cpu_usage。然后,我们累加这个字段就能够获取到当前的整体 CPU 使用率。
for (int i = 0; i < threadCount; i++) {
thread_info_data_t threadInfo;
thread_basic_info_t threadBaseInfo;
mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
// 获取 CPU 使用率
threadBaseInfo = (thread_basic_info_t)threadInfo;
if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
cpuUsage += threadBaseInfo->cpu_usage;
}
}
}
assert(vm_deallocate(mach_task_self(), (vm_address_t)threads, threadCount * sizeof(thread_t)) == KERN_SUCCESS);
return cpuUsage;
}
FPS 线上监控方法
FPS 是指图像连续在显示设备上出现的频率。FPS 低,表示 App 不够流畅,还需要进行优化。
和前面对 CPU 使用率和内存使用量的监控不同,iOS 系统中没有一个专门的结构体,用来记录与 FPS 相关的数据。但是,对 FPS 的监控也可以比较简单的实现:通过注册 CADisplayLink 得到屏幕的同步刷新率,记录每次刷新时间,然后就可以得到 FPS。具体的实现代码如下:
- (void)start {
self.dLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsCount:)];
[self.dLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
// 方法执行帧率和屏幕刷新率保持一致
- (void)fpsCount:(CADisplayLink *)displayLink {
if (lastTimeStamp == 0) {
lastTimeStamp = self.dLink.timestamp;
} else {
total++;
// 开始渲染时间与上次渲染时间差值
NSTimeInterval useTime = self.dLink.timestamp - lastTimeStamp;
if (useTime < 1) return;
lastTimeStamp = self.dLink.timestamp;
// fps 计算
fps = total / useTime;
total = 0;
}
}
内存使用量的线上监控方法
通常情况下,我们在获取 iOS 应用内存使用量时,都是使用 task_basic_info 里的 resident_size 字段信息。但这样获得的内存使用量和 Instruments 里看到的相差很大。后来,在 2018 WWDC Session 416 iOS Memory Deep Dive,苹果公司介绍说 phys_footprint 才是实际使用的物理内存。
struct task_vm_info {
mach_vm_size_t virtual_size; // 虚拟内存大小
integer_t region_count; // 内存区域的数量
integer_t page_size;
mach_vm_size_t resident_size; // 驻留内存大小
mach_vm_size_t resident_size_peak; // 驻留内存峰值
...
/* added for rev1 */
mach_vm_size_t phys_footprint; // 物理内存
...
开发一款自定义 Instruments 工具
Instruments 通过提供 os_signpost API 的方式使得开发者监控自定义的性能指标时更方便,从而解决了在此之前只能通过重新建设工具来完成的问题。并且,Instruments 是通过 XML 标准数据接口解耦展示和数据分析
主要包括以下这几个步骤:
- 在 Xcode 中,点击 File > New > Project;
- 在弹出的 Project 模板选择界面,将其设置为 macOS;
- 选择 Instruments Package,点击后即可开始自定义工具的开发了。
创建之后仅有一个源文件(.instrpkg)
运行后会弹出一个 Instruments 页面,在菜单栏 -> Instruments -> Preferences -> Packages
开发过程主要是对 instrpkg
文件的配置工作。这些配置工作中最主要的是要完成 Standard UI 和 Analysis Core 的配置。
苹果公司还提供了大量的代码片段,帮助你进行个性化的配置。你可以查看官方指南中的详细教程:https://help.apple.com/instruments/developer/mac/current/
配置 instrpkg 文件
Xcode提供的instrpkg模板中注释很多,核心的代码没有多少。
但是基本上可以知道这个代码是 XML 格式的,通过不同的标签标示不同的功能,package 标签标示一个包,紧接着是其子标签:id、title 与 owner 等等。
com.forping.Test
Test
forping
json-parse
JSON Decode
"com.forping.forping"
"jsonDecode"
"Parsing"
"Parsing started"
"Parsing end SIZE:" ?data-size-value
data-size
JSON Data Size
size-in-bytes
?data-size-value
impact
Impact
event-concept
(if (> ?data-size-value 80) then "High" else "Low")
com.forping.ticksinstrument
FPTicks
Behavior
tickDemo
Generic
json-parse
json-parse
JSON Decode
JSON Analyz
json-parse
data-size
impact
data-info
json-parse
data-size
impact
duration
这就是核心实现 Instruments 功能的代码了,详细解释如下:
- 使用了 Instrument 之后依旧需要添加对应的标识、标题等基本信息。
- 需要创建一个对这个自定义的 Instrument 需要有一张对应的表(table),故需要使用 create-table,值得注意的是这个表所需要的数据是直接来自于 tick schema。
- 开始创建一个轨道视图,这个轨道视图的数据来自 tick-table 这张表,由于这张表引用系统的 tick schema,tick 中有一个 time 属性,所以可以直接使用这个时间戳字段。
- 详情视图,使用 list 标签主要是在详情视图中显示数据的。这个 list 相当于我们开发中的 UITableView,tick-table 相当于数据源(dataSource)。
使用方法
选择 Blank , 点击新视图右侧的 +
号,选择我们 instrument 标题的 title
Analysis Core
如果你想要更好地进行个性化定制,就还需要再了解 Instruments 收集和处理数据的机制,也就是分析核心(Analysis Core )的工作原理。Analysis Core 收集和处理数据的过程,可以大致分为三步:
处理我们配置好的各种数据表,并申请存储空间 store;
store 去找数据提供者,如果不能直接找到,就会通过 Modeler 接收其他 store 的输入信号进行合成;
store 获得数据源后,会进行 Binding Solution 工作来优化数据处理过程。
在通过 store 找到的这些数据提供者中,对开发者来说最重要的就是 os_signpost。
os_signpost 的主要作用,是让你可以在程序中通过编写代码来获取数据。你可以在工程中的任何地方通过 os_signpost API ,将需要的数据提供给 Analysis Core。
模拟代码
os_log_t parsingLog = os_log_create("com.forping.forping", "jsonDecode");
os_signpost_id_t signid = os_signpost_id_generate(parsingLog);
os_signpost_interval_begin(parsingLog, signid, "Parsing started");
// 模拟耗时操作
[self jsonDecode];
os_signpost_interval_end(parsingLog, signid, "Parsing end");
运行效果
上面的代码,主要是获取项目中耗时操作的开始与结束的。其中在结束的时候会匹配出项目中的元数据:解析字符的大小。这里主要使用的就是 CLIPS 语言的变量。
接着就是 column
, 这个标签为 shema
定义一些字段, schema
是一个数据库。其中这个是数据库中有两个 key:data-size
与 impact
,其中 impact
是由 data-size-value
的值决定的,大于 80
时值是 High
, 否则为 Low
。
可以很清楚的看到每次 JSON 解析的开始与结束,以及执行所花的时间。
在实际开发中可能还会同时选中其它的调试模块,比如 Time Profiler、内存检测 等,这样能很好的全方位的分析当前的运行环境以及运行状态。
其他示例
官方示例
苹果公司在 WWDC 2018 Session 410 Creating Custom Instruments 里提供了一个范例:https://developer.apple.com/videos/play/wwdc2018/410
通过 os_signpost API 将图片下载的数据提供给 Analysis Core 进行监控观察。这个示例在 App 的代码如下所示:
//os_signpost 的 begin 和 end 需要成对出现。
os_signpost(.begin, log: parsinglog, name:"Parsing", "Parsing started SIZE:%ld", data.count)
// Decode the JSON we just downloaded
let result = try jsonDecoder.decode(Trail.self, from: data)
os_signpost(.end, log: parsingLog, name:"Parsing", "Parsing finished")
上面这段代码就是使用 os_signpost 的 API 获取程序里的数据。
Instruments 是如何通过配置数据表来使用这些数据的。配置的数据表的 XML 设计如下所示:
json-parse
Image Download
"com.apple.trailblazer
"Networking
"Parsing"
"Parsing started SIZE:" ?data-size
data-size
JSON Data Size
size-in-bytes
?data-size
配置数据表是要对数据输出进行可视化配置,从而可以将代码中的数据展示出来。如下图所示,就是对下载图片大小监控的效果。
参考链接:
https://juejin.cn/post/6844903854065057806