iOS 性能优化 二、卡顿监控及处理

·  阅读 415
  • 卡顿产生原理

  • 如何收集卡顿

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

  • 自己收集卡顿

  • 监控主线程RunLoop

  • 子线程ping

卡顿产生原理

FPS (Frames Per Second) 表示每秒渲染帧数,通常用于衡量画面的流畅度,每秒帧数越多,则表示画面越流畅。通常60是临界值,如果主线层FPS低于60fps,应用程序就可能产生卡顿。

如何收集卡顿

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

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

自己收集卡顿

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

监控主线程RunLoop

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

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

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

@interface AKStuckMonitor (){    int timeoutCount;    CFRunLoopObserverRef observer;@public    dispatch_semaphore_t semaphore;    CFRunLoopActivity activity;}@end@implementation FQLAPMStuckMonitor+ (instancetype)sharedInstance{    static id instance = nil;    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        instance = [[self alloc] init];    });    return instance;}static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){    AKStuckMonitor *moniotr = (__bridge AKStuckMonitor*)info;    moniotr->activity = activity;    dispatch_semaphore_t semaphore = moniotr->semaphore;    dispatch_semaphore_signal(semaphore);}- (void)stop{    if (!observer)        return;    CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);    CFRelease(observer);    observer = NULL;}- (void)start{    if (observer)        return;    // 信号    semaphore = dispatch_semaphore_create(0);    // 注册RunLoop状态观察    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,                                       kCFRunLoopAllActivities,                                       YES,                                       0,                                       &runLoopObserverCallBack,                                       &context);    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);    // 在子线程监控时长    dispatch_async(dispatch_get_global_queue(0, 0), ^{        float time = 50;        while (YES)        {            long st = dispatch_semaphore_wait(self->semaphore, dispatch_time(DISPATCH_TIME_NOW, time * NSEC_PER_MSEC));            if (st != 0)            {                if (!self->observer)                {                    self->timeoutCount = 0;                    self->semaphore = 0;                    self->activity = 0;                    return;                }                if (self->activity==kCFRunLoopBeforeSources || self->activity==kCFRunLoopAfterWaiting)                {                    if (++self->timeoutCount < 5)                        continue;                    NSlog(@"检测到卡顿");                }            }            self->timeoutCount = 0;        }    });}@end
复制代码

子线程ping

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

@interface PingThread : NSThread......@end@implementation PingThread- (void)main {    [self pingMainThread];}- (void)pingMainThread {    while (!self.cancelled) {        @autoreleasepool {            dispatch_async(dispatch_get_main_queue(), ^{                [_lock unlock];            });            CFAbsoluteTime pingTime = CFAbsoluteTimeGetCurrent();            [_lock lock];            if (CFAbsoluteTimeGetCurrent() - pingTime >= _threshold) {                ......            }            [NSThread sleepForTimeInterval: _interval];        }    }}@end
复制代码

iOS开发交流技术群:563513413,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

分类:
iOS
标签: