美文网首页
runloop 核心点

runloop 核心点

作者: George_Luofz | 来源:发表于2018-04-08 18:33 被阅读19次
概述
  • 主要参考一些面试中提问的点,来从核心角度理解runloop
  • 想要全面理解,可以参考经典文章:深入理解Runloop
  • 文章源码参考swift版开源代码swift-corelibs-foundation,这个版本更新
1. 原理
1.1 runloop与线程的关系

要想理解这个还是得参考源码,创建runloop时,内部调用的其实是_CFRunLoopGet0这个方法

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
   // 1.如果__CFRunLoops这个全局字典不存在,就创建一个
    if (!__CFRunLoops) {
    // 1.1 创建全局字典
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    // 1.2创建主线程的runloop,保存在全局字典中
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
    }
    // 2. 创建一个runloop,先从全局字典中找,没有就创建并加入字典中
    CFRunLoopRef newLoop = NULL;
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
    newLoop = __CFRunLoopCreate(t);
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
    __CFUnlock(&loopsLock);
    // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
    if (newLoop) { CFRelease(newLoop); }
    // 3.检查如果是当前线程(就是说在当前线程创建了其runloop),则做额外处理
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

理解:

  1. 线程与runloop是一对一的,主线程的runloop默认会创建,其他线程的runloop默认不创建;同一个线程的runloop是唯一的,是说已经创建了就不会再创建
  2. 创建时机:当线程第一次获取其runloop时,才会创建对应的runloop(我原来的理解有误)
  3. 销毁时机:可以肯定线程结束,就没有runloop了;具体销毁时机,暂时不明
1.2 runloop的mode作用是什么

两方面理解:

  • 每种mode决定每次runloop循环在何种mode下执行,然后能够执行该mode下的操作,可以在CFRunLoopRunSpecific方法中看到runloop mode的运行逻辑
  • 对于加入runloop的多个mode,还起到一个优先级的作用,就像在滚动时,系统让UITrackingRunLoopMode下的操作执行,让其他非commonModes下的操作暂停,以此保证滚动的流畅性
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
   // 1.如果modeName不合法,直接return
    if (modeName == NULL || modeName == kCFRunLoopCommonModes || CFEqual(modeName, kCFRunLoopCommonModes)) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            CFLog(kCFLogLevelError, CFSTR("invalid mode '%@' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution."), modeName);
            _CFRunLoopError_RunCalledWithInvalidMode();
        });
        return kCFRunLoopRunFinished;
    }
   // 2.如果runloop正在释放,return finish
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
   // 3.找到了对应的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    // 3.1 合理性判断
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
    Boolean did = false;
    if (currentMode) __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopUnlock(rl);
    return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
   // 3.2 初始配置
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;
        // 3.3 runloop的状态是entry(就是进入),然后运行该runloop,核心逻辑就是调用__CFRunLoopRun()方法
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
        cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_START, rl, currentMode, seconds, previousMode);
        result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
        cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_END, rl, currentMode, seconds, previousMode);
       // 3.4 如果状态是exit,就销毁其中的perData等数据(可以理解是退出时释放其持有的数据)
        if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
        rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

理解:

  1. runloop每次都是在对应的mode下运行,至于在该mode下如何运行,则要继续参考函数static int32_t __CFRunLoopRun()的实现
1.2.1 runloop与mode的关系
  • 一个runloop可以运行多个不同的mode,参考runloop的实现原理,每次都要在指定的mode下才能运行
  • 一个mode可以有多个source/timer/observer(统称为mode item),拿timer来理解,可以在主线程中创建多个timer,将其加入commonModes中,那么每次do{}while循环到指定时间每个timer都会触发其对应的事件
  • 一个mode item可以加入多个mode中,比如一个timer可以加入不同的mode中,我们加入commonModes实际上就是加入了多个mode,因为commonModes是多个mode的集合
    此处的source分source0和source1两种,source1就是port,用于线程间传递数据
  • mode常见的有两种:
    NSRunLoopDefaultMode runloop默认就在此mode类型下运行
    UITrackingRunLoopMode UIScrollView滚动时在该mode下运行
    NSRunLoopCommonModes 是一个mode集合,不算是单独的mode类型
  • 如何切换runloop的mode?
    由下边代码可以看到,一个runloop一旦开始运行,该运行期内的mode是确定的,所以应该stop掉该runloop,再重新指定mode
1.3 runloop的实现原理

