对页面的性能衡量很重要的一个指标就是页面的加载时间
如何定义页面的加载时间 只关注 UIViewController 的 viewDidLoad、viewWillAppear、viewDidLayoutSubviews 等生命周期去衡量一个页面真实的加载时间明显不够准确。
因为一旦我们需要关注一个页面的加载时间,那势必会涉及 tableView 的 reload。如下图所示
一个非常主观的感知:当 frame 和层级不再发生变动了,我们就认定这个页面已经加载完了。 ps:这里有个缺陷,如果 vc 中有动来动去的子 view 就会被认定为不稳定。 我认为这样定义页面的加载时间可能更加符合用户体验。
以 viewDidLoad 调用之前的一瞬间记为开始 当视图结构稳定记为结束
怎么判断视图结构稳定 在这里做了一个大胆的近似计算如果当某次布局的开始距离上次布局的结束
在每次 layout 之后触发一个「计算页面加载」的行为,并且延迟1秒执行。这里的1秒就是需要进行调整的阈值,
1 2 3 4 5 6 7 8 calculateBlock = dispatch_block_create(0, ^{ JFTVCLoadModel *model = self.vcLoadModelCache[p]; model.stable = YES; NSLog(@"vc end load"); NSLog(@"vc load time:%f", (model.endTime - model.startTime)); }); self.vcBlockCache[p] = calculateBlock; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), calculateBlock);
在每次 layout 之前触发一个取消上一次计算的操作
1 2 3 4 5 6 dispatch_block_t calculateBlock = self.vcBlockCache[p]; if (calculateBlock) { dispatch_block_cancel(calculateBlock); NSLog(@"cancle block %p", calculateBlock); _vcBlockCache[p] = nil; }
那么当最后一次 layout 结束以后一秒以内没有其他 view 需要被 layout,则认为这个视图结构已经稳定了,可以计算真实的页面加载时间
无痕埋点的实现 给View打标记 从 window 往下的整个视图有很多很多,既然要无痕埋点势必需要全局 hook。 思路就是把VC下所有的 view 都打上一个标记,当 view layout 的时候判断标记,把页面加载的时间点进行一个记录。
1 2 3 4 5 6 7 8 9 10 11 12 @interface JFWeakWarraper : NSObject @property (nonatomic, weak) UIViewController *vc; - (instancetype)initWithVC:(UIViewController *)vc; @end @interface UIView (JFTVCLife) @property (nonatomic, strong) JFWeakWarraper *jftVCRef; - (void)prepareVCLifeRecord; - (void)recordVCLife; - (void)jft_layoutSublayersOfLayer:(CALayer *)layer; @end
jftVCRef
就是一个标记,让 View 能够通过这个属性快速判断是否属于某个 VC
如何打标记
如果自身有一个标记,那么就把 subviews 都添加相关标记,并且把这个事件丢给一个专门处理页面生命周期的类JFTVCLoadCenter
1 2 3 4 5 6 7 8 - (void)prepareVCLifeRecord { UIViewController *vc = self.jftVCRef.vc; if (!vc) return; [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { obj.jftVCRef = [[JFWeakWarraper alloc] initWithVC:vc]; }]; [[JFTVCLoadCenter defaultCenter] postVCStartLayoutNotification:vc]; }
当 layout 结束的时候会调用 recordVCLife,在这个地方同样把事件丢给 JFTVCLoadCenter
处理
1 2 3 4 5 - (void)recordVCLife { UIViewController *vc = self.jftVCRef.vc; if (!vc) return; [[JFTVCLoadCenter defaultCenter] postVCEndLayouttNotification:vc]; }
为什么只遍历一层subView,而不是把所有的子 view 直接都打上标记
假设我们有这么一个结构 :
VC–>View–>TableView–>View(a)–>View(b)–>View(b)
layout 从 View
开始左到右进行。 网络回调中触发 TableView
的 reload,layout 从 TableView
开始重新向右进行。
方案一:如果每次 layout 都遍历其下整个视图树,并打标记 越靠近左边的 view 触发打标记的行为也就越多,到了叶子结点,很多打标记的行为都是不必要的。
方案二:如果只在起点打标记,后续直接忽略。 那么当 TableView
的 reload 时,动态创建 Cell
其所有的子 View 都不会被打上标记。如果把这个案例当作特殊情况处理明显不合理。
这样统计会不会有问题? 子view
layout 的是由父view
触发的,这就意味着父亲的调用永远在孩子之前,这样的调用显然没问题。
JFTVCLoadCenter 这是处理页面的加载周期消息处理的核心代码
一个初始化方法,两个接收「消息」的方法
1 2 3 4 5 @interface JFTVCLoadCenter : NSObject + (instancetype)defaultCenter; - (void)postVCStartLayoutNotification:(UIViewController *)vc; - (void)postVCEndLayouttNotification:(UIViewController *)vc; @end
每次vc下面的 view 在layout调用以前需要调用 postVCStartLayoutNotification 每次vc下面的 view 在layout调用完成以后需要调用 postVCEndLayouttNotification
布局开始之前记录调用时间,并且取消所有之前调用的 block。
1 2 3 4 5 6 7 8 9 10 - (void)postVCStartLayoutNotification:(UIViewController *)vc { NSString *p = [NSString stringWithFormat:@"%p", vc]; [self addStartTimeIfNeed:p]; dispatch_block_t calculateBlock = self.vcBlockCache[p]; if (calculateBlock) { dispatch_block_cancel(calculateBlock); NSLog(@"cancle block %p", calculateBlock); _vcBlockCache[p] = nil; } }
布局结束以后记录调用时间,也需要取消所有之前调用的 block。 这里取消之前的调用不能省,因为调用是这样的 1start–>2start–>2end–>1end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - (void)postVCEndLayouttNotification:(UIViewController *)vc { NSString *p = [NSString stringWithFormat:@"%p", vc]; [self updateEndTimeIfNeed:p]; dispatch_block_t calculateBlock = self.vcBlockCache[p]; if (calculateBlock) { dispatch_block_cancel(calculateBlock); NSLog(@"cancle block %p", calculateBlock); _vcBlockCache[p] = nil; } calculateBlock = dispatch_block_create(0, ^{ JFTVCLoadModel *model = self.vcLoadModelCache[p]; model.stable = YES; NSLog(@"vc end load"); NSLog(@"vc load time:%f", (model.endTime - model.startTime)); }); self.vcBlockCache[p] = calculateBlock; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), calculateBlock); }
这里用一个字典保存了之前提交的 block,key 是 vc 的指针。
1 2 3 4 @interface JFTVCLoadCenter () @property (nonatomic, strong) NSMutableDictionary *vcBlockCache; @property (nonatomic, strong) NSMutableDictionary *vcLoadModelCache; @end
用另外一个字典保存了页面加载的时间节点信息。包含了页面开始的时间和结束的时间。用一个布尔值标记页面是否已经稳定。
1 2 3 4 5 @interface JFTVCLoadModel : NSObject @property (nonatomic, assign) CFTimeInterval startTime; @property (nonatomic, assign) CFTimeInterval endTime; @property (nonatomic, assign) BOOL stable; @end
页面生命周期只记录最早的一次 start
1 2 3 4 5 6 7 8 9 10 - (void)addStartTimeIfNeed:(NSString *)p { JFTVCLoadModel *model = self.vcLoadModelCache[p]; if (!model) { model = [JFTVCLoadModel new]; self.vcLoadModelCache[p] = model; } if (model.startTime == 0) {/// need update model.startTime = CACurrentMediaTime(); } }
页面生命周期稳定以后不需要再更新结束时间
1 2 3 4 5 6 7 8 9 - (void)updateEndTimeIfNeed:(NSString *)p { JFTVCLoadModel *model = self.vcLoadModelCache[p]; NSAssert(model, @"结束必须比开始先执行"); if (model.stable) { NSLog(@"vc have stable, no need to update end time"); } else { model.endTime = CACurrentMediaTime(); } }
hook 1 2 3 4 5 6 7 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [UIViewController jr_swizzleMethod:@selector(setView:) withMethod:@selector(jft_setView:) error:nil]; [UIViewController jr_swizzleMethod:@selector(viewWillLayoutSubviews) withMethod:@selector(jft_viewWillLayoutSubviews) error:nil]; [UIViewController jr_swizzleMethod:@selector(viewDidLayoutSubviews) withMethod:@selector(jft_viewDidLayoutSubviews) error:nil]; [UIView jr_swizzleMethod:@selector(layoutSublayersOfLayer:) withMethod:@selector(jft_layoutSublayersOfLayer:) error:nil]; return YES; }
具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @implementation UIViewController (JFTLife) - (void)jft_setView:(UIView *)view { JFWeakWarraper *weakWapper = [[JFWeakWarraper alloc] initWithVC:self]; view.jftVCRef = weakWapper; [self jft_setView:view]; } - (void)jft_viewWillLayoutSubviews { [self.view prepareVCLifeRecord]; [self jft_viewWillLayoutSubviews]; } - (void)jft_viewDidLayoutSubviews { [self.view recordVCLife]; [self jft_viewDidLayoutSubviews]; } @end
后续优化 如果认为布局稳定了不算加载完成,也可以把其他的指标也纳入进来 图片加载也和布局是同等级别的,逻辑直接复用就可以了。
如果认为图片加载比布局更重要 比方说布局稳定了,但是前面有个图片加载已经开始了,那布局稳定的计算的操作就 cancel 掉,等图片加载完成再计算。
总之就看你怎么界定界面稳定这一事情,并且函数调用的开始&结束转化为两个消息触发计算和 cancel。这样就能完美不需要前端埋点。
Demo 本文的例子可以在 github 获取