基于iOS平台的性能检测方案

简介: 导语 在开发过程中,功能不仅要满足业务需求,也要关注功能对App性能带来的一些问题。开发人员在开发阶段检测性能比较容易,iOS端可以直接通过instruments工具进行检测。但是在测试阶段,测试人员要检测性能需要下载开发工具成本比较高。

导语

在开发过程中,功能不仅要满足业务需求,也要关注功能对App性能带来的一些问题。开发人员在开发阶段检测性能比较容易,iOS端可以直接通过instruments工具进行检测。但是在测试阶段,测试人员要检测性能需要下载开发工具成本比较高。如果客户端能够将性能数据上传到服务端并且通过一些界面进行展示,对测试人员来说是一种可以检测性能的比较好的方法。

本文章主要介绍iOS端如何通过代码采集性能数据,其中包括电池数据,CPU数据,内存数据,卡顿数据,流量数据以及冷启动时间等。

电池数据

首先来看一下电池数据,iOS电池数据采集方案主要有以下三种方案:UIDevice,IOKit,越狱。

1、UIDevice:提供了获取设备电池的相关信息,包括当前电池的状态以及电量。获取电池信息之前需要先将 batteryMonitoringEnabled 属性设置为 YES,然后就可以通过 batteryState 和 batteryLevel 获取电池信息。

优点:api简单,易于使用。
缺点:粗粒度,能够采集到的数据较少,不符合需求。

2、IOKit: 是一个iOS 系统的私有框架,它可以被用来获取硬件和设备的详细信息,也是与硬件和内核服务通信的底层框架。通过它可以获取设备电量信息,精确度达到1%。

优点:可以获取较多的电池相关的数据。
缺点:因为要访问私有api,不能通过苹果审核,只能在线下取值。获取到的值是设备的电池数据,无法达到应用级别的数据获取。

3、越狱方案:通过iOSDiagnosticsSupport 私有库,Runtime 拿到 MBSDevice 实例,获取电量日志信息表,日志信息表中包含了 iOS 系统采集的小时级别的耗电量。

优点:可以获取到应用的耗电量。
缺点:获取到的耗电量是以小时为单位的,时间间隔太长,不符合需求。

最后,为了能够采集更多的电池数据,我们选择的方案是通过访问IOKit的私有api获取数据,并且在提交到app store时将这部门代码从包里移除掉,以免影响app的审核结果。

核心代码如下:
1

通过以上方式可以获取到的数据包括但不限于:

当前充电状态
电量
是否连接USB(支持iOS10以下系统)
是否有电池
最大值
电压
温度(支持iOS10以下系统)

CPU数据

iOS的线程技术是基于Mach 线程技术实现的,在 Mach 层中thread_basic_info 结构体提供了线程的基本信息,并且每个线程中包含线程的cpu_usage。获取当前App的占用率就是所有线程的cpu_usage之和。
2

通常一个 task 包含多个线程,在内核提供了 task_threads API 调用获取指定 task 的线程列表以及线程个数,也就是target_task 任务中的所有线程保存在 act_list 数组中,数组中包含 act_listCnt 个条目。然后可以通过 thread_info API 调用来查询指定线程的信息。
3

因此,获取当前APP的CPU占用率需要遍历所有线程,将cpu_usage求和。

接下来就是获取当前设备的CPU总占用率。 iOS中CPU状态一般包括CPU_STATE_USER, CPU_STATE_SYSTEM, CPU_STATE_IDLE 和 CPU_STATE_NICE等四种。

1、CPU_STATE_USER:运行在用户态空间或者说是用户进程。
2、CPU_STATE_SYSTEM:在内核空间运行的分配内存、IO操作、创建子进程……等。
3、CPU_STATE_IDLE:空闲状态。
4、CPU_STATE_NICE:用户空间进程的CPU的调度优先级。

因此,除了空闲状态都属于CPU占用状态,因此当前CPU的总使用率为(用户+系统+调度)/(用户+系统+调度+空闲)。通过host_statistics获取host_cpu_load_info结构体数据,该结构体中 cpu_ticks 包含了 CPU 运行时四种不同该状态的时钟脉冲的数量,并且根据这四个不同状态的时间脉冲,计算出CPU的总占用率。

内存数据

