iOS性能指标及SDK实施方案

4,181 阅读11分钟
  • SDK分为Monitor性能监测数据获取和Exception异常错误获取两部分。具体实施测试机会先在线下检控阶段根据理论值经验+开发工具的检测进行一波针对的优化。设定好Exception理论错误阈值。
  • Monitor性能监测获取会将用户页面性能数据进行收集保存为本地文件,打开app的时候进行监测文件上报记录,解析文件,按机型和系统统计帧率/内存/CPU阈值报警数据。
  • Exception异常错误获取包括:SignalException崩溃异常,UncaughtException崩溃异常,FpsException帧率卡顿异常,cpuException使用率超限,viewDurationException页面视图展示出的时间过长,networkException网络接口数据异常。
  • Monitor性能监测采集分析指标作为专项优化的性能基准点。后期也可以利用采集到的大量指标数据利用机器学习知识,通过iOS的CordeML框架加载机器学习训练的模型输出Exception需要的合理阈值。

1.卡顿--SDK线上性能监控

1.1 页面帧率监控

  • iOS系统中顺滑屏幕刷新率为60Hz(60次每秒)。多次掉帧或大幅度偏离16.7ms的刷新值,即可看作是发生了卡顿。基于CADisplayLink实现FPS监控指示器。
  • 每次页面刷新时会发出一个屏幕刷新信号,CADisplayLink允许我们注册一个与刷新信号同步的回调处理。
  • CADisplayLink以屏幕刷新的频率调用指定selector,也就是说每次屏幕刷新的时候就调用selector,在selector方法里面统计每秒这个方法执行的次数,通过次数/时间就可以得出当前屏幕的刷新率了。
  • 如果出现频繁跳帧或帧率持续过低说明流畅度存在问题。

1.2页面卡死监控监测

  • 结合帧率变化监测数据和 **4.1 **CPU使用情况

2.耗时--SDK线上性能监控

2.1 APP启动时间监测

  • 冷启动 :App 总冷启动时间 = t1( main()之前的加载时间 ) + t2( main()之后的加载时间 )。

t1 = 系统的 dylib (动态链接库)和 App 可执行文件的加载时间;

t2 = main()函数执行之后到AppDelegate类中的applicationDidFinishLaunching方法执行结束前这段时间。

t1这部分时间开发阶段会通过xcode的Run → Environment Variables 进行采集。

t2这部分时间会代码注入进行计算开发和线上持续监测上报。

  • 热启动:会对app的生命周期函数进行监测。applicationWillEnterForeground(将进入前台)~applicationDidBecomeActive(已经变成前台)之间的计算时间进行持续监测上报。

2.2 页面启动时间监测

方案一:

hook打开页面方法操作,注入:异步线程开定时器,每100ms截图一次,对视图进行分析,判断是否白屏或预加载屏,直到非白屏或到达时间阈值,记录区间加载时间。

方案二

通过观察者的模式系统会生成一个NSKVO_Controller类,然后hook当前页面开启方法viewDidLoad,记录当前页面创建控件所需要时间。

2.3 页面展示出网络数据加载时间监测

方案一:

hook网络数据页面startLoading方法,注入:异步线程开定时器,每100ms截图一次,对视图进行分析,判断是否有loading图和页面是否白屏或预加载屏,直到页面上视图分析存在文字结束或到达时间阈值,记录区间加载时间。

方案二:

  • 页面上如果出现tableViewcollectionView控件,可以使用hook代理方法cell将要出现的方法(tableView:willDisplayCell:forRowAtIndexPath:),在这里做时间统计。
  • 如果界面不包含tableViewcollectionView控件,可以在基类中定义一个创建视图的基类方法(createUI),通过hook这个基类方法来实现时间统计。(针对项目改动比较大)

3.发热耗电--SDK线上性能监控

3.1 CPU使用率监测

  • 在进程运行时,每个线程对 CPU 的使用率不同。各个线程对 CPU 使用率的总和,即可视为当前 App对CPU的占用率。
  • 基于Mach内核底层类,获取app启动运行时长的底层原理其实就是对Mach内核底层类进行操作的。Mach中暴露可知每个线程中都存在thread_basic_info结构体,里面存储的就有CPU使用率。通过对底层代码进行操作获取到Mach task,进而可以获取到线程信息thread_basic_info结构体。
  • 这样实时统计总CPU使用率,高于百分之六十?总量异常时,提取出高CPU使用的线程数据和页面。

