iOS底层原理09: RunLoop在开发中的使用

445 阅读4分钟

一、线程保活

  1. 当我们开启一个子线程处理任务时, 如果这个任务完成后, 线程就会被销毁. 如果我们某个任务需要多次不停的在子线程处理, 那么频繁的开启/销毁子线程就会有很大的性能开销. 我们可以创建一个子线程, 并让他保活不被销毁, 每次处理任务时都放进这个子线程, 这样就可以减少CPU的开销.
  2. 当我们开启一个线程处理数据, 但是数据是延迟返回的, 为了防止线程提前销毁, 需要对线程保活, 保证拿到返回数据后线程在销毁. AFN(2.x)就是一个这样很好的例子, NSURLConnection需要等待回调处理, 就需要将线程保活. AFN(3.x)以后使用NSURLSession进行封装, 开发者可以自己设置回调队列, 所以就不需要保活线程了
// 定义一个自定义线程, 让线程执行一个方法. 并且通过GCD保证只执行一次
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

// 在执行的方法中拿到当前RunLoop, 给RunLoop添加一个[NSMachPort port], 保证当前Mode中有一个port这样RunLoop就不会退出, 达到线程保活的目的
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

二、解决NSTimer在滑动过程中会停止工作

NSTimer是在RunLoop的DefaultMode工作, 当界面滑动时, RunLoop会切换到TrackingMode, 所以定时器就会失效. 我们需要将定时器加入RunLoop时, 使用CommonMode就可以解决

    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate now] interval:1.0 target:self selector:@selector(test) userInfo:NULL repeats:YES];
    // 将timer 加入到CommonModes
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

那么为什么使用NSRunLoopCommonModes就可以解决问题呢? 我们看下add方法的具体实现:

  1. 进入方法会先判断是否是NSRunLoopCommonModes
  2. 如果是NSRunLoopCommonModes, 就会获取当前runloop的_commonModes和_commonModeItems,
  3. 将当前timer加入runloop的_commonModeItems
  4. 将当前runloop和timer作为context, 对所有的_commonModes执行__CFRunLoopAddItemToCommonModes函数
  5. __CFRunLoopAddItemToCommonModes函数中拿到timer和runloop会递归调用CFRunLoopAddTimer方法, 此时传入的mode是具体的mode, 就会进行走到else里边, 执行对应的timer
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {    
    
    // ...
    __CFRunLoopLock(rl);
    if (modeName == kCFRunLoopCommonModes) {
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
        if (NULL == rl->_commonModeItems) {
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        CFSetAddValue(rl->_commonModeItems, rlt);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rlt};
            /* add new item to all common-modes */
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
    } else {
        // ...
    }
    __CFRunLoopUnlock(rl);
}

static void __CFRunLoopAddItemsToCommonMode(const void *value, void *ctx) {
    CFTypeRef item = (CFTypeRef)value;
    CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
    CFStringRef modeName = (CFStringRef)(((CFTypeRef *)ctx)[1]);
    if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
        CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
    } else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
        CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
    } else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
        CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
    }
}

三、监控应用卡顿

使用RunLoop监控应用卡顿的原理是监控RunLoop的状态. 如果当前RunLoop在进入休眠前的方法执行时间过长导致无法休眠或线程唤醒后接受消息时间过长无法进入下一步, 就可以认为是当前线程受阻了, 如果当前线程是主线程, 那么表现就是卡顿. 我们可以监听主线程RunLoop的状态变化, 在kCFRunLoopBeforeSource和kCFRunLoopAfterWaiting之间的时间, 如果超过某个时间, 我们就认为发生了卡顿.

以下示例代码参考了戴铭大佬的代码: GCDFetchFeed

  1. 首先我们创建一个runloop的observer, 监听runloop状态的变化; 同时初始化一个信号量, 用于线程同步

- (void)createObserver {

    //使用信号量保证同步
    dispatchSemaphore = dispatch_semaphore_create(0); 

    // 创建一个观察者
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);
    //将观察者添加到主线程runloop的common模式下的观察中
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
}

  1. 在监听回调中, 记录当前runloop的状态, 并且在每次runloop状态转换是释放一次信号量

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
    lagMonitor->runLoopActivity = activity;
    
    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}

  1. 创建子线程, 进行监控runloop的状态
    //创建子线程监控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 创建一个死循环, 持续监听runloop的状态
        while (YES) {
            // 设置信号量阻塞 50毫秒 (我们认定如果50毫秒runloop没有响应就是卡顿, 可以根据实际情况定义)
            long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));

            // 当信号量等待超时时 进行判断
            if (semaphoreWait != 0) {

                // 如果出现超时, 并且是 BeforeSources 或 AfterWaiting, 就认为发生卡顿, 可以做出相应的处理
                if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
      
                    NSLog(@"monitor trigger");

                } //end activity
            }// end semaphore wait
        }// end while
    });
  1. 根据上述流程, 我们可以总结
  • 如果没有发生卡顿, 那么我们的信号量会在runloop发生状态切换时不停释放(小于50ms), 在我们的监控while循环中信号量如果没有超时, 就一直跳过
  • 如果发生卡顿, 那么我们的信号量就会延迟释放(比如1s), 那么在while循环中, 信号量超时(意味着超过了50msrunloop的状态没有发生改变), 并且runloop的状态为BeforeSources 或 AfterWaiting, 我们就认为是发生了卡顿
  • 判定卡顿的条件可以根据自己的情况定义, 相应的处理也可以自己处理