获取内存数据同样也可以通过mach_task_basic_info结构获取resident_size值作为当前App已占用的内存大小。
4

但是在测试中发现,通过该结构体获取的值与Xcode中的内存数据对不上,往往差好几兆甚至好几十兆。因此通过查找资料,有一篇文章介绍通过逆向Xcode来获取Xcode计算内存方法以及结构体。该方法获取到的已占用内存大小与Xcode的值几乎一致,可以作为一个判断标准。具体参考代码如下:
5

接下来,如果要获取当前设备可以使用的空闲内存,首先要了解iOS系统的内存分配。
6

Free Memory:未使用的 RAM 容量,随时可以被应用分配使用。

Wired Memory:用来存放内核代码和数据结构,它主要为内核服务,如负责网络、文件系统之类的;对于应用、framework、一些用户级别的软件是没办法分配此内存的。但是应用程序也会对 Wired Memory 的分配有所影响。

Active Memory:活跃的内存,正在被使用或很短时间内被使用过。
Inactive Memory:最近被使用过,但是目前处于不活跃状态。
Purgeable Memory:可以理解为可释放的内存,主要是大对象或大内存块才可以使用的内存,此内存会在内存紧张的时候自动释放掉。

因此,空闲内存看成总内存大小减去 Wired Memory大小,Active Memory大小以及Inactive Memory大小。在32位系统通过这种方式获取空闲内存与Xcode数据作比较误差范围较小,而在64位系统上的数据与Xcode数据一比较误差较大,同样找到一个逆向Xcode获取Xcode的计算内存方法。64位系统获取空闲内存的具体代码如下:
7

卡顿数据

检测卡顿数据的方式通常有两种:一种是FPS卡顿检测,另一种是主线程卡顿检测。

FPS卡顿检测:检测当前页面的帧率,帧率越高意味着界面越流畅,通过计算丢帧率来检测当前页面的卡顿情况。
主线程卡顿检测:通过开辟一个子线程来监控主线程的RunLoop,当两个状态区域之间的耗时大于阈值时,就记为发生一次卡顿。

一、FPS卡顿检测

目前我们要采集的方主要是基于CADisplayLink以屏幕刷新频率同步绘图的特性,观察屏幕当前帧数的指示器,若帧率少于指定的帧率看成一个FPS卡顿。具体代码如下:
8

二、主线程卡顿检测

在主线程在Runloop的某个阶段进行长时间的耗时操作,因此主要思路就是开辟一个子线程去计算kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting两个状态区域之间的耗时是否超过某个阀值来断定主线程的卡顿情况。

那么,为什么要用kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting状态进行判定呢?首先要理清楚Runloop的运行机制,以下为RunLoop 顺序:
9

看完RunLoop顺序,就可以看到处理事件主要有两个时间段 — kCFRunLoopBeforeSources 发送之后与 kCFRunLoopAfterWaiting 发送之后。dispatch_semaphore_t 是一个信号量机制,信号量到达或者超时会继续向下进行。若超时则返回的结果必定不为0,若信号量到达返回的结果为0。利用这个特性我们判断卡顿出现的条件为在信号量发送 kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting后进行了大量的操作,在一段时间内没有再发送信号量,看成超时。也就是主线程长时间的停留在这两个状态上。转换为代码就是判断有没有超时,若超时了,再判断当前停留的状态是不是这两个状态,如果是,就判定为卡顿。具体参考代码如下:
10

流量数据

流量数据主要统计在当前App内发生的所有网络请求相应的数据大小。首先,先通过facebook提供的sonar框架捕捉app内的所有request和response。在实际的网络请求中 Request 和 Response 不一定是成对的,如果网络断开、或者突然关闭进程,都会导致不成对现象,如果将 Request 和 Response 记录在同一条数据,将会对统计造成偏差。因此request和response分开统计流量。若有对SonarKit框架感兴趣的同学可以直接访问官网进一步了解,官网:https://fbsonar.com/docs/getting-started.html

一、统计Request流量

首先需要了解请求报文的组成,如图:
11

那么,Request所花费的流量就是将把Line的大小,Header的大小,空格以及Body大小累加的合。

1、Line大小的统计

Line没有可以直接转换成CFNetwork相关数据的私有接口,但是我们很清楚 HTTP 请求报文 Line 部分的组成,因此可以手动计算Line的大小。
12

