有些时候,需要知道什么时候View会布局完成。比如需要在View布局完成之后,希望页面自动跳转到某一
个模块,如果不知道View什么时候布局完成,那跳转到某个位置的高度就无法计算了。
页面渲染布局必然是在准备好数据之后,所以通常一个页面先要通过网络请求将数据获取,然后再通过UI组
件进行渲染。所以要对View的布局进行监听肯定是在网络请求回来之后了,对于网络什么时候会结束只可能
是业务层自己才能知道,所以主要精力是分析网络请求回来之后的事情。
基本思路是通过CADdisplayLink来记录每一帧当前View作为根节点时视图层级的情况,同时和上一帧进行比较。
如果在一个较短的时间内发现两者不一致,就说明布局还未结束。如果这个时间之内两者一致,则说明布局完成
了。正常情况下,如果界面没有发生卡顿,一秒应该在40帧以上。如果设置的这个时间间隔是0.2s,那么在这个
时间间隔中,会绘制屏幕8次,也就是说如果连续8次屏幕渲染没有发生视图层级的变化就认为是布局结束了。
具体如何比较前后两帧是否一致呢,我是通过字符串记录当前帧视图层级中每一个View的对象信息(内存地址,坐
标和宽高),比较两个字符串是否相等,如果相等就说明布局没有发生变化如果不等就说明发生了变化。
要点:
1 为了更少的侵入业务层,通过category + associatedObject来实现。
2 0.2s的时间间隔需要一个定时任务,通过GCD Source来实现,不使用NSTimer因为前者要更精确,不依赖
Runloop,不受其他任务的影响。
3 如何说明它的正确性:主要是通过和ViewController的声明周期方法viewDidLayoutSubViews调用时间先
后的对比,如果还未布局完成那么之后系统框架还会再调用viewDidLayoutSubViews,所以只需要看回调是
不是发生在viewDidLayoutSubViews之后,最后通过demo验证了这种方案的可行性。
代码:
UIView + LayoutCompleteChecker.h
1 2
| typedef void(^callback)(); - (void)startCheckingWithCompletionBlock:(callback)callback;
|
UIView + LayoutCompleteChecker.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| - (NSString *)viewTreeString { return objc_getAssociatedObject(self, @selector(viewTreeString)); }
- (void)setViewTreeString:(NSString *)treeString { objc_setAssociatedObject(self, @selector(viewTreeString), treeString, OBJC_ASSOCIATION_COPY); }
- (NSNumber *)hasLayoutCompleted { NSNumber *result = objc_getAssociatedObject(self, @selector(hasLayoutCompleted)); if (!result) { return @(NO); } else { return result; } }
- (void)setLayoutCompleted:(NSNumber *)result { objc_setAssociatedObject(self, @selector(hasLayoutCompleted), result, OBJC_ASSOCIATION_RETAIN); }
- (CADisplayLink *)displayLink { CADisplayLink *link = objc_getAssociatedObject(self, @selector(displayLink)); if (!link) { link = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_checkerTick)]; objc_setAssociatedObject(self, @selector(displayLink), link, OBJC_ASSOCIATION_RETAIN); } return link; }
- (void)p_checkerTick { NSString *treeString = self.viewTreeString; NSString *currentTreeString = self.currentLayoutString; if (![treeString isEqualToString:currentTreeString]) { [self setViewTreeString:currentTreeString]; } else { [self setLayoutCompleted:@(YES)]; } }
- (NSString *)currentLayoutString { NSMutableString *layoutString = [NSMutableString stringWithFormat:@"%@", self]; if (self.subviews.count) { for (int i = 0; i < self.subviews.count; i++) { UIView *subView = self.subviews[i]; NSString *subViewString = [subView currentLayoutString]; [layoutString appendString:subViewString]; } } return [layoutString copy]; }
- (void)startCheckingWithCompletionBlock:(callback)callback { [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, kCheckInterval * NSEC_PER_SEC, kTolerance * NSEC_PER_SEC); id __weak weakSelf = self; dispatch_source_set_event_handler(timer, ^{ NSNumber *result = self.hasLayoutCompleted; if ([result boolValue]) { id __strong strongSelf = weakSelf; dispatch_source_cancel(timer); [[strongSelf displayLink] invalidate]; callback(); } }); dispatch_resume(timer); }
|
最后对需要检测的View调用startCheckingWithCompletionBlock方法即可:
1 2 3
| [self.view startCheckingWithCompletionBlock:^{ NSLog(@"布局完了"); }];
|
GitHub:https://github.com/huanshijiushiniu/layoutCompleteChecker
现在已经提交到CocoaPods,可以通过在podfile中添加引用来使用了:
1
| pod 'layoutCompleteChecker', '1.0.0'
|