背景
在一次组内会议中, 被分配到了这样一个技术研究需求, 目的是通过检测页面加载耗时, 来对页面进行针对性的优化. 拿到这个任务之后, 立马去搜集了一些网上现有的资料, 并作出了一些总结.
目前实现检测的几种方式
基本思路
通常是利用 swizlling 在 viewDidLoad 方法里保存一个初始时间, 然后在 viewDidAppear 里得到页面出现的时间.
- @implementation UIViewController (LoadTime)
- + (void)load
- {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- swizzleMethod([UIViewController class], @selector(viewDidLoad), @selector(my_viewDidLoad));
- });
- swizzleMethod([UIViewController class], @selector(viewDidAppear), @selector(my_viewDidAppear:));
- });
- }
- - (void)my_viewDidLoad
- {
- NSDate *date = [NSDate date];
- // 保存开始时间
- _date = date;
- [self my_viewDidLoad];
- }
- - (void)my_viewDidAppear:(BOOL)animated{
- [self my_viewDidAppear:animated];
- // 得到加载时间
- NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:_date];
- NSLog(@"Page %@ cost %g to be appeared", [self class], duration);
- }
- @end
一种基于 KVO 的页面加载时间获取(作者五子棋)
原文在这里 http://satanwoo.github.io/2017/11/27/KVO-Swizzle/
这篇博客指出了上述方法的问题, 你 hook 的其实是父类 UIViewController 的方法, 子类其实是调用了 [super xxxxxx] 方法, 这种处理方式没办法对每种页面都进行处理, 需要各自建立对应分类. 于是作者突发奇想, 利用 KVO 拿到派生的子类进行 IMP 的替换, 从而解决了这个问题. 以上两种方式只能得出代码加载时间, 如果某些页面和网络有关, 网络请求这部分时间就很难拿到了.
「无侵入页面加载完成检测」的一些思路(作者 Limboy)
原文在这里
这个方法是我完全没有想到的一种处理方式. 利用图像纯色占比来判断当前页面是否是加载完成. 简单来说, 就是开启一个 CADisplayLink 定时器, 对当前页面进行截图, 然后利用计算纯色占比的算法算出比例, 当比例大于某一个阈值, 就说明页面已经加载成功了. 这种方法我觉得是最直观的方法, 但作者也列举了一些问题:
1. 需要主动去截屏检测, 而不能加载完成后告知. 这其中的差别在于无法得知具体哪个时间加载完成了.
2. 有些页面被故意设计成有较多留白, 这时就不容易判断了.
3.「未加载完成」不同的页面会有不同的表现.
4. 当用户滑动时, 有可能之前的页面已经加载了
美团 Hertz 的思路
原文在这里 https://tech.meituan.com/hertz.html
这篇文章介绍了美团关于性能监控的一些措施, 也提到了 iOS 中页面加载时间检测的方式: 在 iOS 中我们采取了不同的做法, Hertz 在配置文件中指定最终渲染页面的某个元素的 tag, 并在网络请求成功后开启 CADisplayLink 检查该元素是否出现在根节点下面.
总结下来的三个问题
问题 1: 即使解决了无法直接 hook 子类的实现, 但是也不能得到确切的加载时间如下面的例子:
- - (void)viewDidLoad {
- [super viewDidLoad];
- self.view.backgroundColor = [UIColor whiteColor];
- // 模拟了一个异步网络请求
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- sleep(3);
- dispatch_async(dispatch_get_main_queue(), ^{
- TempView *tView = [[TempView alloc] init];
- tView.frame = CGRectMake(10, 20, 300, 200);
- tView.backgroundColor = [UIColor orangeColor];
- [self.view addSubview:tView];
- });
- });
- }
在这个例子中, 用户所感受到的加载完成应该是 tView 在网络请求之后显示的时间. 单纯的 hook 生命周期方法是无法获取网络延迟这段时间的.
问题 2: 如果开启定时器进行页面检测截图, 消耗内存的同时也会影响页面渲染, 可能会造成性能问题.
问题 3: 美团这种思路是可行的, 但是这个 tag 该怎么打呢, 如果是一个纯 tableView 的控制器显示该怎么判定呢, webview 呢?.
新的思路
这里我借鉴了美团的思路, 但又有所不同.
我们先弄清楚 2 个问题:
1. 我们检测页面加载时长的目的主要是为了检测某些页面从加载到显示的时间, 通常页面的出现除了页面自身的渲染出现, 还伴随着接口数据的刷新, 有些界面依赖网络请求, 有些界面依赖本地读取或者直接显示静态页面.
2. 并不是所有的界面都需要进行检测, 我们应该把监测重点放到一些用户常用的界面上, 当然, 能覆盖越多越能发现更多可能的问题.
所以我们只需要检测某个控制器中的某个关键子 view 出现, 就可以确定这个时间. 那我们怎么判断这些子页面真的显示呢?
如果该页面主要子 view 是 UITableView 或者 UICollectionView, 那么可以指定找到这个页面的这两种子类, 通过 visibleCells 是否大于 0 来判断.
如果该页面主要子 view 的类型是其他 view, 那么根据这个 view 是否出现在屏幕上来判断.
如果该页面主要子 view 类型是 webview, 那么可以根据 webview 是否加载 url 并且是否 loading 来判断(有更好的方式可以告诉我).
知道如何判断页面显示了, 那么我们需要一个配置文件, 来指定那些页面关键子 view 的类型和其它属性. 为了能够灵活配置, 建议通过后台接口下发一个 JSON, 当然也可以本地配置一个 dictionary. 我这里的文件格式如下:
- /*
- TargetSubview: 关键子 view
- TargetSubviewType: 子 view 的类型 0:UITableView/UICollectionView 1:NormalView 2:Webview
- TargetEmptyViewType: 可能会有的空白 view 类型
- */
- @"ViewController":@{
- @"TargetSubview" : @"UITableView",
- @"TargetSubviewType" : @(0),
- @"TargetEmptyViewType":@"NoDataView"
- },
- @"TempViewController":@{
- @"TargetSubview" : @"TempView",
- @"TargetSubviewType" : @(1),
- @"TargetEmptyViewType":@"NoDataView"
- },
- @"TempWebviewController":@{
- @"TargetSubview" : @"WKWebView",
- @"TargetSubviewType" : @(2),
- @"TargetEmptyViewType":@"NoDataView"
- }
然后我们就可以 hook UIViewController 的 viewDidLoad 方法, 拿到初始时间, 同时开启一个 CADisplayLink 定时器进行检测.
在定时器的方法里, 我们就开始根据需要检测的页面, 找到目标的子 view, 然后根据 view 类型进行相应的判断即可. 如果符合判断条件, 就可以进行上报了.
注意点:
1. 对于空白页的处理, 需要考虑多种情况, 例如是直接加在关键子 view 里还是加在控制器中.
2. 启动的广告页是否对首页加载有影响.
3. 这里遍历子控件的时候, 注意子控件层次不能太深, 最好是一层, 不然可能超过 16.7ms, 造成误报, 这种情况是一个比较蛋疼的点, 需要我们去控制子 view 的层级, 但是为了更精准的获得加载时间, 这一步也很值得, 这也是说为啥要用后台接口去控制, 就是为了在业务发生变化之后能灵活调整 view 的层级.
以上就是我的检测思路, 并且项目中已经运行了几个版本, 中间也发现了不少问题, 并得以解决. 这里有一个比较简单的 demo, 可以让大家了解一下, 有啥问题, 欢迎指正!
来源: https://juejin.im/post/5c6e7870f265da2ddb296245