2、Header大小统计

通过request.allHTTPHeaderFields 拿到的头部数据是有很多缺失的,并不是完整的数据。同时由于无法直接转换到 CFNetwork 层,所以一直拿不到完整的 Header 数据。缺少的数包括但不限于以下几个字段:Accept,Connection,Host,当前Request的Cookie。由于基本上缺失的都是固定的几个字段,忽略这几个字段对统计的结果影响不大。因此主要针对cookie的数据并且手动大小进行补全。因此总Header的大小可以看成request.allHTTPHeaderFields数据大小加上cookie大小。

3、Body大小统计

最后是body部分,通过resquest.HTTPBody来计算Body大小。这里要注意的地方就是通过 NSURLConnection 发出的网络请求 resquest.HTTPBody 拿到的是 nil。需要通过 HTTPBodyStream 读取 stream 来获取 request 的 Body 大小。

最后,将Line大小,Header大小,Body大小相加就是当前request所话费的流量。

二、统计Response流量

请求报文的组成如下:
13

那么Response所花费的流量就是将把Status Line的大小,Header的大小,空格以及Body大小累加的合。

1、StatusLine大小

NSURLResponse没有接口能直接获取报文中的 Status Line。因此,最后通过转换到 CFNetwork 相关类拿到了Status Line 的数据后计算它的大小,这其中可能涉及到了读取私有 API,因此需要注意审核问题。

2、Header大小

通过 httpResponse.allHeaderFields拿到 Header 字典,转换成 NSData 计算大小。

3、Body大小

对于 Body 的计算,采用 expectedContentLength 或者去 NSURLResponse 对象的 allHeaderFields 中获取 Content-Length 值,其实都不够准确。Content-Length 只是表示 Body 部分的大小,因此采取直接获取body大小的方式。还有一个需要注意对 gzip 情况进行区别分析。我们知道 HTTP 请求中,客户端在发送请求的时候会带上 Accept-Encoding,这个字段的值将会告知服务器客户端能够理解的内容压缩算法。而服务器进行相应时,会在 Response 中添加 Content-Encoding 告知客户端选中的压缩算法。若Content-Encoding使用了 gzip,则模拟一次 gzip 压缩,再计算字节大小。

冷启动时间

App的冷启动就是,当应用启动时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用, 这个启动方式就叫做冷启动(后台不存在该应用进程)。下面先看看苹果官方文档给的应用的启动时序图,图中可以看到冷启动是一个User taps app icon到Final initialization(applicationDidFinishLaunching: withOptions:)的过程,所以冷启动时间就是从用户唤醒App开始一直到App已启动所消耗的时间。
14

因此,冷启动时间 =DidLauching时间 - main()函数执行之前的时间。类的+ load方法在main函数执行之前调用,所以我们采取在+ load方法记录开始时间的方案。具体参考代码如下:
15

当applicationDidFinishLaunching:withOptions:方法执行完毕后,添加一个回调获取AppDidFinishLaunching后的时间。并且将开始时间与load开始时间相减作为应用冷启动时间。

总结

以上介绍了iOS中通过代码采集性能数据的方案,目前还在继续优化采集方案,希望本文章能够帮助大家对iOS性能数据采集的了解。

参考文章:

https://fbsonar.com/docs/getting-started.html

http://www.cocoachina.com/ios/20170629/19680.html

http://www.cocoachina.com/ios/20180606/23691.html

https://cloud.tencent.com/developer/article/1006222

https://www.jianshu.com/p/6c10ca55d343

http://ddrccw.github.io/2017/12/30/2017-12-30-reverse-xcode-with-lldb-and-hopper-disassembler/

https://www.jianshu.com/p/8e764d05275b?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