3.2 网络流量监测

  • NSURLProtocol: 采用继承抽象类NSURLProtocol对发起的网络请求进行拦截,initWithRequestdidReceiveResponsedidReceiveData方法记录关键属性,从而作为依据获取下行流量数据。NSURLProtocol方式要编写一定的代码量来实现,而且NSURLProtocol并不能拦截所有形式的请求,要对NSURLSession发起的shareSession形式单独处理。
  • NetworkExtension:在以后的业务发展和机型占比上,如果我们的app可以允许放弃iOS9以下的用户,那么可以在官方NetworkExtension库里接管所有网络请求来获取数据。

4.内存--线下性能检控

4.1 内存泄漏检控

  • 内存泄漏的产生属于固定代码编写方式层面。

4.2 爆内存监控

  • 开发过程中会借助开发工具观察记录页面产生的内存变化
  • 代码方案尝试Hook内存分配方法,按页面记录并抓取内存分配的堆栈和内存大小。
  • 分析产生的页面的对象, 然后对可能存在的地方和循环积累大量对象的地方,手动实现autoReleasePool自动释放池。

4.3 IO量检测

  • 我们的app对于本地存储文件的读写应用场景不多,所以这里产生的性能问题应该很小。
  • 使用Instruments工具System UsageFile Activity和Network。统计出运行状态下应用的文件和网络IO操作数据。

5.项目瘦身--线下性能检控

  • 使用fui(Find Unused Imports)定期扫描工程中不用的类
  • APPCode定期检查工程中无用的代码
  • 压缩文件资源管理

6.存储获取数据

6.1 Monitor监测统计存储

  • 把监测到的数据存到json文件,文件以机型+系统版本+app名称+app版本命名。每一个 app的生命周期算作一个统计数据文件,保存到沙盒。gzip的格式压缩上传,每次启动APP,后台线程上传到后台服务,每次最大10个文件。上传失败就保存到本地等待下次启动上传。
  • 文件内容包含对每个页面帧率Fps, Cpu使用率, 页面展示数据的时间进行监测的数据。以页面名称为key,value为数组,内部包含:
    • keepTime页面停留持续时间,openTimes页面打开次数。
    • 这个页面的fps_max最大值,fps_min最小值,fps_avgs质量值(字符串拼接,前30s,fps的每秒变化值), fps_tota生命周期内页面fps的和。
    • cpu_max使用率最大值,cpu_min使用率最小值,cpu_avgs使用率质量值(字符串拼接,前30s,cpu的每秒变化值),cpu_total生命周期内页面cpu的和。
    • viewDuration_max页面展示启动时间的最大值,viewDuration_min页面展示启动时间的最小值,viewDuration_avgs页面启动时间的质量值(字符串拼接,每一次打开页面的时间)
  • 文件内容包含对APP信息的监测,内部包含dev_id设备ID,dev_n设备机型,system_v设备系统, app_napp名称,app_vapp版本,network_type网络类型,time启动时间戳,app_launchDuration冷启动时间,热启动时间(字符串拼接)。

3.5.2_1585536369.json 统计文件内容如下:

{
  "APP" : {
    "dev_id" : "03715FB2-0C31-4C65-9AE7-92A4A5E8DCEF",
    "system_v" : "12.3.1",
    "app_launchDuration" : "4.804108",
    "dev_n" : "iPhone9,2",
    "time" : "1585536369",
    "app_n" : "XXX",
    "app_hotLoadDuration" : "0.297370,0.297456",
    "app_v" : "3.5.2",
    "network_type" : "WIFI"
  },
  "page" : {
    "ECProductDetailController" : {
      "cpu_min" : "3",
      "viewDuration_max" : "0.733516",
      "keepTime" : "30",
      "fps_min" : "40",
      "viewDuration_total" : "1.297453",
      "cpu_total" : "600",
      "cpu_avgs" : "86,46,33,25,10,9,11,11,7,6,37,10,41,22,12,15,13,11,8,7,6,23,6,8,3,17,66,30,9,12",
      "fps_avgs" : "43,58,60,60,60,60,59,60,60,60,50,60,50,60,60,60,60,60,60,40,60,58,60,55,45,59,60,60,60,58",
      "viewDuration_min" : "0.251687",
      "cpu_max" : "86",
      "fps_max" : "60",
      "viewDuration_avgs" : "0.733516,0.251687,0.312250",
      "fps_total" : "1715",
      "openTimes" : "3"
    },
    "ECHomePageController" : {
      "cpu_min" : "2",
      "viewDuration_max" : "2.586971",
      "keepTime" : "14",
      "fps_min" : "46",
      "viewDuration_total" : "2.586971",
      "cpu_total" : "321",
      "cpu_avgs" : "9,2,50,34,11,28,34,44,15,28,16,13,16,21",
      "fps_avgs" : "59,60,48,58,60,60,46,55,60,58,59,57,60,59",
      "viewDuration_min" : "2.586971",
      "cpu_max" : "50",
      "fps_max" : "60",
      "viewDuration_avgs" : "2.586971",
      "fps_total" : "799",
      "openTimes" : "1"
    },
    "FLBFlutterViewContainer" : {
      "cpu_min" : "5",
      "cpu_avgs" : "56,69,43,5,5,10",
      "fps_avgs" : "41,35,60,60,60,60",
      "keepTime" : "6",
      "fps_min" : "35",
      "cpu_max" : "69",
      "fps_total" : "316",
      "cpu_total" : "188",
      "fps_max" : "60"
    }
  }
}

6.2 Exception异常错误存储

以txt文件的形式存储下来,内部包含dev_id设备ID,dev_n设备机型,system_v设备系统, app_n名称,app_v版本,network_type网络类型,time启动时间戳,name页面名称,reason产生错误的具体值(如fpscpu)。Exception错误类型ErrorType,堆栈信息等。可以接入钉钉Webhook报警和根据业务上传自己的后台建立系统。

  • SignalException崩溃异常
  • UncaughtException崩溃异常
  • FpsException帧率卡顿异常(帧率持续3秒左右均小于设置阈值40以下,保存错误记录,发送错误报警)
  • cpuException使用率超限( 帧率持续3秒左右均大于设置阈值60,保存错误记录,发送错误报警)
  • viewDurationException页面视图展示出的时间过长
  • networkException网络接口数据异常(http响应code非200或失败)

例: 3.5.2_1585537822.json 异常错误报告内容如下:

 <b>发送异常错误报告</b>

<b>app_n:</b>XXX
<b>app_v:</b>3.5.2
<b>system_v:</b>12.3.1
<b>dev_n:</b>iPhone9,2
<b>dev_id:</b>03715FB2-0C31-4C65-9AE7-92A4A5E8DCEF
<b>network_type:</b>WIFI
<b>time:</b>1585537822
<b>ErrorType:</b>SignalException

<b>callStackSymbols:</b>
0   enjoyChanging                       0x000000010162af08 SignalExceptionHandler + 140
1   enjoyChanging                       0x000000010170ff28 BLYBSDSignalHandlerCallback + 256
2   libsystem_platform.dylib            0x000000018eba19ec <redacted> + 56
3   libsystem_pthread.dylib             0x000000018eba7094 <redacted> + 380
4   libsystem_c.dylib                   0x000000018ea87ea8 abort + 140
5   libc++abi.dylib                     0x000000018e154788 __cxa_bad_cast + 0
6   libc++abi.dylib                     0x000000018e154934 <redacted> + 0
7   libobjc.A.dylib                     0x000000018e16be00 <redacted> + 124
8   enjoyChanging                       0x0000000101733358 _ZL24BLYCPPExceptionTerminatev + 1932
9   libc++abi.dylib                     0x000000018e160838 <redacted> + 16
10  libc++abi.dylib                     0x000000018e160434 __cxa_rethrow + 144
11  libobjc.A.dylib                     0x000000018e16bbc8 objc_exception_rethrow + 44
12  CoreFoundation                      0x000000018ef1d11c CFRunLoopRunSpecific + 544
13  GraphicsServices                    0x000000019111d79c GSEventRunModal + 104
14  UIKitCore                           0x00000001bb8cb978 UIApplicationMain + 212
15  enjoyChanging                       0x0000000100e4a7e0 main + 160
16  libdyld.dylib                       0x000000018e9e28e0 <redacted> + 4