美文网首页
App 的性能监控

App 的性能监控

作者: forping | 来源:发表于2021-01-13 09:47 被阅读0次

    本文是<<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

    相关文章

      网友评论

          本文标题:App 的性能监控

          本文链接:https://www.haomeiwen.com/subject/gwidaktx.html