相关文章
|
17天前
|
安全 Android开发 iOS开发
探索安卓与iOS开发的差异:平台特性与用户体验的深度对比
在移动应用开发的广阔天地中,安卓和iOS两大平台各占半壁江山。本文旨在通过数据驱动的分析方法,深入探讨这两大操作系统在开发环境、用户界面设计及市场表现等方面的差异。引用最新的行业报告和科研数据,结合技术专家的观点,本文将提供对开发者和市场分析师均有价值的洞见。
|
7天前
|
Java Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【7月更文挑战第8天】在移动应用开发的广阔天地中,Android与iOS两大平台各自占据着半壁江山。本文将深入探讨这两个平台在开发环境、用户界面设计、性能优化以及市场覆盖等方面的根本差异,并分析这些差异如何影响项目的成功。通过比较和分析,旨在为开发者在选择平台时提供更全面的视角,帮助他们根据项目需求和目标市场做出更明智的决策。
|
18天前
|
传感器 安全 Android开发
探索iOS与安卓应用开发的性能差异
在移动操作系统领域,iOS和安卓的较量从未停歇。本文将深入探讨两大平台在应用开发中的性能表现,揭示它们各自的优势与局限。通过对比分析,我们将理解开发者如何在这两个不同的生态系统中做出权衡,以及这些选择如何影响最终用户的体验。
9 0
|
20天前
|
Java 开发工具 Android开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
在移动应用开发的广阔天地中,Android和iOS两大平台各自占据着半壁江山。本文将深入探讨这两个平台在开发过程中的关键差异点,包括编程语言、开发工具、用户界面设计、性能优化以及市场覆盖等方面。通过对这些关键因素的比较分析,旨在为开发者提供一个清晰的指南,帮助他们根据项目需求和目标受众做出明智的平台选择。
|
1天前
|
开发工具 Android开发 Swift
安卓与iOS开发环境对比:选择适合你的平台
【7月更文挑战第14天】在移动应用开发的广阔天地中,安卓和iOS两大平台各领风骚。本文将深入探讨这两个系统在开发环境上的差异,从编程语言、工具到生态系统的多维度比较,旨在为开发者提供一份实用的参考指南。无论你是初涉移动开发的新手,还是寻求跨平台解决方案的老手,这篇文章都将助你一臂之力,找到最适合你的开发路径。
|
1天前
|
IDE 开发工具 Android开发
安卓与iOS开发环境对比:选择合适的平台
【7月更文挑战第14天】本文深入探讨了安卓与iOS开发环境的差异,并分析了选择合适平台的考量因素。通过比较两者的开发工具、编程语言和用户群体,旨在帮助开发者做出明智的决策,以优化资源利用和提升应用性能。文章将提供实用的建议,助力开发者在竞争激烈的应用市场中脱颖而出。
8 2
|
4天前
|
IDE 开发工具 Android开发
安卓与iOS开发环境对比:选择适合你的平台
【7月更文挑战第11天】在移动应用开发的广阔天地中,安卓与iOS两大平台各据一方。它们不仅在用户群体上有所区别,更在开发环境、工具和生态系统上展现出独特的特色。本文将深入探讨这两个平台的开发环境差异,帮助开发者根据个人技能、项目需求及市场定位做出明智的选择。
16 3
|
7天前
|
移动开发 开发工具 Android开发
探索安卓与iOS开发的差异:平台特性与编程实践
【7月更文挑战第8天】在移动开发的广阔天地中,安卓和iOS这两大操作系统各自占据着半壁江山。它们在用户界面设计、系统架构及开发工具上展现出截然不同的特色。本文将深入探讨这两个平台在技术实现和开发生态上的关键差异,并分享一些实用的开发技巧,旨在为跨平台开发者提供有价值的见解和建议。
|
19天前
|
监控 Android开发 iOS开发
探索Android与iOS开发的差异:平台、工具和用户体验的比较
【6月更文挑战第25天】在移动应用开发的广阔天地中,Android和iOS两大平台各领风骚,它们在开发环境、工具选择及用户体验设计上展现出独特的风貌。本文将深入探讨这两个操作系统在技术实现、市场定位和用户交互方面的关键差异,旨在为开发者提供一个全景式的视图,帮助他们在面对项目决策时能够更加明智地选择适合自己项目需求的平台。
|
26天前
|
Java Android开发 Swift
探索Android与iOS开发的差异:平台选择对项目成功的影响
【6月更文挑战第18天】在移动应用开发的广阔天地中,Android和iOS两大平台各据一方,它们在市场份额、用户群体及开发环境上各有千秋。本文将深入分析这两个操作系统的开发差异,探讨如何根据项目需求选择合适的平台,并讨论跨平台解决方案的可行性与挑战。我们将通过实际案例,揭示平台选择对项目成功的关键性影响,为开发者提供决策支持。