内容简介:iOS 系统及其应用以丝般顺滑闻名,界面的顺滑程度对于用户体验至关重要,因此需要针对性地对流程度进行优化。在优化之前必须要找到问题所在,那么就需要解决这两个问题:卡顿的原因是什么?哪里出现了卡顿?YYKit 作者 ibireme 写了在 VSync 信号到来后,系统图形服务会通过
背景
iOS 系统及其应用以丝般顺滑闻名,界面的顺滑程度对于用户体验至关重要,因此需要针对性地对流程度进行优化。在优化之前必须要找到问题所在,那么就需要解决这两个问题:卡顿的原因是什么?哪里出现了卡顿?
卡顿原因
YYKit 作者 ibireme 写了 一篇很好的文章 来解释卡顿问题及解决方法,其中写到卡顿的原因是:
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink
等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
简而言之就是 CPU 和 GPU 的工作量太大,无法在理想的时间内完成。相应的优化策略文章里也写得很清楚。
卡顿监控常见方案
-
FPS 监控:通过
CADisplayLink来获取每一帧的耗时,进而计算出 FPS。 -
通过开辟一个子线程监听 runLoop 状态变化来计算停留在各个状态的时间,当 runloop 处于
kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间的时间过长就可以断定发生了卡顿。
但常见的 FPS 监控存在一些问题。
FPS 监控优化
当界面处于静止状态时,其 PFS 一般都会接近 60,卡顿一般都发生在界面发生滚动时。为了避免界面发生滚动时 FPS 的数据被静止时的数据平均掉,我们需要监听界面的滚动状态。
iOS 的 UIScrollViewDelegate
有三个方法可以做到:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView; // 用户开始拖动 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate; // 拖动结束 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; // 滑动结束
我们只需要 swizzle 这三个方法就可以排除界面静止时的数据。
Swizzling
因为我们要 siwzzle 的是 delegate,跟常见的 swizzle 方法有些不同:
- 我们并不知道 delegate 的类型。
- delegate 可能没有实现上面三个方法。
针对一个问题,我们可以直接 hook setDelegate
方法,在 setDelegate
方法内部再 hook UIScrollViewDelegate
的三个方法:
+ (void)load {
[self sm_swizzleMethod:@selector(setDelegate:) withMethod:@selector(hmfps_setDelegate:)];
}
- (void)hmfps_setDelegate:(id<UIScrollViewDelegate>)delegate {
NSLog(@"[HMFluencyMonitor] Hook %@", [self class]);
[self hmfps_hookDelegate:delegate];
[self hmfps_setDelegate:delegate];
}
但是这样做的好处是可以实现无痕监控,各个页面代码不需要做任何修改;风险是 app 里面的所有 UIScrollView 都会被 hook,包括嵌套的 UIScrollView,范围会比较广,一来 hook 了不需要 hook 的类,二来 crash 风险比较大,也可以提供方法让各个页面自行调用 hook。
- (BOOL)hmfps_shouldSwizzleDelegate:(id _Nonnull)delegate {
if ([delegate isProxy]) {
return NO;
}
if ([self isKindOfClass:[UITextView class]]) {
return NO;
}
return YES;
}
对于第二个问题,delegate 可能并没有实现我们要 hook 的三个方法,因此需要为他们增加一个默认的实现,内容是什么都不干。最终的代码如下:
- (void)hmfps_doNothing:(id)nothing {
// Do nothing
}
+ (void)hmfps_swizzleMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector forClass:(Class)originalClass{
Method testMethod = class_getInstanceMethod(originalClass, swizzledSelector);
if (testMethod) {
return;
}
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
Method dummyMethod = class_getInstanceMethod([self class], @selector(hmfps_doNothing:));
class_addMethod(originalClass,
originalSelector,
method_getImplementation(dummyMethod),
method_getTypeEncoding(dummyMethod));
class_addMethod(originalClass,
swizzledSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
originalMethod = class_getInstanceMethod(originalClass, originalSelector);
swizzledMethod = class_getInstanceMethod(originalClass, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
还有另外一个问题,如果项目里面使用了 BlockKit 的 A2DynamicDelegate
,hook 时会发生 crash,真正使用时要进行排除,猜测是因为 A2DynamicDelegate
的基类是 NSProxy
而不是 NSObject
。这里需要业务方自行实现三个 delegate 方法。
Ref
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Hacking Growth
Sean Ellis、Morgan Brown / Crown Business / 2017-4-25 / USD 29.00
The definitive playbook by the pioneers of Growth Hacking, one of the hottest business methodologies in Silicon Valley and beyond. It seems hard to believe today, but there was a time when Airbnb w......一起来看看 《Hacking Growth》 这本书的介绍吧!
RGB转16进制工具
RGB HEX 互转工具
XML 在线格式化
在线 XML 格式化压缩工具