本文是<<iOS开发高手课>> 第十六篇学习笔记.
通常情况下,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 等等。
<?xml version="1.0" encoding="UTF-8" ?>
<package>
<id>com.forping.Test</id>
<title>Test</title>
<owner>
<name>forping</name>
</owner>
<!-- 可以理解成一个数据来源 -->
<os-signpost-interval-schema>
<id>json-parse</id>
<title>JSON Decode</title>
<!-- 这三个是与项目中代码一一对应 -->
<subsystem>"com.forping.forping"</subsystem>
<category>"jsonDecode"</category>
<name>"Parsing"</name>
<!-- 开始匹配-->
<start-pattern>
<message>"Parsing started"</message>
</start-pattern>
<!-- 结束匹配-->
<end-pattern>
<message>"Parsing end SIZE:" ?data-size-value</message>
</end-pattern>
<!-- 表中的一列 -->
<column>
<!-- 助记符标识, 在 graph 与 list 中只认这个标识 -->
<mnemonic>data-size</mnemonic>
<title>JSON Data Size</title>
<!-- 数据的类型 size-in-bytes -->
<type>size-in-bytes</type>
<!-- 显示 data-size 的值 -->
<expression>?data-size-value</expression>
</column>
<!-- https://help.apple.com/instruments/developer/mac/current/#/dev66257045 -->
<column>
<mnemonic>impact</mnemonic>
<title>Impact</title>
<type>event-concept</type>
<expression>(if (> ?data-size-value 80) then "High" else "Low")</expression>
</column>
</os-signpost-interval-schema>
<!-- 导入 tick 模块 可以使用 tick作为数据来源 相当于 `import` -->
<!-- <import-schema>tick</import-schema>-->
<!-- 开始构建一个 instrument -->
<instrument>
<id>com.forping.ticksinstrument</id>
<!-- 在 instrument中的title-->
<title>FPTicks</title>
<category>Behavior</category>
<purpose>tickDemo</purpose>
<icon>Generic</icon>
<!-- 创建一个表, 这个表中使用到了 `tick` -->
<create-table>
<id>json-parse</id>
<!-- os-signpost-interval-schema 的 id -->
<schema-ref>json-parse</schema-ref>
</create-table>
<!-- 轨道视图 为您的仪器定义要绘制的图形(可选) -->
<graph>
<title>JSON Decode</title>
<lane>
<title>JSON Analyz</title>
<table-ref>json-parse</table-ref>
<!-- 绘图、绘图模板或直方图元素 -->
<plot>
<value-from>data-size</value-from>
<color-from>impact</color-from>
</plot>
</lane>
</graph>
<!-- 详情视图 - 为您的仪器定义至少一个详细视图 -->
<list>
<title>data-info</title>
<table-ref>json-parse</table-ref>
<column>data-size</column>
<column>impact</column>
<column>duration</column>
</list>
</instrument>
<!-- Instruments Developer Help: https://help.apple.com/instruments/developer/mac/current/ -->
<!-- MARK: Schema Definitions -->
<!-- Define point and interval schemas needed to represent the input and output tables your package will use. -->
<!-- Two kinds are available: schemas with automatically generated modelers, and schemas that require custom modelers -->
<!-- Generated modelers: 'os-log-point-schema', 'os-signpost-interval-schema', 'ktrace-point-schema', 'ktrace-interval-schema' -->
<!-- Custom modeler required: 'point-schema', 'interval-schema' -->
<!-- To use existing schemas from other packages, declare 'import-schema' elements -->
<!-- MARK: Modeler Declarations -->
<!-- If there are schemas defined that require a custom modeler, each can be declared with a 'modeler' element -->
<!-- Modelers are based on CLIPS rules and may define 1..n output schemas, each requiring 1..n input schemas -->
<!-- MARK: Instrument Definitions -->
<!-- Instruments record and display data, creating concrete table requirements that instance modelers and data streams. -->
<!-- Any number of 'instrument' elements can be defined; each instrument should provide a cohesive graph and detail experience. -->
<!-- MARK: Embed Templates -->
<!-- Templates may be included and represent a collection of tools configured for a specific tracing workflow -->
<!-- Each 'template' element specifies the relative path to a .tracetemplate file in the project -->
<!-- To create a template: start with a blank document, configure with instruments desired, and choose "File -> Save as Template" -->
</package>
这就是核心实现 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 设计如下所示:
<os-signpost-interval-schema>
<id>json-parse</id>
<title>Image Download</title>
<subsystem>"com.apple.trailblazer</subsystem>
<category>"Networking</category>
<name>"Parsing"</name>
<start-pattern>
<message>"Parsing started SIZE:" ?data-size</message>
</start-pattern>
<column>
<mnemonic>data-size</mnemonic>
<title>JSON Data Size</title>
<type>size-in-bytes</type>
<expression>?data-size</expression>
</column>
</os-signpost-interval-schema>
配置数据表是要对数据输出进行可视化配置,从而可以将代码中的数据展示出来。如下图所示,就是对下载图片大小监控的效果。
参考链接: