App 的性能监控

本文是<> 第十六篇学习笔记.

通常情况下,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 等等。



    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 功能的代码了,详细解释如下:

  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 设计如下所示:


json-parse
Image Download
"com.apple.trailblazer
"Networking
"Parsing"

"Parsing started SIZE:" ?data-size 


data-size
JSON Data Size
size-in-bytes
?data-size


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


image.png

参考链接:

https://juejin.cn/post/6844903854065057806

你可能感兴趣的:(App 的性能监控)