App 的性能监控

本文是<<iOS开发高手课>> 第十六篇学习笔记.

通常情况下,App 的性能问题虽然不会导致 App 不可用,但依然会影响到用户体验。

如果这个性能问题不断累积,达到临界点以后,问题就会爆发出来。这时,影响到的就不仅仅是用户了,还有负责 App 开发的我们。

为了能够主动、高效地发现性能问题,避免 App 质量进入无人监管的失控状态,我们就需要对 App 的性能进行监控。

对 App 的性能监控,主要是从线下和线上两个维度展开。

Instruments

Instruments 是苹果公司官方的性能监控工具。被集成在 Xcode 里,专门用来在线下进行性能分析。

Instruments 的功能非常强大,

  • Energy Log 就是用来监控耗电量的,
  • Leaks 就是专门用来监控内存泄露问题的,
  • Network 就是用来专门检查网络情况的,
  • Time Profiler 就是通过时间采样来分析页面卡顿问题的。
image.png

除了对各种性能问题进行监控外,还有以下两大优势:

  • Instruments 基于 os_signpost 架构,可以支持所有平台。
  • Instruments 由于标准界面(Standard UI)和分析核心(Analysis Core)技术,使得我们可以非常方便地进行自定义性能监测工具的开发。当你想要给 Instruments 内置的工具换个交互界面,或者新创建一个工具的时候,都可以通过自定义工具这个功能来实现。

从整体架构来看,Instruments 包括 Standard UIAnalysis 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 (&gt; ?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 功能的代码了,详细解释如下:

  1. 使用了 Instrument 之后依旧需要添加对应的标识、标题等基本信息。
  2. 需要创建一个对这个自定义的 Instrument 需要有一张对应的表(table),故需要使用 create-table,值得注意的是这个表所需要的数据是直接来自于 tick schema。
  3. 开始创建一个轨道视图,这个轨道视图的数据来自 tick-table 这张表,由于这张表引用系统的 tick schema,tick 中有一个 time 属性,所以可以直接使用这个时间戳字段。
  4. 详情视图,使用 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");

运行效果

image.png

上面的代码,主要是获取项目中耗时操作的开始与结束的。其中在结束的时候会匹配出项目中的元数据:解析字符的大小。这里主要使用的就是 CLIPS 语言的变量。
接着就是 column, 这个标签为 shema 定义一些字段, schema 是一个数据库。其中这个是数据库中有两个 key:data-sizeimpact,其中 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>

配置数据表是要对数据输出进行可视化配置,从而可以将代码中的数据展示出来。如下图所示,就是对下载图片大小监控的效果。


image.png

参考链接:

https://juejin.cn/post/6844903854065057806

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342