__CFRunLoopRun()函数太长,具体解释参考Run Loop记录与源码注释,用伪代码(参考:深入理解Runloop)表示:

    // 0. 先判断mode里有没有source/timer/observer,如果没有,就直接返回。
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    // 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
   // 2. 进入loop
 do{
      // 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
      __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
      // 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
      __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
     // 4. 执行被加入的block
     __CFRunLoopDoBlocks(runloop, currentMode);
    // 5. 如果有source1,直接处理source1
   __CFRunLoopServiceMachPort(dispatchPort, &msg)
    // 6. 通知即将进入休眠,其实如果没有timer、mainQueue的block等任务,本次循环就算执行完了
   __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
   // 7. 等待mach_port消息
   __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort)        {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
    }
    // 8. 通知线程被唤醒了
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
     // 9.1 timer时间到,执行timer
     if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
    // 9.2.执行mainQueue中的block
    else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
    // 9.3 执行source1的事件
    else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
    // 10.执行加入到其中的block
    __CFRunLoopDoBlocks(runloop, currentMode);
   // 11.都执行完毕,修改状态为stopped/finished/超时等
   if (sourceHandledThisLoop && stopAfterHandle) {
                /// 进入loop时参数说处理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) { 
                /// 被外部调用者强制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }
 }while(retVal == 0);

从上边runloop的内部逻辑可以看出,runloop其实就是一个死循环,让线程一直运行或者处于休眠状态;直到超时或者停止才会结束循环

1.4 runloop的数据结构
  • runloop会存在全局的dictionary中
  • runloop本身结构如下:
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;            /* locked for accessing mode list */
    __CFPort _wakeUpPort;            // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread; //线程,runloop整个是基于pthread_t的
    uint32_t _winthread;
    CFMutableSetRef _commonModes; //加入该runloop的commonModes
    CFMutableSetRef _commonModeItems; // 所有commonModes里的items
    CFRunLoopModeRef _currentMode; //当前的mode
    CFMutableSetRef _modes; //加入该runloop的所有modes
    struct _block_item *_blocks_head; //存放 CFRunLoopPerformBlock 函数添加的 block 的双向链表的头指针
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

参考:Run Loop记录与源码注释

2. 应用

2.1. 解决NSTimer不执行

  • 大家都很6,不多写了;要注意的是加commonModes不是defaultMode,这样UISrollView滚动时它还是不会走的
  • 由上方代码可知,timer执行依赖于runloop,timer执行的时间在runloop中是确定的,如果到这个点没执行,该次timer就不再执行了(这个逻辑还要结合timer的源码确定)

2.2. tableView优化

  • 由于runloop会有空闲时间,我们根据runloop的回调是可以拿到进入休闲和唤醒的时间的,所以可以利用这段空闲时间做点儿事情,比如计算下cell的高度缓存下来,因为heightForRow会在滚动时频繁调用,可以参考著明的# UITableView-FDTemplateLayoutCell实现

2.3. 线程保活

  • 可以用NSMachPort来做保活,相当于给runloop加了一个包含source0的mode:
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
  • runloop的启动与停止,由上边分析可以看到,子线程runloop默认是不创建的,要想创建并运行可以有多种方法,最好的方法是使用CFRunloopRun()这个函数,因为这个方法创建的runloop,可以随时调用CFRunloopStop()停止该loop,参考:深入研究Runloop 与线程保活
    在AFNetWorking使用NSUrlConnection时代,有著名的例子
+ (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;
}
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
AFN该线程是一个单例,所以不需要考虑停止runloop,也就使用[[NSRunLoop currentRunLoop] run]这个就可以满足要求了

2.4. 代码监测卡顿
这个类似于tableView的优化,思路就是在子线程监听runloop的休眠和唤醒周期,计算两个时间间隔,如果间隔连续多次(比如5次)超过50ms,就认为出现了卡顿,然后获取当前runloop线程的堆栈信息,上报给监控平台
代码参考:iOS实时卡顿监控

相关文章

  • runloop 核心点

    概述 主要参考一些面试中提问的点,来从核心角度理解runloop 想要全面理解,可以参考经典文章:深入理解Runl...

  • RunLoop 源码阅读

    获取runloop的函数 创建runloop的函数 运行runloop的函数 Runloop 运行的核心函数__C...

  • RunLoop介绍

    RunLoop 的概念 RunLoop 与线程的关系 RunLoop核心数据结构 CFRunLoopRef CFR...

  • RunLoop 原理和核心机制(摘自网络)

    RunLoop 原理和核心机制

  • RunLoop和线程关系

    1.runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop...

  • runloop 小结

    OC的两大核心runtime和runloop runloop简介 runloop本质上是一个do-while循环,...

  • AsyncDisplayKit使用的技术原理之一——Runloo

    Runloop work distribution (Runloop任务分发)是ASDK使用的比较核心的一种技术...

  • RunLoop的核心

    RunLoop的核心部分就是当有任务需要处理的时候,线程被唤醒,执行指定的任务。当没有任务需要处理的时候,线程处于...

  • Runloop

    1、Runloop 和 线程 的关系 1、runloop与线程是一一对应的,一个runloop对应一个核心的线程,...

  • iOS复习之RunLoop

    1、事件循环2、用户态3、核心态4、常驻线程 主要 RunLoop文章:RunLoop入门 看我就够了iOS - ...

网友评论

      本文标题:runloop 核心点

      本文链接:https://www.haomeiwen.com/subject/jvkmhftx.html