21

iOS FPS 监控

 5 years ago
source link: http://nixwang.com/2018/08/03/a-better-fps-monitor/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

背景

iOS 系统及其应用以丝般顺滑闻名,界面的顺滑程度对于用户体验至关重要,因此需要针对性地对流程度进行优化。在优化之前必须要找到问题所在,那么就需要解决这两个问题:卡顿的原因是什么?哪里出现了卡顿?

卡顿原因

YYKit 作者 ibireme 写了 一篇很好的文章 来解释卡顿问题及解决方法,其中写到卡顿的原因是:

在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

简而言之就是 CPU 和 GPU 的工作量太大,无法在理想的时间内完成。相应的优化策略文章里也写得很清楚。

卡顿监控常见方案

  • FPS 监控:通过 CADisplayLink 来获取每一帧的耗时,进而计算出 FPS。
  • 通过开辟一个子线程监听 runLoop 状态变化来计算停留在各个状态的时间,当 runloop 处于 kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting 之间的时间过长就可以断定发生了卡顿。

但常见的 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK