iOS性能优化 —  二、卡顿监控及处理 - 图1

上篇文章为大家讲解了crash监控及防崩溃处理,这片文章继续为大家讲解下卡顿监控及处理。

  • 卡顿产生原理
  • 如何收集卡顿
    • 利用bugly、听云等第三方收集
    • 自己收集卡顿
      • 监控主线程RunLoop
      • 子线程ping

卡顿产生原理

FPS (Frames Per Second) 表示每秒渲染帧数,通常用于衡量画面的流畅度,每秒帧数越多,则表示画面越流畅。通常60是临界值,如果主线层FPS低于60fps,应用程序就可能产生卡顿。大家可以看这篇文章详细了解卡顿产生原理。

如何收集卡顿

利用bugly、听云等第三方收集

国内有很多第三方网站可以用来收集卡顿,常用的有bugly、听云等。笔者推荐大家用腾讯的bugly来收集卡顿。

自己收集卡顿

如果我们要自己手动监控卡顿,其实有好几种方案,如下:

监控主线程RunLoop

我们知道iOS App基于RunLoop运行,我们先来看看RunLoop简化后的代码。

  1. // 1.进入loop
  2. __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled)
  3. // 2.RunLoop 即将触发 Timer 回调。
  4. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
  5. // 3.RunLoop 即将触发 Source0 (非port) 回调。
  6. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
  7. // 4.RunLoop 触发 Source0 (非port) 回调。
  8. sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle)
  9. // 5.执行被加入的block
  10. __CFRunLoopDoBlocks(runloop, currentMode);
  11. // 6.RunLoop 的线程即将进入休眠(sleep)。
  12. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
  13. // 7.调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
  14. __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort)
  15. // 进入休眠
  16. // 8.RunLoop 的线程刚刚被唤醒了。
  17. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting
  18. // 9.如果一个 Timer 到时间了,触发这个Timer的回调
  19. __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
  20. // 10.如果有dispatch到main_queue的block,执行bloc
  21. __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
  22. // 11.如果一个 Source1 (基于port) 发出事件了,处理这个事件
  23. __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
  24. // 12.RunLoop 即将退出
  25. __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

我们可以看到RunLoop调用方法主要集中在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间。我们可以开辟一个子线程来监控主线程RunLoop,然后实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况,比如如果连续5次超时50ms,则认为发生了卡顿。代码如下:

  1. @interface AKStuckMonitor ()
  2. {
  3. int timeoutCount;
  4. CFRunLoopObserverRef observer;
  5. @public
  6. dispatch_semaphore_t semaphore;
  7. CFRunLoopActivity activity;
  8. }
  9. @end
  10. @implementation FQLAPMStuckMonitor
  11. + (instancetype)sharedInstance{
  12. static id instance = nil;
  13. static dispatch_once_t onceToken;
  14. dispatch_once(&onceToken, ^{
  15. instance = [[self alloc] init];
  16. });
  17. return instance;
  18. }
  19. static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
  20. AKStuckMonitor *moniotr = (__bridge AKStuckMonitor*)info;
  21. moniotr->activity = activity;
  22. dispatch_semaphore_t semaphore = moniotr->semaphore;
  23. dispatch_semaphore_signal(semaphore);
  24. }
  25. - (void)stop{
  26. if (!observer)
  27. return;
  28. CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
  29. CFRelease(observer);
  30. observer = NULL;
  31. }
  32. - (void)start{
  33. if (observer)
  34. return;
  35. // 信号
  36. semaphore = dispatch_semaphore_create(0);
  37. // 注册RunLoop状态观察
  38. CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
  39. observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
  40. kCFRunLoopAllActivities,
  41. YES,
  42. 0,
  43. &runLoopObserverCallBack,
  44. &context);
  45. CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
  46. // 在子线程监控时长
  47. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  48. float time = 50;
  49. while (YES)
  50. {
  51. long st = dispatch_semaphore_wait(self->semaphore, dispatch_time(DISPATCH_TIME_NOW, time * NSEC_PER_MSEC));
  52. if (st != 0)
  53. {
  54. if (!self->observer)
  55. {
  56. self->timeoutCount = 0;
  57. self->semaphore = 0;
  58. self->activity = 0;
  59. return;
  60. }
  61. if (self->activity==kCFRunLoopBeforeSources || self->activity==kCFRunLoopAfterWaiting)
  62. {
  63. if (++self->timeoutCount < 5)
  64. continue;
  65. NSlog(@"检测到卡顿");
  66. }
  67. }
  68. self->timeoutCount = 0;
  69. }
  70. });
  71. }
  72. @end

子线程ping

创建一个子线程通过信号量去ping主线程,每次检测时设置标记位为YES,然后派发任务到主线程中将标记位设置为NO。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成NO,如果没有说明主线程发生了卡顿。

  1. @interface PingThread : NSThread
  2. ......
  3. @end
  4. @implementation PingThread
  5. - (void)main {
  6. [self pingMainThread];
  7. }
  8. - (void)pingMainThread {
  9. while (!self.cancelled) {
  10. @autoreleasepool {
  11. dispatch_async(dispatch_get_main_queue(), ^{
  12. [_lock unlock];
  13. });
  14. CFAbsoluteTime pingTime = CFAbsoluteTimeGetCurrent();
  15. [_lock lock];
  16. if (CFAbsoluteTimeGetCurrent() - pingTime >= _threshold) {
  17. ......
  18. }
  19. [NSThread sleepForTimeInterval: _interval];
  20. }
  21. }
  22. }
  23. @end

资料推荐

如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

iOS性能优化 —  二、卡顿监控及处理 - 图2