谈谈iOS app的线上性能监测
在移动端开发者中最重要的KPI应该是崩溃率。当崩溃率稳定下来后,工作的重心就应该转移到性能优化上。那么问题来了,如果你的项目也没有接入任何性能监测SDK,没有量化的指标来衡量,那你说你优化了性能领导信么?
虽然现在市面上第三方性能检测平台已经很成熟,但笔者还是比较建议公司自己写自己的sdk,原因如下
1. 数据安全
2. 避开费用,有的平台是MAU三万以下不收费,超出后费用极高。
3. 可以自定义指标没有无用代码,一接别人SDK包体积增大不少,其实你只用到了个别几个功能。
本文主要内容是给小项目团队自己写性能sdk的建议,也会提到当前的第三方平台。
1.页面的打开速度
关于页面的加载速度的测量有两个指标,一个是页面渲染速度,一个是页面加载速度。
页面渲染速度就是计算一个viewcontroller从viewdidload的第一行到viewdidappear的最后一行所用的时间。 这种方法可以适用于大多数相对静态的页面,一打开就直接加载的。
页面加载速度相对比较麻烦,适用于有些页面并不是一进入就加载而是先发个请求,请求回调后才继续搭建页面的。加载时间应该是用户点开页面到用户能完全的看到内容才算加载完毕,这里就需要算进去请求消耗的时间了,对于源码级别的sdk,我们可以通过每发出一个请求就记到栈里,回调一个消除一个,当栈里的每一个请求都已经回调后才能认定是界面加载完毕。 业界也有一些黑盒的云测平台是录屏解析视频流,认定一个页面从打开到页面稳定后为加载时间。 个人认为肯定没有源码级别的准啊。 (董铂然博客园)
关于这种接入一个sdk直接hook每个页面生命周期方法也简单提一下吧,就是写一个类作为UIViewController的分类,增加几个方法如XXXviewdidload , XXXviewdidappear等,然后使用这种swizzle的做法替换方法的实现
Method viewDidAppear = class_getInstanceMethod([UIViewController class], @selector(viewDidAppear:));
Method XXXViewDidAppear = class_getInstanceMethod([UIViewController class], @selector(XXXViewDidAppear:));
method_exchangeImplementations(viewDidAppear, XXXViewDidAppear);
对于一些新增的计量属性就使用运行时关联对象的那一套吧。 如果觉得要hook的方法太多或是太麻烦或是怕和别的hook冲突,可以使用埋点的方式,直接埋上你需要计算的开始和结束时间的采集点,这种更加灵活只关心自己关心的页面。
2.内存使用值
关于内存和cpu的获取方法业内基本都有统一的代码了,大致如下。
+ (unsigned long)memoryUsage
{
struct task_basic_info info;
mach_msg_type_number_t size = sizeof(info);
kern_return_t kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
if (kr != KERN_SUCCESS) {
return -1;
}
unsigned long memorySize = info.resident_size >> 10;
return memorySize;
}
需要先引入这两个头文件
#include <mach/task_info.h>
#include <mach/mach.h>
获取到了数据存入数组那接下来的事情就是上报策略的制定了。没必要每次获取数据都上报,可以设置每次启动上报上一次session的全部记录就好,启动后隔个10秒或20秒错开请求高峰期。上报时的数据结构也要尽可能的精简,因为不能对用户的流量造成太大的损失,也可以选择先压缩后再上传。
3.CPU占用率
同样需要内存的那两个头文件
+ (CGFloat)cpuUsage
{
thread_array_t thread_list;
mach_msg_type_number_t thread_count;
thread_info_data_t thinfo;
mach_msg_type_number_t thread_info_count;
thread_basic_info_t basic_info_th;
// get threads in the task
kern_return_t kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS) {
return -1;
}
CGFloat tot_cpu = 0;
for (int j = 0; j < thread_count; j++)
{
thread_info_count = THREAD_INFO_MAX;
kr = thread_info(thread_list[j], THREAD_BASIC_INFO,(thread_info_t)thinfo, &thread_info_count);
if (kr != KERN_SUCCESS) {
return -1;
}
basic_info_th = (thread_basic_info_t)thinfo;
if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {
tot_cpu = tot_cpu + basic_info_th->cpu_usage / (CGFloat)TH_USAGE_SCALE * 100.0;
}
} // for each thread
//free mem
kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
assert(kr == KERN_SUCCESS);
return tot_cpu;
}
关于采集数据的频率完全是自己制定的,而且大部分app都是在刚启动不久内cpu占用较大 之后就渐渐区域稳定,所以建议在刚开始采集间隔短一点比如1s,之后采集间隔逐渐加大最后稳定到5分钟获取一次。
4.页面的帧率
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
float fps = _count / delta;
_count = 0;
}
这个测量页面帧率的做法就是通过这个CADisplayLink刷帧方法的调用次数计算的,一般这个方法最快能每秒调用60次,如果是CPU或是GPU某个步骤耗时导致渲染错过了一次垂直信号,那这个方法就不会被调用了,之后统计的帧数也就随之降低了。 上面代码中的fps就是求出的这一时刻的帧率可以塞入数组再稍作处理上传。这里需要注意的是 性能监测平台自己对性能的影响,这个统计帧率的方法可能相对来说要耗性能一些,所以需要控制在某些时刻采集一定的样本就及时暂停。 这个数据建议hook每个页面加载时的方法如viewwillappear,或是tableview滑动时的代理方法,因为卡顿大多发生在这两个场景。 对帧率有兴趣的可以做一个悬浮窗测试帧率,我之前写过一个,但是还没有抽离成组件有兴趣的可以交流下。
5.url响应时间监控
这里普通的做法就是继承NSURLProtocol 这个类写一个子类,然后在子类中实现NSURLConnectionDelegate 的那五个代理方法。
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
// 这个方法里可以做计时的开始
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
// 这里可以得到返回包的总大小
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
// 这里将每次的data累加起来,可以做加载进度圆环之类的
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
// 这里作为结束的时间
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
// 错误的收集
(经过调研connection在升级ipv6后还是可以使用的)你可以得到请求的url,建议只记录host和path,因为吧拼上的参数也上传第一不好快速分类,第二服务端还要做截取处理增加压力,第三浪费流量。 也可以得到response包的length。 响应时间可以根据结束方法中的时间减去开始方法中的时间得到,然后以key-value的方式上传。个人觉得请求没必要与viewcontroller关联,要考虑到管理页面栈还要考虑present和dismiss,收效甚微。并且想知道哪个url属于哪个页面还有很多可执行的方案。
关于webView的url响应时间监控,比普通的native内url要复杂一些。 虽然上面说的统计方法也可以统计到webview的url响应时间,但是只能统计到完整url时间。webView的打开时可能会伴随一些302跳转之类的url,最后才到目标url。 如果一个url经历了三次跳转,响应时间很长,用上面说的统计方法只能统计总时间并不能定位到具体是哪一个子url异常。 这时就需要webView内特殊的时间统计了。
有的一个url会调多个不同团队维护的服务导致了重定向(图中ABC都是完整url的子url),分别在webView的两个代理方法中埋点计算差值,从A走了didstart到B走了didstart这个时间的差值为①就是A所消耗的时间,①+②+③是完整的时间。 通过这种分离统计的方法,可以把慢url明确定位到每一个服务,不对拖累好人。
6.请求错误码统计
如果没有做错误码统计的话,你服务器有问题或者某些地区被运营商劫持之类的,你app的界面可能显示的就是一直转菊花,这时是无法精确定位问题的。 所以将自己的请求都进行编号并收集错误码是非常有必要的。 我们所关注的错误码有三种:
1. 正常的HTTP Response code 比如200 ,404,500
2. AFN(现在ASI用不了了,ipv6强制升级后 应该大部分人都是用AFN3.0了)指定的code 如 -1101, 1102等
3. 你自己请求制定的errorcode。 如 errorcode : 7, message : "您还没有登录" 或 errorcode:10,message:"签名验证失败"
还有很重要一点,就是将自己的所有接口做成一整套映射的编号, 举个例子67对应的是persondetail接口。出现了请求错误,上传的请求错误码应该包含两个部分:接口编号+错误编号。 错误编号建议优先使用自己制定的errcode,其次是AFNcode,最后才是HTTPresponsecode。
“67-10” “35-404” 如果以后上报了类似的错误码,当然比只看到转菊花要好定位的多。
最后提当下的第三方性能监控平台,有听云,OneApm,博睿。 这里就不一一对比每一个平台的优缺点了,因为大体上都是差不多的。
就拿oneapm来说吧,首先ui做的比其他今个平台都要好,让人感觉更专业一点。 接入的方式也是对代码侵入性较小,接一个SDK大小6M左右,加几个framework,然后main函数加一行代码,上传的token是注册后生成的。
我在自己github的一个库里接了这个sdk,后来发现用的人还不少。里面有demo和一些性能监测平台的截图有兴趣可以去看看
https://github.com/dsxNiubility/SXNews
核心功能(这里说一下,我只用了这些平台的移动端功能)也就是包括:UI交互,url响应时间,用户的版本地域系统分布,及crash。 其中崩溃率如果需要统计的话需要手动上传dsym文件。 并且用着用着就遇到了我前面说的问题,有一些我们不需要的功能,并且也有一些我们需要他却没有的功能,比如帧率和webView中的子url响应时间。 优点内就是FE展示时指标内的图表很清晰,并且可以自定义定制仪表盘。这是一个小公司自己写sdk前期难以达到的。 还有一点就是可配置,在设置页面可以自行选择自己所关注的指标。 然后程序每次启动都会先有一个GET请求获取配置,哪些打开哪些关闭,然后测量和上传我们需要的(这里有一点可疑的是,他是真没测量还是全都测量上传了,只是在控制台里不给你展示罢了),我们自写的sdk做个可配置也是必要的。
本文是作者一段时间内的经验,希望能对你有所帮助,观点不同欢迎讨论。