开启生长之旅!这是我参与「日新计划 12 月更文应战」的第16天,点击检查活动详情

咱们知道,与用户交互的工作都是在主线程里处理的,但假如主线程无法呼应用户的交互就会造成卡顿,卡顿时刻比较长是非常影响App的功能和用户体会的,所以这也是一个非常值得重视的问题。

一般可以造成卡顿的几种原因如下:

  • 复杂的UI、图文混排的制作量过大
  • 在主线程上做网络同步恳求
  • 在主线程做大量的IO操作
  • 运算量过大,CPU持续高占用
  • 死锁和主线程抢锁

卡顿监控的办法

要想治理卡顿问题,首先要先监控到卡顿的问题才行,一般监控卡顿有三种计划:

  • 运用Instruments监测卡顿;
  • 运用CADisplayLink来监控改写的帧率;
  • 运用runLoop,创立一个观察者,监控runLoop状态。

Instruments

iOS卡顿监控探索与实践
Instruments只能在Debug联调的时候运用,但许多用户的场景和数据都与debug下的数据并不共同,而且没有办法做到线上实时监控用户的卡顿数据。

FPS

FPS是一秒显示的帧数,也就是一秒内画面变化的数量。一般以为50FPS以上会不卡顿。

可以运用CADisplayLink,CADisplayLink是一个特别的定时器,咱们常说的60HZ,就是说默许情况下CADisplayLink每秒调用60次。咱们可以经过核算它1秒内调用多少次来检查界面的流通度。虽然CADisplayLink更轻量,但需要在CPU稍微清闲时才可以回调,严峻卡顿的仓库获取不一定及时,而且就算50FPS以下经过肉眼来看也是连贯的,所以简略的经过监督FPS很难确定是否出现了卡顿问题。

RunLoop

iOS卡顿监控探索与实践
监控卡顿就是要去找到主线程上做了哪些工作。咱们知道主线程有一个RunLoop。RunLoop是一个Event Loop模型,让线程可以处于接收消息、处理工作、进入等候而不立刻退出。在进入工作的前后,RunLoop会向注册的Observer通知相应的工作。

运用RunLoop监控的计划是。经过监控RunLoop的状态来判别是否会出现卡顿。

想要监听RunLoop,首先需要创立一个CFRunLoopObserverContext观察者,代码如下:

_observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
        case kCFRunLoopEntry: {
        } break;
        case kCFRunLoopBeforeTimers: {
        } break;
        case kCFRunLoopBeforeSources: {
        } break;
        case kCFRunLoopBeforeWaiting: {
            //处理完工作,即将休眠
        } break;
        case kCFRunLoopAfterWaiting: {
            //将被唤醒
        } break;
        case kCFRunLoopExit: {
        } break;
        default:
            break;
    }
});

将创立好的观察者_observer添加到主线程RunLoop的common模式下观察。

CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);

然后,创立一个持续的子线程专门用来监控主线程的RunLoop状态。

//创立子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    //子线程开启一个持续的 loop 用来进行监控
    while (YES) {
        long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
        if (semaphoreWait != 0) {
            if (!_observer) {
                timeoutCount = 0;
                dispatchSemaphore = 0;
                runLoopActivity = 0;
                return;
            }
            //BeforeSources 和 AfterWaiting 这两个状态可以检测到是否卡顿
            if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                //将仓库信息上报服务器的代码放到这里
            } //end activity
        }// end semaphore wait
        timeoutCount = 0;
    }// end while
});

5s是触发卡顿的时刻阈值,这个时刻如何设置才更合理,咱们可以参阅WatchDog机制来设置。WatchDog在不同状态下设置的不同时刻,如下所示:

  • 发动(Launch):20s
  • 恢复(Resume):10s
  • 挂起(Suspend):10s
  • 退出(Quit):6s
  • 后台(Background):3min(每次请求3min,可接连请求,最多请求到10min) 大原则是这个阈值要小于WatchDog的约束时刻。然后卡顿首要也是处理掉用户感知非常显着的卡顿问题,所以可以动态的去配置阈值的时刻。假如是刚刚开始监控的话可以把阈值设置的大一些,然后渐渐的收紧,达到一个可监控和体会的平衡。

获取仓库信息

当监控到卡顿后,还需要将卡顿的仓库记录下来,而且上传给开发者剖析和定位问题。

获取仓库信息的一种办法是直接调用系统函数。这种办法的长处在于,功能消耗小。可是只能获取到简略的信息。

反常捕获的办法:

NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);

这个办法也用来获取crash反常,运用时要进行区分。

第二种方法是运用第三方库来获取仓库信息,第三方库要满意以下条件:

  • 可以在任何时候获取到仓库信息,假如只有crash时才干获取则不满意
  • 获取到的仓库信息中要包括Binary Images,不然或许无法符号化

在这里推荐运用 PLCrashReporter 。假如项目中包括了其他获取仓库的库,假如满意以上的条件那就可以直接用;假如不满意需要运用其他库时需要留意是否影响crash的获取和上报。上报成功后经过dsym进行符号化剖析。

总结

咱们经过卡顿监控与上报处理了多个卡顿问题,是之前的复现手段无法复现的(经过日志路径模仿、特别账号复现等)。目前卡顿问题也被逐步梳理出来并得到处理。

在运用过程中总结了以下一些经历:

  1. 在咱们处理的过程中发现大部分的卡顿问题都是代码逻辑中把错误的逻辑放到主线程中了。比方运用异步线程做一些操作可是回调后又回到了主线程;一些文件IO在主线程中进行的;一些UI的bug等。
  2. 在监控的仓库里也并不是所有仓库都是有用的,因为获取卡顿仓库的逻辑是经过一个时刻阈值来获取仓库,比方卡顿了5s然后真正卡顿的仓库现已过去了,这时候再取仓库或许就不能提供有用信息了。可是大部分都仍是准确的。