前言
在介绍框架之前,先看一张图:
在系统设置中可以查看过去24小时(甚至更久)的电池使用情况。除了电池使用信息之外,系统还收集了APP的其他信息,比如占用CPU时间,内存使用峰值等。 这些信息都可以通过MetricKit框架获取到。
一、MetricKit 简介
MetricKit适用于iOS 13.0+的设备。它会在一天结束后,将过去24小时搜集的性能数据归集在一起,然后在下一次启动 App 后,在 delegate 的回调中提供给我们。
实际使用时除了获取到过去24小时的数据,24小时之前未被收集过的数据也会一并返回。
MetricKit 只是统计线上APP某些指标的使用情况,如果想统计某行代码/某个函数的资源使用情况,可以使用该框架提供的打点功能。
MetricKit 能统计哪些数据
- 设备信息,APP版本信息
- CPU
- GPU
- 蜂窝网络
- 前后台运行时间
- 定位(位置)
- 网络
- APP启动时间
- APP挂起
- 磁盘IO
- 内存
- 屏幕显示
- 自定义代码块打点
具体指标信息见下文的表格
接入 MetricKit 能产生什么价值
MetricKit 将系统收集的数据交给开发者,由开发来决定这些指标数据如何来处理。(例如解析后直接上传到第三方统计平台)
与MetricKit一起发布的还有另外两个工具
- XCTest Metrics (开发和测试阶段)
- MetricsKit (内测阶段和线上阶段)
- Xcode Metrics Organizer (线上阶段)
Xcode11 已经提供了 Xcode Metrics Organizer 功能,可使用快捷键 option + command + shift + o(英文o)
调出。这些数据是由iOS系统自动收集的,开发者只能在该窗口查看。
二、如何使用
MetricKit基本属于无侵入式接入,对原项目代码影响较小(接入后是否会带来性能问题有待研究),只需要简单几步即可使用。
一般在AppDelegate中接入即可。
1. 获取MetricManager单例
MXMetricManager *metricManager = [MXMetricManager sharedManager];
从目前来看,框架并没有提供任何自定义manager的接口,只能通过shared获取单例。
2. 为MetricManager单例添加订阅者
[metricManager addSubscriber:self];
注:框架同时提供了移除订阅者的api
[metricManager removeSubscriber:self];
3. 订阅者实现回调
// 1. 实现 MXMetricManagerSubscriber 协议
@interface ViewController ()<MXMetricManagerSubscriber>
@end
@implementation ViewController
// 2. 完善回调方法
- (void)didReceiveMetricPayloads:(NSArray<MXMetricPayload *> *)payloads {
// 每个payload表示一个24小时内的统计数据包。
// 数组包括过去24小时和其他时间段还未被处理过的数据
}
@end
通过上述三步就可以轻松接入MetricKit
完整接入案例
#import "ViewController.h"
#import <MetricKit/MetricKit.h>
@interface ViewController ()<MXMetricManagerSubscriber>
@property (nonatomic, strong) MXMetricManager *metricManager;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 获取MetricManager单例
self.metricManager = [MXMetricManager sharedManager];
// 2. 为MetricManager单例添加订阅者
[self.metricManager addSubscriber:self];
}
// 3. 完善回调方法
- (void)didReceiveMetricPayloads:(NSArray<MXMetricPayload *> *)payloads {
for (MXMetricPayload *payload in payloads) {
NSLog(@"%@", payload);
}
}
// 4. 移除订阅者
- (void)dealloc {
[self.metricManager removeSubscriber:self];
}
* 自定义打点
与iOS12发布的 os_signpost API 使用类似,因为MetricKit负责的是线上版本数据的收集,所以代码段的打点信息可以在回调中获取。
os_log_t loginterest = [MXMetricManager makeLogHandleWithCategory:@"categoryName"];
os_signpost_id_t spidinterest = os_signpost_id_generate(loginterest);
MXSignpostIntervalBegin(testHandle, spidinterest, "Launch");
// do something...
MXSignpostIntervalEnd(testHandle, spidinterest, "Launch");
最后得到的数据样式
signpostMetrics = (
{
signpostCategory = TestSignpostCategory1;
signpostIntervalData = {
histogrammedSignpostDurations = {
histogramNumBuckets = 3;
histogramValue = {
0 = {
bucketCount = 50;
bucketEnd = "100 ms";
bucketStart = "0 ms";
};
1 = {
bucketCount = 60;
bucketEnd = "400 ms";
bucketStart = "100 ms";
};
2 = {
bucketCount = 30;
bucketEnd = "700 ms";
bucketStart = "400 ms";
};
};
};
signpostAverageMemory = "100,000 kB";
signpostCumulativeCPUTime = "30,000 ms";
signpostCumulativeLogicalWrites = "600 kB";
};
signpostName = TestSignpostName1;
totalSignpostCount = 30;
}
);
三、注意事项
- 线上环境是24小时触发一次,开发阶段可以使用Xcode模拟触发,
Xcode > Debug > Simulate MetricKit Payloads
, 注意需要在真机调试才可以模拟,如果是模拟器,按钮是灰色的,无法点击。 - 该框架的大部分数据都是以 (value: double型数值,uint: 单位) 的方式返回,例如 1s, 1ms, 1kb。
- MXMetricPayload类(可理解为24小时内获取的数据总包)和MX_ _ _Metric类(具体到某种设备数据)都含有
- (NSData *)JSONRepresentation;
和- (NSDictionary *)DictionaryRepresentation;
,可快速转换为JSON类型查看。下文提到的payload均表示一个过去24小时的数据包。 - 部分设备数据以直方图(柱状图,histogram)的概念表示,整个结构会包括
numbBuckets
表示统计了几个区间,然后会有value
数组表示多个区间的具体数据, 每个区间对象会包括bucketStart
区间起始坐标,bucketEnd
区间结束坐标,bucketCount
区间具体数值。 以APP启动时间和蜂窝信号指标举例:
// APP启动时间
// 假设过去24小时数据统计内,共启动了110次,可以被划分为2个使用区间。
// 有50次启动时间出现在1000-1010ms内,60次在2000-2010ms内
histogrammedTimeToFirstDrawKey = {
histogramNumBuckets = 2;
histogramValue = {
0 = {
bucketCount = 50;
bucketEnd = "1,010 ms";
bucketStart = "1,000 ms";
};
1 = {
bucketCount = 60;
bucketEnd = "2,010 ms";
bucketStart = "2,000 ms";
};
};
};
// 蜂窝信号:bucketCount表示当前信号下APP使用时间的百分比。
// 在此payload的统计下,有20%的时间是1格信号,30%是2格信号,50%是三格信号
{
cellConditionTime = {
histogramNumBuckets = 3; // 共统计了三个区间
histogramValue = {
0 = {
bucketCount = 20;
bucketEnd = "1 bars";
bucketStart = "1 bars";
};
1 = {
bucketCount = 30;
bucketEnd = "2 bars";
bucketStart = "2 bars";
};
2 = {
bucketCount = 50;
bucketEnd = "3 bars";
bucketStart = "3 bars";
};
};
};
}
四、More
os_signpost()
iOS12 提供的代码段打点功能,配合Instruments,可视化观察打点代码段的运行时间。 详细使用参考: www.jianshu.com/p/4c112d850… everettjf.github.io/2018/08/13/…
NSDimension
度量单位抽象类,该框架很多数据都提供了具体单位
MXHistogram
我理解是此类包含一个数组的数量,和用于快速遍历数组的NSEnumerator对象。 类似于一个数组快速迭代器。
五、参考文章
- developer.apple.com/documentati…
- punmy.cn/2019/06/16/…
- nshipster.com/metrickit/
- appspector.com/blog/metric…
* 框架内部具体实现
MXMetricManager
Metric管理类。
有三个常用的方法:
-
- (id)sharedManager 单例
-
- (void)addSubscriber: 添加订阅者
-
- (void)removeSubscriber: 移除订阅者
当一个对象实现了 MXMetricManagerSubscriber
协议,并且将自己添加到MXMetricManager单例中,就可以接收系统收集的指标信息;
其他属性和方法:
- NSArray<MXMetricPayload *> *pastPayloads 接收到的性能指标信息
-
- (os_log_t)makeLogHandleWithCategory: 自定义打点有关
MXMetricManagerSubscriber
该协议有一个 @required 的方法需要实现,用以接收性能信息。
调用时机: 每天我们的应用最多只会收到一次回调,该次回调会把上一段 24 小时收集到的数据返回给我们。同时,如果在上一个 24 小时之前,存在老数据没有返回给我们的,也会在该次回调中一并返回。返回的数据会存储成数组的形式,每个数组的元素表示一天的数据。
- (void)didReceiveMetricPayloads:(NSArray<MXMetricPayload *> * _Nonnull)payloads;
MXMetricPayload
用以封装每天的指标信息。类似于一个24小时内的数据总包,内部通过只读属性提供具体的指标信息(CPU,GPU,蜂窝网络等)。
- MXMetricPayload封装了MetricKit目前支持的指标类型(如CPU,GPU,网络等,形如MX__Metric的类),这些指标信息可以为空,表示此指标数据不可用。
- MXMetricPayload提供了快速格式化为JSON的方法。
- (NSData *)JSONRepresentation;
和- (NSDictionary *)DictionaryRepresentation;
,可以转换成NSData或者NSDictionary. - MXMetricPayload包含24小时应用程序使用期间的数据。可以使用timeStampBegin和timeStampEnd属性来确定数据的时间范围。
- 考虑到APP会有不同的版本,MXMetricPayload会携带一个
latestApplicationVersion
的字符串,此字段会和数据统计时项目配置中的Version版本保持一致。语义上latest并不是表示APP的最新版本,而是说,在此24小时数据采集的区间内,如果你从1.0版本升级到了1.1版本,也就是当前payload的数据是两个版本的混合数据,latestApplicationVersion应该是显示的1.1而不是1.0。 (BOOL)includesMultipleApplicationVersions
字段表示是否发生了4中提到的,数据采集期间发生更新版本的现象。
总结 MXMetricPayload 内容如下
下面所提到的所有指标根据对设备的影响可被分为几类,电量指标、性能指标、磁盘访问指标、自定义指标
属性/方法 | 类型 | 介绍 |
---|---|---|
latestApplicationVersion | NSString | 收集此数据时的最高版本信息 (例如:1.0), 参考上面第4条 |
includesMultipleApplicationVersions | BOOL | 当前24小时数据包是否 包含多个版本,参考上面4,5 |
timeStampBegin | NSDate | 数据采集的开始时间 |
timeStampEnd | NSDate | 数据采集的结束时间 |
cpuMetrics | MXCPUMetric | 【电量】CPU指标 |
gpuMetrics | MXGPUMetric | 【电量】GPU指标 |
cellularConditionMetrics | MXCellularConditionMetric | 【电量】蜂窝网络指标 |
applicationTimeMetrics | MXAppRunTimeMetric | 【性能】运行时间指标, 前/后台运行时间, 后台媒体、定位的运行时间等 |
locationActivityMetrics | MXLocationActivityMetric | 【电量】定位(位置)指标 |
networkTransferMetrics | MXNetworkTransferMetric | 【电量】网络指标 |
applicationLaunchMetrics | MXAppLaunchMetric | 【性能】APP启动指标 |
applicationResponsivenessMetrics | MXAppResponsivenessMetric | 【电量】APP挂起指标 |
diskIOMetrics | MXDiskIOMetric | 磁盘IO指标 |
memoryMetrics | MXMemoryMetric | 【性能】内存指标 |
displayMetrics | MXDisplayMetric | 【电量】显示指标 |
signpostMetrics | NSArray<MXSignpostMetric *> | MXSignpost打点, 针对关键代码块打点, 记录性能数据 |
metaData | MXMetaData | 杂项元数据指标 (手机版本,os版本,Build等) |
- JSONRepresentation | (NSData *) | JSON格式化 |
- DictionaryRepresentation | (NSDictionary *) | JSON格式化 |
MXMetric
各种指标类的抽象基类,目前只提供了 JSONRepresentation
和 DictionaryRepresentation
两个通用的方法。
MXCPUMetric - 【电量】CPU指标
- cumulativeCPUTime: 记录CPU运行时间, 数据表示整个payload期间,该APP消耗的总CPU时间,会返回一个double时间值和unit单位。
MXGPUMetric - 【电量】GPU指标
记录GPU运行时间,规则同CPU
MXCellularConditionMetric - 【电量】蜂窝网络指标
- histogrammedCellularConditionTime: 此数据表示应用程序在不同蜂窝信号强度下运行的时间百分比。
在该类中使用到了一个自定义的单位 - bars MXUnitSignalBars: 一个表示信号强度的信号条数自定义度量单位
得到的JSON数据结构如下:
// APP在此payload的统计下,有20%的时间是1格信号,30%是2格信号,50%是三格信号
{
cellConditionTime = {
histogramNumBuckets = 3;
histogramValue = {
0 = {
bucketCount = 20;
bucketEnd = "1 bars";
bucketStart = "1 bars";
};
1 = {
bucketCount = 30;
bucketEnd = "2 bars";
bucketStart = "2 bars";
};
2 = {
bucketCount = 50;
bucketEnd = "3 bars";
bucketStart = "3 bars";
};
};
};
}
MXAppRunTimeMetric - 【性能】运行时间指标
表示APP在前后台,以及后台音频和定位服务所运行的时间,每个属性均有时长和单位
- cumulativeForegroundTime: 用户可见(前台)的运行时间
- cumulativeBackgroundTime: 用户不可见(后台)的运行时间
- cumulativeBackgroundAudioTime:后台播放音频的累计时间
- cumulativeBackgroundLocationTime:后台获取和处理位置信息的累计时间
MXLocationActivityMetric - 【电量】定位指标
在与相关的CLLocation中给出了几个枚举值:
- kCLLocationAccuracyBest:精度最高的定位
- kCLLocationAccuracyBestForNavigation:最适合导航用的定位
- kCLLocationAccuracyNearestTenMeters:定位精度在10米以内
- kCLLocationAccuracyHundredMeters:定位精度在100米以内
- kCLLocationAccuracyKilometer:定位精度在1000米以内
- kCLLocationAccuracyThreeKilometers:定位精度在3000米以内
所以该指标类提供了对应定位类型所用的时间(下面不再注释),结果为数值+时间单位
- cumulativeBestAccuracyTime
- cumulativeBestAccuracyForNavigationTime
- cumulativeNearestTenMetersAccuracyTime
- cumulativeHundredMetersAccuracyTime
- cumulativeKilometerAccuracyTime
- cumulativeThreeKilometersAccuracyTime
MXNetworkTransferMetric - 【电量】网络传输指标
返回值包含数值和单位 (单位转换提供了1:1000和1:1024两种方式,请自行选择)
- cumulativeWifiUpload:通过Wifi上传的累计数据量
- cumulativeWifiDownload:通过Wifi下载的累计数据量
- cumulativeCellularUpload:通过蜂窝网络上传的累计数据量
- cumulativeCellularDownload:通过蜂窝网络下载的累计数据量
MXAppLaunchMetric -【性能】APP启动指标
本类包含两个时间相关的数组,在过去24小时内,用户会不止一次的打开/后台切换APP,所以一天内会记录多个值,最后以10ms为间隔绘制直方图,然后显示某个时间区间出现的次数。
- histogrammedTimeToFirstDraw:应用程序启动所需时间的柱状图。(从点击APP图标开始,到第一个不是LaunchScreen的屏幕出现)
- histogrammedApplicationResumeTime:从后台恢复应用程序所需时间的柱状图。
// 在过去24小时内,有50次启动时间出现在1000-1010ms秒内
histogrammedTimeToFirstDrawKey = {
histogramNumBuckets = 3;
histogramValue = {
0 = {
bucketCount = 50;
bucketEnd = "1,010 ms";
bucketStart = "1,000 ms";
};
1 = {
bucketCount = 60;
bucketEnd = "2,010 ms";
bucketStart = "2,000 ms";
};
2 = {
bucketCount = 30;
bucketEnd = "3,010 ms";
bucketStart = "3,000 ms";
};
};
};
MXAppResponsivenessMetric -【电量】APP挂起指标
当APP挂起超过9秒时,将被记录到最后一个时间区间内。
- histogrammedApplicationHangTime:挂起时间分布直方图
MXDiskIOMetric - 磁盘IO指标
- cumulativeLogicalWrites: 累计写入的数据量,带单位
MXMemoryMetric - 【性能】内存指标
属性值包括数值和单位
- peakMemoryUsage:此APP在当前数据统计包下的内存使用峰值
- averageSuspendedMemory:此APP在当前数据统计包下的内存使用平均值
MXDisplayMetric - 【电量】显示指标
- averagePixelLuminance: OLED显示器上像素的平均亮度,值为0-100,为null则表示屏幕不支持APL。
MXSignpostMetric - 【自定义】代码块打点
与 os_signpost API 类似。(代码段打点,配合Instruments,可视化观察打点代码段的运行时间。) os_signpost 是开发阶段通过 Instruments 查看,MetricKit的打点功能可以将线上APP代码段的资源消耗情况提供给开发者。
- signpostName: 打点名称
- signpostCategory:打点分类名称
- signpostIntervalData:记录具体的的指标,如果当前name和category下没有记录信息,则为nil (MXSignpostIntervalData)
- totalCount:在此payload下使用该打点名称发出的打点总数。
MXSignpostIntervalData - 自定义打点的数据类 (还在研究)
- histogrammedSignpostDuration:当前name和category下打点在不同时间间隔分布的直方图
- cumulativeCPUTime:代码段累计CPU时间,nullable
- averageMemory:内存快照的平均值,nullable
- cumulativeLogicalWrites:累计逻辑写入数据的大小,nullable
MXMetaData - 设备信息
设备的部分信息(已经全部列出)
- regionFormat
- osVersion
- deviceType
- applicationBuildVersion
MXSignpost - 与自定义打点相关的宏定义
在 MXSignpost.h 中声明了几个调用 os_signpost API 的宏,
使用 MXSignpost 的打点功能要比普通 os_signpost 开销大,如果过度使用 MXSignpost, 会使APP性能倒退
// 具体参考 <os/signpost.h>
#define MXSignpostEventEmit(log, event_id, name, ...) _MXSignpostEventEmit_guaranteed_args(log, event_id, name, "" __VA_ARGS__)
#define MXSignpostIntervalBegin(log, event_id, name, ...) _MXSignpostIntervalBegin_guaranteed_args(log, event_id, name, "" __VA_ARGS__)
#define MXSignpostIntervalEnd(log, event_id, name, ...) _MXSignpostIntervalEnd_guaranteed_args(log, event_id, name, "" __VA_ARGS__)
一个payload模拟数据Dictionary格式如下
MetricKit提供的Dict格式化方法返回的数据并不是完整的,部分参数已被省略,详细参数请参考上表。
{
appVersion = "1.2";
applicationLaunchMetrics = {
histogrammedResumeTime = {
histogramNumBuckets = 3;
histogramValue = {
0 = {
bucketCount = 60;
bucketEnd = "210 ms";
bucketStart = "200 ms";
};
1 = {
bucketCount = 70;
bucketEnd = "310 ms";
bucketStart = "300 ms";
};
2 = {
bucketCount = 80;
bucketEnd = "510 ms";
bucketStart = "500 ms";
};
};
};
histogrammedTimeToFirstDrawKey = {
histogramNumBuckets = 3;
histogramValue = {
0 = {
bucketCount = 50;
bucketEnd = "1,010 ms";
bucketStart = "1,000 ms";
};
1 = {
bucketCount = 60;
bucketEnd = "2,010 ms";
bucketStart = "2,000 ms";
};
2 = {
bucketCount = 30;
bucketEnd = "3,010 ms";
bucketStart = "3,000 ms";
};
};
};
};
applicationResponsivenessMetrics = {
histogrammedAppHangTime = {
histogramNumBuckets = 3;
histogramValue = {
0 = {
bucketCount = 50;
bucketEnd = "100 ms";
bucketStart = "0 ms";
};
1 = {
bucketCount = 60;
bucketEnd = "400 ms";
bucketStart = "100 ms";
};
2 = {
bucketCount = 30;
bucketEnd = "700 ms";
bucketStart = "400 ms";
};
};
};
};
applicationTimeMetrics = {
cumulativeBackgroundAudioTime = "30 sec";
cumulativeBackgroundLocationTime = "30 sec";
cumulativeBackgroundTime = "40 sec";
cumulativeForegroundTime = "700 sec";
};
cellularConditionMetrics = {
cellConditionTime = {
histogramNumBuckets = 3;
histogramValue = {
0 = {
bucketCount = 20;
bucketEnd = "1 bars";
bucketStart = "1 bars";
};
1 = {
bucketCount = 30;
bucketEnd = "2 bars";
bucketStart = "2 bars";
};
2 = {
bucketCount = 50;
bucketEnd = "3 bars";
bucketStart = "3 bars";
};
};
};
};
cpuMetrics = {
cumulativeCPUTime = "100 sec";
};
diskIOMetrics = {
cumulativeLogicalWrites = "1,300 kB";
};
displayMetrics = {
averagePixelLuminance = {
averageValue = "50 apl";
sampleCount = 500;
standardDeviation = 0;
};
};
gpuMetrics = {
cumulativeGPUTime = "20 sec";
};
locationActivityMetrics = {
cumulativeBestAccuracyForNavigationTime = "20 sec";
cumulativeBestAccuracyTime = "30 sec";
cumulativeHundredMetersAccuracyTime = "30 sec";
cumulativeKilometerAccuracyTime = "20 sec";
cumulativeNearestTenMetersAccuracyTime = "30 sec";
cumulativeThreeKilometersAccuracyTime = "20 sec";
};
memoryMetrics = {
averageSuspendedMemory = {
averageValue = "100,000 kB";
sampleCount = 500;
standardDeviation = 0;
};
peakMemoryUsage = "200,000 kB";
};
metaData = {
appBuildVersion = 1;
deviceType = "iPhone11,2";
osVersion = "iPhone OS 13.1.2 (17A861)";
regionFormat = CN;
};
networkTransferMetrics = {
cumulativeCellularDownload = "80,000 kB";
cumulativeCellularUpload = "70,000 kB";
cumulativeWifiDownload = "60,000 kB";
cumulativeWifiUpload = "50,000 kB";
};
signpostMetrics = (
{
signpostCategory = TestSignpostCategory1;
signpostIntervalData = {
histogrammedSignpostDurations = {
histogramNumBuckets = 3;
histogramValue = {
0 = {
bucketCount = 50;
bucketEnd = "100 ms";
bucketStart = "0 ms";
};
1 = {
bucketCount = 60;
bucketEnd = "400 ms";
bucketStart = "100 ms";
};
2 = {
bucketCount = 30;
bucketEnd = "700 ms";
bucketStart = "400 ms";
};
};
};
signpostAverageMemory = "100,000 kB";
signpostCumulativeCPUTime = "30,000 ms";
signpostCumulativeLogicalWrites = "600 kB";
};
signpostName = TestSignpostName1;
totalSignpostCount = 30;
},
{
signpostCategory = TestSignpostCategory2;
signpostIntervalData = {
histogrammedSignpostDurations = {
histogramNumBuckets = 3;
histogramValue = {
0 = {
bucketCount = 60;
bucketEnd = "200 ms";
bucketStart = "0 ms";
};
1 = {
bucketCount = 70;
bucketEnd = "300 ms";
bucketStart = "201 ms";
};
2 = {
bucketCount = 80;
bucketEnd = "500 ms";
bucketStart = "301 ms";
};
};
};
signpostAverageMemory = "60,000 kB";
signpostCumulativeCPUTime = "50,000 ms";
signpostCumulativeLogicalWrites = "700 kB";
};
signpostName = TestSignpostName2;
totalSignpostCount = 40;
}
);
timeStampBegin = "2019-11-24 16:00:00 +0000";
timeStampEnd = "2019-11-25 15:59:00 +0000";
}