Runloop备览

1. Runloop是什么

运行循环,就是在程序运行过程中循环做一些事情,如果没有runloop,程序执行完就会退出。有了runloop,程序就能一直运行,并且时刻等待用户的输入操作。runloop可以在需要的时候自己运行起来,没有操作的时候就休眠。可以节省cpu资源,提高程序性能。

2. Runloop作用

1.保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会创建对应的runloop,runloop保证主线程不被销毁,也就保证了程序的持续运行。
2.处理APP各种事件,例如触摸事件,定时器事件,selector事件
3.节省CPU资源,提高程序性能。开启runloop的线程没什么操作要做的时候,线程就会休眠,释放CPU资源。有事情要处理的时候,又会立刻唤醒线程进行处理。
Runloop内部原理
图中说明Runloop在运行中,收到input sources(mach port,custom input source,performSelector:onThread:…等)或者Timer sources时就会交给对应的处理方法去处理。没有事件消息传入的时候,runloop就休息了。

3. RunLoop和线程间的关系

Runloop是基于pthread进行管理的,pthread是基于c的跨平台多线程操作底层API。它是mach thread的上层封装(可以参见Kernel Programming Guide),和NSThread一一对应(而NSThread是一套面向对象的API,所以在iOS开发中我们也几乎不用直接使用pthread)。

苹果开发的接口中并没有直接创建Runloop的接口,如果需要使用Runloop通常使用CF的CFRunLoopGetMain()和CFRunLoopGetCurrent()两个方法来获取。对应地,可以调用Foundation中[NSRunLoop currentRunLoop]获得当前线程的RunLoop对象,
调用[NSRunLoop mainRunLoop]获得主线程的RunLoop对象。

//获取主线程的RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    //传入主线程
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

//获取当前Runloop,没有就调用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

// 查看_CFRunLoopGet0方法内部
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
     //t为空默认是主线程
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    //__CFRunLoops字典为空
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    // 为主线程创建对应的RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    // 将主线程-key和RunLoop-Value保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // 将线程作为key从字典里获取一个loop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    // 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
    if (!loop) {  
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    // 创建好之后,以线程为key,runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
    if (!loop) { 
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    //如果传入的线程和方法调用的线程是同一个,且尚未为该runloop注册销毁函数,则为runloop注册一个销毁函数__CFFinalizeRunLoop
    if (pthread_equal(t, pthread_self())) {
     //将RunLoop存在线程特定数据区,提高后续查找速度   _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

从上面的代码可以看出:
1.线程和RunLoop 之间是一一对应的,其关系是保存在一个Dictionary里,线程作为key,RunLoop作为value。
2.我们要创建子线程的RunLoop时,只需在子线程中获取当前线程的RunLoop对象即可,使用CFRunLoopGetCurrent()或[NSRunLoop currentRunLoop]。方法调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。如果不获取,那子线程就不会创建与之相关联的RunLoop。
3.主线程的Runloop比较特殊,任何线程创建之前都会保证主线程已经存在Runloop,而且CFRunLoopGetMain()不管在主线程还是子线程中调用,都可以获取到主线程的RunLoop。
4.RunLoop在第一次获取时创建,在线程结束时销毁。

此外,APP启动时,主线程中UIApplicationMain函数内启动了Runloop,程序不会马上退出,而是保持运行状态。因此每一个应用必须要有一个runloop。

4. 详解RunLoop相关类及作用

在Foundation框架中和RunLoop相关的类包括NSRunLoop。而Core Foundation中关于RunLoop的5个类:

  1. CFRunLoopRef - 获得当前RunLoop和主RunLoop
  2. CFRunLoopModeRef - RunLoop 运行模式,只能选择一种,在不同模式中做不同的操作
  3. CFRunLoopSourceRef - 事件源,输入源
  4. CFRunLoopTimerRef - 定时器事件
  5. CFRunLoopObserverRef - 观察者

4.1 CFRunLoopRef

4.1.1 RunLoop的种类

前面我们已经知道了,我们可以通过CFRunLoopGetCurrent函数获取当前线程的RunLoop,通过CFRunLoopGetMain函数获取主线程的RunLoop,以及它们两者之间的一些差别。

4.1.2 CFRunLoopRef的结构

CFRunLoopRef是指向__CFRunLoop结构体的指针。
通过源码我们找到__CFRunLoop结构体:

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;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

除一些记录性属性外,主要来看一下以下两个成员变量
CFRunLoopModeRef _currentMode;指向RunLoop当前的mode,
CFMutableSetRef _modes;RunLoop包含的mode。CFMutableSetRef _commonModes中包含了_modes中被标记为common mode的mode name,CFMutableSetRef _commonModeItems中包含了应当被添加到common mode中的mode item。具体在CFRunLoopModeRef中我们还将讨论它。

CFRunLoopModeRef代表RunLoop的运行模式。
一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个被称为mode item的Source、Timer、Observer。
RunLoop启动、运行时,只能且必须指定其中一个 Mode,这个Mode被称作Current Mode。
如果需要切换Mode,只能退出当前Mode,再重新指定一个Mode进入,这样做主要是为了分隔开不同组的Source、Timer、Observer,让其互不影响。如果Mode里没有任何Source0/Source1/Timer,RunLoop会立马退出。

CFRunLoopModeRef示意图

4.1.3 CFRunLoopRef的操作

CFRunLoopRun
Runs the current thread’s CFRunLoop object in its default mode indefinitely.
让当前线程的RunLoop在default mode下运行。

CFRunLoopRunInMode
Runs the current thread’s CFRunLoop object in a particular mode.
让当前线程的RunLoop在指定的mode下运行。

CFRunLoopWakeUp
Wakes a waiting CFRunLoop object.
唤醒一个休眠的RunLoop。

CFRunLoopStop
Forces a CFRunLoop object to stop running.
停止该RunLoop的运行。

CFRunLoopIsWaiting
Returns a Boolean value that indicates whether the run loop is waiting for an event.
判断一个RunLoop是否在休眠,等待事件唤醒。

4.2 CFRunLoopModeRef

4.2.1 mode种类

iOS上系统默认注册的5个Mode,苹果公开提供的 Mode有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。

这五种运行模式分别是:

  1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
  5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode。

这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。

4.2.2 CFRunLoopModeRef的结构

CFRunLoopModeRef 其实是指向__CFRunLoopMode结构体的指针,__CFRunLoopMode结构体源码如下:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

主要查看以下成员变量

CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

CFRunLoopModeRef中我们已经讨论了CFRunLoopModeRef、CFRunLoopModeRef、mode item之间的关系,这里不再赘述。

mode item包括Source1/Source0/Timers/Observer,它们分别代表什么?

  1. Source1 : 基于Port的线程间通信
  2. Source0 : 非Port的事件,包括触摸事件,不带延迟的Thread相关的PerformSelectors等
  3. Timers : 定时器,NSTimer
  4. Observer : 监听器,用于监听RunLoop的状态
4.2.3 RunLoop mode的操作

在Core Foundation中,针对Mode的操作,苹果只开放了以下3个API(Cocoa中也有功能一样的函数,不再列出):

CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
Adds a mode to the set of run loop common modes.
向当前RunLoop的common modes中添加一个mode。

CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
CFArray
返回当前运行的mode的name

Ref CFRunLoopCopyAllModes(CFRunLoopRef rl)
返回当前RunLoop的所有mode

CFRunLoopAddCommonMode

我们没有办法直接创建一个CFRunLoopMode对象,但是我们可以调用CFRunLoopAddCommonMode传入一个字符串向RunLoop中添加Mode,传入的字符串即为Mode的名字,RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。下面来看一下源码。

void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    //看rl中是否已经有这个mode,如果有就什么都不做
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
        CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
        //把modeName添加到RunLoop的_commonModes中
        CFSetAddValue(rl->_commonModes, modeName);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, modeName};
            /* add all common-modes items to new mode */
            //这里调用CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的时候会调用
            //__CFRunLoopFindMode(rl, modeName, true),CFRunLoopMode对象在这个时候被创建
            CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
            CFRelease(set);
        }
    } else {
    }
    __CFRunLoopUnlock(rl);
}

可以看得出:

1.modeName不能重复,modeName是mode的唯一标识符。
2.RunLoop的_commonModes数组存放所有被标记为common的mode的名称。
3.添加commonMode会把commonModeItems数组中的所有source同步到新添加的mode中
4.CFRunLoopMode对象在CFRunLoopAddItemsToCommonMode函数中调用CFRunLoopFindMode时被创建

CFRunLoopCopyCurrentMode/CFRunLoopCopyAllModes

CFRunLoopCopyCurrentMode和CFRunLoopCopyAllModes的内部逻辑比较简单,直接取RunLoop的_currentMode和_modes返回,就不贴源码了。

4.2.4 Mode间的切换

在主线程中Schedule一个Timer并正常运行,然后滑动界面上的TableView或UIScrollView时,之前的Timer不触发了。为什么呢?

[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer1) userInfo:nil repeats:YES];

这个问题也与RunLoop的RunLoopMode有关。因为schedule一个Timer时,Timer是被加到了RunLoop的NSDefaultRunLoopMode下,而在滑动TableView或UIScrollView时,RunLoop的Mode被切换到了UITrackingRunLoopMode下,RunLoop就只会去跟踪UI上的滑动事件了而Timer被暂停不会被触发了。

CF框架内并没有实现Mode切换的功能,所以UIKit在使用的时候需要自行处理。一般的过程是UIApplication push一个即将要执行的mode,然后wakeup RunLoop。切换回来的时候,只需要pop这个mode,然后再wakeup即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FlolCpqS-1652108715759)(https://upload-images.jianshu.io/upload_images/1049809-74da0b6560623ec5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000)]

4.3 CFRunLoopSourceRef事件源

4.3.1 CFRunLoopSource的种类

CFRunLoopSource
在我 RunLoopMode 数据结构代码中可以看到这两个东西CFMutableSetRef _source0 和 CFMutableSetRef _source1,首先这两个东西是 Set(集合),集合中存放的是一堆数据结构,那这个 source 到底是个什么东西呢,他们其实也是一个数据结构 CFRunLoopSourceRef。

CFRunLoopSource是对input sources的抽象。CFRunLoopSource分source0和source1两个版本。

Source0:非基于Port的,用于用户主动触发的事件(点击button或点击屏幕,不带delay的Thread相关的PerformSelectors是source0,带delay的是timer)。
Source1:基于Port的,通过内核和其他线程相互发送消息(与内核相关)。

4.3.2 CFRunLoopSourceRef的结构

CFRunLoopSourceRef是指向__CFRunLoopSource结构体的指针,它的结构如下:

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;   //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;
    union {
      CFRunLoopSourceContext version0;    /* source0的数据结构 */
        CFRunLoopSourceContext1 version1;    /* source1的数据结构 */
    } _context;
};

数据结构__CFRunLoopSource中包含一个 _context成员,他的类型是 CFRunLoopSourceContext 或者是 CFRunLoopSourceContext1,指明了该source的类型。

source0
source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态,以下是source0的结构体:

//source0
typedef struct {
    CFIndex    version;  // 版本号,用来区分是source1还是source0
    void *    info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef    (*copyDescription)(const void *info);
    Boolean    (*equal)(const void *info1, const void *info2);
    CFHashCode    (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

source1
source1由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是source1的结构体

//source1
typedef struct {
    CFIndex    version;  // 版本号,用来区分是source1还是source0
    void *    info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef    (*copyDescription)(const void *info);
    Boolean    (*equal)(const void *info1, const void *info2);
    CFHashCode    (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t    (*getPort)(void *info);  // 端口
    void *    (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *    (*getPort)(void *info);
    void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

Source1除了包含回调指针外包含一个mach port,Source1可以监听系统端口,通过内核和其他线程通信,接收、分发系统事件,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)。官方也指出可以自定义Source,因此对于CFRunLoopSourceRef来说它更像一种协议,框架已经默认定义了两种实现,如果有必要开发人员也可以自定义,详细情况可以查看官方文档

4.3.3 CFRunLoopSource的操作

CFRunLoopAddSource
CFRunLoopAddSource的代码结构如下:

//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {    /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rls)) return;
    Boolean doVer0Callout = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes
    if (modeName == kCFRunLoopCommonModes) {
        //如果runloop的_commonModes存在,则copy一个新的复制给set
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
       //如果runl _commonModeItems为空
        if (NULL == rl->_commonModeItems) {
            //先初始化
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        //把传入的CFRunLoopSourceRef加入_commonModeItems
        CFSetAddValue(rl->_commonModeItems, rls);
        //如果刚才set copy到的数组里有数据
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rls};
            /* add new item to all common-modes */
            //则把set里的所有mode都执行一遍__CFRunLoopAddItemToCommonModes函数
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
        //以上分支的逻辑就是,如果你往kCFRunLoopCommonModes里面添加一个source,那么所有_commonModes里的mode都会添加这个source
    } else {
        //根据modeName查找mode
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        //如果_sources0不存在,则初始化_sources0,_sources0和_portToV1SourceMap
        if (NULL != rlm && NULL == rlm->_sources0) {
            rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
        }
        //如果_sources0和_sources1中都不包含传入的source
        if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
            //如果version是0,则加到_sources0
            if (0 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources0, rls);
                //如果version是1,则加到_sources1
            } else if (1 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources1, rls);
                __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                if (CFPORT_NULL != src_port) {
                    //此处只有在加到source1的时候才会把souce和一个mach_port_t对应起来
                    //可以理解为,source1可以通过内核向其端口发送消息来主动唤醒runloop
                    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
                    __CFPortSetInsert(src_port, rlm->_portSet);
                }
            }
            __CFRunLoopSourceLock(rls);
            //把runloop加入到source的_runLoops中
            if (NULL == rls->_runLoops) {
                rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
            }
            CFBagAddValue(rls->_runLoops, rl);
            __CFRunLoopSourceUnlock(rls);
            if (0 == rls->_context.version0.version) {
                if (NULL != rls->_context.version0.schedule) {
                    doVer0Callout = true;
                }
            }
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
    if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
    }
}

通过添加source的这段代码可以得出如下结论:

如果modeName传入kCFRunLoopCommonModes,则该source会被保存到RunLoop的_commonModeItems中,并被添加到所有common Mode的mode items中。
如果modeName传入的不是kCFRunLoopCommonModes,则会先查找该Mode,如果没有,会创建一个
同一个source在一个mode中只能被添加一次

CFRunLoopRemoveSource

remove操作和add操作的逻辑基本一致,很容易理解。

//移除source
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
    CHECK_FOR_FORK();
    Boolean doVer0Callout = false, doRLSRelease = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes,则从_commonModes的所有mode中移除该source
    if (modeName == kCFRunLoopCommonModes) {
        if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rls)) {
            CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
            CFSetRemoveValue(rl->_commonModeItems, rls);
            if (NULL != set) {
                CFTypeRef context[2] = {rl, rls};
                /* remove new item from all common-modes */
                CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context);
                CFRelease(set);
            }
        } else {
        }
    } else {
        //根据modeName查找mode,如果不存在,返回NULL
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false);
        if (NULL != rlm && ((NULL != rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL != rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) {
            CFRetain(rls);
            //根据source版本做对应的remove操作
            if (1 == rls->_context.version0.version) {
                __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                if (CFPORT_NULL != src_port) {
                    CFDictionaryRemoveValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port);
                    __CFPortSetRemove(src_port, rlm->_portSet);
                }
            }
            CFSetRemoveValue(rlm->_sources0, rls);
            CFSetRemoveValue(rlm->_sources1, rls);
            __CFRunLoopSourceLock(rls);
            if (NULL != rls->_runLoops) {
                CFBagRemoveValue(rls->_runLoops, rl);
            }
            __CFRunLoopSourceUnlock(rls);
            if (0 == rls->_context.version0.version) {
                if (NULL != rls->_context.version0.cancel) {
                    doVer0Callout = true;
                }
            }
            doRLSRelease = true;
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
    if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.cancel(rls->_context.version0.info, rl, modeName);   /* CALLOUT */
    }
    if (doRLSRelease) CFRelease(rls);
}

CFRunLoopContainsSource
Boolean CFRunLoopContainsSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
判断RunLoop中指定mode下是否存在指定的source

这里简单总结一下:
1.CFRunLoopSourceRef 是事件产生的地方;
2.这个 CFRunLoopSourceRef 有两个版本就是 source0 和 source1;
3.source0只包含一个回调(函数指针),不能主动出发事件,需要 CFRunLoopSourceSignal(source) 将 Source 标记为待处理,CFRunLoopWakeUp(runloop) 唤醒 RunLoop,让其处理事件
4.source1包含mach_port 和一个回调(函数指针),用于通过内核和其它线程相互发送消息,能主动唤醒 RunLoop。
5.input sources分发异步事件到对应的处理者,引起runUntilDate:退出;而timer分发同步事件给它们的处理者,不会引起RunLoop退出。
6.perform selector是一种custom input source。perform selector请求在目标线程上被串行化接收。selector被执行后就会将自身从runloop中移除,而基于端口的source不会。runloop每次运行会处理所有的selector,而不是每次处理一个。

4.4 CFRunLoopTimerRef

CFRunLoopTimerRef是基于时间的触发器,它和 NSTimer是toll-free bridged 的,可以混用。因此,NSTimer是对RunLoopTimer的封装。CFRunLoopTimerRef包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

4.4.1 CFRunLoopTimerRef的结构

CFRunLoopTimerRef是指向__CFRunLoopTimer结构的指针。

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;		/* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;			/* TSR units */
    CFIndex _order;			/* immutable */
    CFRunLoopTimerCallBack _callout;	/* immutable */
    CFRunLoopTimerContext _context;	/* immutable, except invalidation */
};
4.4.2CFRunLoopTimerRef的操作

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
向指定RunLoop的指定mode下添加指定的timer

void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
移除指定RunLoop的指定mode下的指定的timer

Boolean CFRunLoopContainsTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
判断指定RunLoop的指定mode下是否存在指定的timer

CFAbsoluteTime CFRunLoopGetNextTimerFireDate(CFRunLoopRef rl, CFRunLoopMode mode);
获取指定RunLoop的指定mode下注册的所有timer的最近的触发时间

####4.4.3 Timer的实现
由于篇幅原因,在另外一篇里介绍。

4.5CFRunLoopObserverRef

4.5.1RunLoop的状态

CFRunLoopObserverRef是消息循环中的一个监听器,能够监听RunLoop的状态改变,随时通知外部当前RunLoop的运行状态(它包含一个函数指针_callout_将当前状态及时告诉观察者)。

RunLoop 的状态(CFOptionFlags)包括下面几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1),        // runLoop即将处理 Timers
    kCFRunLoopBeforeSources = (1UL << 2),       // runLoop即将处理 Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),       // runLoop即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),        // runLoop刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),                // 即将退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU       
};
4.5.2CFRunLoopObserverRef的结构

CFRunLoopObserverRef的结构如下:

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;    //observer对应的runLoop
    CFIndex _rlCount;              //observer当前监测的runLoop数量
    CFOptionFlags _activities;   //observer观测runLoop的状态,枚举类型
    CFIndex _order;            //CFRunLoopMode中是数组形式存储的observer,_order就是他在数组中的位置
    CFRunLoopObserverCallBack _callout;    //observer观察者的函数回调
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};

这里有个_order字段,决定了observer的位置,数值越小越靠前,就能更早被调用到。类似地,__CFRunLoopSource,__CFRunLoopTimer以及__CFRunLoopObserver都有这么字段。在7.1 AutoreleasePool中还会详细介绍_order的用处。

这里我们要注意_callout这个字段。在开发过程中几乎所有的操作都是通过Call out进行回调的(无论是Observer的状态通知还是Timer、Source的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听Observer也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数):

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
    static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

例如__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info)内部会调用rlo->_callout指向的函数。

我们在控制器的touchBegin中打入断点查看堆栈(由于UIEvent是Source0,所以可以看到一个Source0的Call out函数CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION调用):

4.5.3CFRunLoopObserverRef的操作

void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
向RunLoop指定mode下添加指定观察者

void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef * mode)
移除RunLoop指定mode下的指定观察者

Boolean CFRunLoopContainsObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
判断RunLoop指定mode下是否存在指定观察者

讨论:
添加observer和timer的内部逻辑和添加source大体类似。
区别在于observer和timer只能被添加到一个RunLoop的一个或者多个mode中,比如一个timer被添加到主线程的RunLoop中,则不能再把该timer添加到子线程的RunLoop,而source没有这个限制,不管是哪个RunLoop,只要mode中没有,就可以添加。
这个区别在前面的一些结构体中也可以发现,CFRunLoopSource结构体中有保存RunLoop对象的数组,而CFRunLoopObserver和CFRunLoopTimer只有单个RunLoop对象。

5. RunLoop的启动与退出

5.1RunLoop的启动

Foundation中RunLoop的启动
Foundation中启动一个runloop有以下三种方法,这三种方式无论通过哪一种方式启动runloop,如果没有一个输入源或者timer附加于runloop上,runloop就会立刻退出。

  • (void)run;
  • (void)runUntilDate:(NSDate *)limitDate;
  • (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

(1) 第一种方式,runloop会一直运行下去,在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;
(2) 第二种方式,可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;
(3) 第三种方式,runloop的一次执行,被指定mode中的input source所锁定,超时时间到达或者第一个input source被处理,本次执行就退出。

前两种启动方式会重复调用runMode:beforeDate:方法。

CF中RunLoop的启动

我们来看CF中启动RunLoop的源码:

 // 用DefaultMode启动
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

//指定mode的一次运行
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

我们发现RunLoop确实是do while通过判断result的值实现的。

5.2RunLoop的退出

1.主线程销毁,RunLoop退出。(线程销毁退出)
2.Mode中有一些Timer 、Source,这些保证Mode不为空时,RunLoop没有空转并且是在运行的,当Mode中为空的时候,RunLoop会立刻退出。(但是苹果并不建议我们这么做,因为系统内部有可能会在当前线程的runloop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证runloop一定会退出。)(无mode item退出)
3.我们在启动RunLoop的时候可以设置什么时候停止。(超时退出)

NSRunLoop的退出方式

6. RunLoop处理逻辑

6.1 RunLoop事件处理

前面我们已经介绍了CF中RunLoop的启动,RunLoop是如何运行的?这就跟其中的函数CFRunLoopRunSpecific有关了。

在CFRunLoopRun函数中调用了CFRunLoopRunSpecific函数,runloop参数传入当前RunLoop对象,modeName参数传入kCFRunLoopDefaultMode。

在CFRunLoopRunInMode函数中也调用了CFRunLoopRunSpecific函数,runloop参数传入当前RunLoop对象,modeName参数继续传递CFRunLoopRunInMode传入的modeName。

我们来看看CFRunLoopRunSpecific的源码。

/*
 * 指定mode运行runloop
 * @param rl 当前运行的runloop
 * @param modeName 需要运行的mode的name
 * @param seconds  runloop的超时时间
 * @param returnAfterSourceHandled 是否处理完事件就返回
 */
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    //根据modeName找到本次运行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //如果没找到 || mode中没有注册任何事件,则就此停止,不进入循环
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    //保存上一次运行的mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    //更新本次mode
    rl->_currentMode = currentMode;
    //初始化一个result为kCFRunLoopRunFinished
    int32_t result = kCFRunLoopRunFinished;
    
    // 1.通知observer即将进入runloop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //10.通知observer已退出runloop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

通过CFRunLoopRunSpecific的内部逻辑,我们可以得出:

  1. 如果指定了一个不存在的mode来运行RunLoop,那么会失败,mode不会被创建,所以这里传入的mode必须是存在的
  2. 如果指定了一个mode,但是这个mode中不包含任何modeItem,那么RunLoop也不会运行,所以必须要传入至少包含一个modeItem的mode
  3. 在进入run loop之前通知observer,状态为kCFRunLoopEntry
  4. 在退出run loop之后通知observer,状态为kCFRunLoopExit

RunLoop的运行的最核心函数是__CFRunLoopRun,接下来我们分析__CFRunLoopRun的源码。
__CFRunLoopRun:

/**
 *  运行run loop
 *
 *  @param rl              运行的RunLoop对象
 *  @param rlm             运行的mode
 *  @param seconds         run loop超时时间
 *  @param stopAfterHandle true:run loop处理完事件就退出  false:一直运行直到超时或者被手动终止
 *  @param previousMode    上一次运行的mode
 *
 *  @return 返回4种状态
 */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    //获取系统启动后的CPU运行时间,用于控制超时时间
    uint64_t startTSR = mach_absolute_time();
    
    //如果RunLoop或者mode是stop状态,则直接return,不进入循环
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
    
    //mach端口,在内核中,消息在端口之间传递。 初始为0
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    //判断是否为主线程
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    //如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    if (rlm->_queue) {
        //mode赋值为dispatch端口_dispatch_runloop_root_queue_perform_4CF
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }
#endif
    
    //GCD管理的定时器,用于实现runloop超时机制
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    
    //立即超时
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    }
    //seconds为超时时间,超时时执行__CFRunLoopTimeout函数
    else if (seconds <= TIMER_INTERVAL_LIMIT) {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
        timeout_context->ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    }
    //永不超时
    else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }
    
    //标志位默认为true
    Boolean didDispatchPortLastTime = true;
    //记录最后runloop状态,用于return
    int32_t retVal = 0;
    do {
        //初始化一个存放内核消息的缓冲池
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;
#endif
        //取所有需要监听的port
        __CFPortSet waitSet = rlm->_portSet;
        
        //设置RunLoop为可以被唤醒状态
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
        //2.通知observer,即将触发timer回调,处理timer事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //3.通知observer,即将触发Source0回调
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        //执行加入当前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        //4.处理source0事件
        //有事件处理返回true,没有事件返回false
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //执行加入当前runloop的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //如果没有Sources0事件处理 并且 没有超时,poll为false
        //如果有Sources0事件处理 或者 超时,poll都为true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        //第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            //从缓冲区读取消息
            msg = (mach_msg_header_t *)msg_buffer;
            //5.接收dispatchPort端口的消息,(接收source1事件)
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
                //如果接收到了消息的话,前往第9步开始处理msg
                goto handle_msg;
            }
#endif
        }
        
        didDispatchPortLastTime = false;
        
        //6.通知观察者RunLoop即将进入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //设置RunLoop为休眠状态
        __CFRunLoopSetSleeping(rl);
        // do not do any user callouts after this point (after notifying of sleeping)
        
        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.
        
        __CFPortSetInsert(dispatchPort, waitSet);
        
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //这里有个内循环,用于接收等待端口的消息
        //进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
        do {
            if (kCFUseCollectableAllocator) {
                objc_clear_stack(0);
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            //7.__CFRunLoopServiceMachPort调用mach_msg等待接受mach_port的消息。线程将进入休眠,知道被下面某个事件唤醒:一个基于Port的Source事件,也就是Source1;一个Timer到时间了;RunLoop自身超时时间到了;被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
            //收到消息之后,livePort的值为msg->msgh_local_port,
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        if (kCFUseCollectableAllocator) {
            objc_clear_stack(0);
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif      
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        
        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.
        
        __CFPortSetRemove(dispatchPort, waitSet);
    __CFRunLoopSetIgnoreWakeUps(rl);
        
        // user callouts now OK again
        //取消runloop的休眠状态
        __CFRunLoopUnsetSleeping(rl);
        //8.通知观察者runloop被唤醒
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
      
        //9.处理收到的消息
    handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);
        
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
            //通过CFRunloopWake唤醒
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            //什么都不干,跳回2重新循环
            // do nothing on Mac OS
       }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //如果是定时器事件
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            //9.1 处理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        //如果是定时器事件
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
           //9.1处理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        //如果是dispatch到main queue的block
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
           //9.2执行block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            // 有source1事件待处理
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
                mach_msg_header_t *reply = NULL;
                //9.2 处理source1事件
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
                if (NULL != reply) {
                    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
                }
#endif
            }
        }
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
        
        __CFRunLoopDoBlocks(rl, rlm);
        //判断是否要退出 RunLoop
        if (sourceHandledThisLoop && stopAfterHandle) {
            //进入run loop时传入的参数,处理完事件就返回
            retVal = kCFRunLoopRunHandledSource;
        }else if (timeout_context->termTSR < mach_absolute_time()) {
            //run loop超时
            retVal = kCFRunLoopRunTimedOut;
        }else if (__CFRunLoopIsStopped(rl)) {
            //run loop被手动终止
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        }else if (rlm->_stopped) {
            //调用_CFRunLoopStopMode使mode被终止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        }else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            //mode中没有要处理的事件
            retVal = kCFRunLoopRunFinished;
        }
        //除了上面这几种情况,都继续循环
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    
    return retVal;
}

这里要注意的一点是宏USE_DISPATCH_SOURCE_FOR_TIMERS在Mac上是1,在iOS上是0.因此这里有些代码在iOS上并不存在。
在iOS上__CFRunLoopRun只有一层do-while循环,可以看做是RunLoop的整个生命周期,直到满足一些条件后,循环结束,意味着RunLoop也结束了。这些条件包括source被处理且要求source被处理后RunLoop需要被停下来,RunLoop超时,RunLoop的当前mode被停止,当前mode下的mode item为空了。__CFRunLoopServiceMachPort用来处理休眠及唤醒。
根据官网描述,每次RunLoop启动的时候,线程的RunLoop都会去处理一些待处理的事件,为注册的观察者产生通知。处理的顺序是特定的:
1.通知observers:进入RunLoop。
2.通知observers:即将处理timer。
3.通知observers:即将处理source0
4.处理就绪的source0,没有处理的话后续会进入休眠,处理的话不会休眠
5.如果当前是主线程的runloop,并且主线程有事件要处理,跳到第9步,也就是不会休眠;但是没有要处理的事件,也会进入休眠。
6.通知observers:线程即将休眠
7.使线程休眠,直到下列任一种事件发生

  • source1事件发生
  • timer触发
  • RunLoop本身超时
  • RunLoop被手动唤醒

8.通知observers:线程被唤醒
9.处理待处理的事件

  • 如果用户定义的timer触发了,处理该timer并重启循环,转到2
  • 如果当前是主线程的runloop,处理主线程的事件,来源只有5
  • source1发生,处理该事件。(如果调用的是CFRunLoopRun,就转到2;调用的是CFRunLoopRunInMode,根据最后一个参数是结束循环还是跳到2继续循环)
  • 如果该RunLoop被手动唤醒,并且没有超时,就重启循环,转到2

10.通知observers:RunLoop已退出

下面的图不一定准确,以上面的总结为准
RunLoop处理逻辑
注意:左图绿色区没有source0

更详细版本

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yysFnppD-1652108715762)(http://mrpeak.cn/images/rl00.png)]
上图将performTask 和 callout_to_observer 用不同的颜色加以了区分,从图中可以直观的看到 5 种 performTask 和 6 种 callout_to_observer 是在一次 loop 里如何分布的。

有些细节难以在图中体现,再单独拿出来解释下。

Poll?

每次 loop 如果处理了source0任务或者RunLoop超时了,那么 poll 值会为 true,直接的影响是不会DoObservers-BeforeWaiting 和 DoObservers-AfterWaiting,也就是说runloop不会进入睡眠,因此也就没有BeforeWaiting 和 AfterWaiting这两个activity。

两次 mach_msg

其实一次loop 里有两次调用 mach_msg,有一次没有标记出来的是发生在 DoSource0 之后,会主动去读取和 mainQueue 相关的 msg 队列,这不过这个 mach_msg 调用是不会进入睡眠的,因为 timeout 值传入的是 0,如果读到了消息,就直接 goto 到 DoMainQueue 的代码,这种设计应该是为了保障 dispatch 到 main queue 的代码总是有较高的机会得以运行。

Port Type

每次 runloop 被唤醒之后,会根据 port type 而决定到底执行哪一类任务,DoMainQueue,DoTimers,DoSource1三者只会运行一个,剩下的会留到下一次loop里去执行。

6.2 RunLoop的休眠与唤醒

其实对于Event Loop而言RunLoop最核心的事情就是保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。RunLoop的这个机制完全依靠系统内核来完成,具体来说是苹果操作系统核心组件Darwin中的Mach来完成的(Darwin是开源的)。可以从下图最底层Kernel中找到Mach:

Mach是Darwin的核心,可以说是内核的核心,提供了进程间通信(IPC)、处理器调度等基础服务。在Mach中,进程、线程间的通信是以消息的方式来完成的,消息在两个Port之间进行传递(这也正是Source1之所以称之为Port-based Source的原因,因为它就是依靠系统发送消息到指定的Port来触发的)。消息的发送和接收使用<mach/message.h>中的mach_msg()函数(事实上苹果提供的Mach API很少,并不鼓励我们直接调用这些API):

而mach_msg()的本质是一个调用mach_msg_trap(),这相当于一个系统调用,会触发内核状态切换。当程序静止时,RunLoop停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而这个函数内部就是调用了mach_msg让程序处于休眠状态。

__CFRunLoopServiceMachPort:

/**
 *  接收指定内核端口的消息
 *
 *  @param port        接收消息的端口
 *  @param buffer      消息缓冲区
 *  @param buffer_size 消息缓冲区大小
 *  @param livePort    暂且理解为活动的端口,接收消息成功时候值为msg->msgh_local_port,超时时为MACH_PORT_NULL
 *  @param timeout     超时时间,单位是ms,如果超时,则RunLoop进入休眠状态
 *
 *  @return 接收消息成功时返回true 其他情况返回false
 */
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {      /* In that sleep of death what nightmares may come ... */
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        msg->msgh_bits = 0;  //消息头的标志位
        msg->msgh_local_port = port;  //源(发出的消息)或者目标(接收的消息)
        msg->msgh_remote_port = MACH_PORT_NULL; //目标(发出的消息)或者源(接收的消息)
        msg->msgh_size = buffer_size;  //消息缓冲区大小,单位是字节
        msg->msgh_id = 0;  //唯一id
       
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
        
        //通过mach_msg发送或者接收的消息都是指针,
        //如果直接发送或者接收消息体,会频繁进行内存复制,损耗性能
        //所以XNU使用了单一内核的方式来解决该问题,所有内核组件都共享同一个地址空间,因此传递消息时候只需要传递消息的指针
        ret = mach_msg(msg,
                       MACH_RCV_MSG|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
                       0,
                       msg->msgh_size,
                       port,
                       timeout,
                       MACH_PORT_NULL);
        CFRUNLOOP_WAKEUP(ret);
        
        //接收/发送消息成功,给livePort赋值为msgh_local_port
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
        
        //MACH_RCV_TIMEOUT
        //超出timeout时间没有收到消息,返回MACH_RCV_TIMED_OUT
        //此时释放缓冲区,把livePort赋值为MACH_PORT_NULL
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
        
        //MACH_RCV_LARGE
        //如果接收缓冲区太小,则将过大的消息放在队列中,并且出错返回MACH_RCV_TOO_LARGE,
        //这种情况下,只返回消息头,调用者可以分配更多的内存
        if (MACH_RCV_TOO_LARGE != ret) break;
        //此处给buffer分配更大内存
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}

mach_msg
我们先来了解下mach_msg的入参:
第一个参数就发送消息内容msg,msg的结构体里定义了消息的收发两方的port及其他内容;
第二个参数属于消息发送或接收的类型,通过宏定义已经定义好类型,发送为 MACH_SEND_MSG,接收为 MACH_RCV_MSG;
第三个参数应该是用于接收者申请额外存储空间暂存消息,便于自己处理,不对源消息的空间有耦合;
倒数第二个参数表示等待时间,如果是0表示发送或接收后立刻返回,如果是TIMEOUT_INFINITY,就阻塞地等待有消息,当前线程会一直处于休眠状态,直到参数1对应port有消息才返回,继续执行后面的代码;

mach port
在runloop中多次提到port,比如基于port的source1就是就是休眠时候的唤醒源之一,比如休眠时监听消息 __CFRunLoopServiceMachPort也是通过port。
那么port是啥?Mach消息是在端口(Port)之间进行传递。一个端口只能有一个接收者,而可以同时有多个发送者。向一个端口发送消息,实际上是将消息放在一个消息队列中,直到消息能被接收者处理。
从源码中可见port类型是__CFPort /mach_port_name_t /mach_port_t,而mach_port_name_t就是无符号整数,也就是端口的索引值。源码中涉及到的port有几种类型:

// 这个port就对应NSTimer;
    mach_port_t _timerPort;
    
// 这个port对应主线程
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    dispatchPort = _dispatch_get_main_queue_port_4CF();
    
// 这个port唤醒runloop
if (livePort == rl->_wakeUpPort)

还记得__CFRunLoopRun方法中休眠时监听的port集合吗?

// 第七步,进入循环开始不断的读取端口信息,如果端口有唤醒信息则唤醒当前runLoop
__CFPortSet waitSet = rlm->_portSet;
...
...
if (kCFUseCollectableAllocator) 
{
    memset(msg_buffer, 0, sizeof(msg_buffer));
}

// waitSet 为所有需要监听的port集合, TIMEOUT_INFINITY表示一直等待
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

而这里的waitSet就是__CFPortSet ,也就是port的集合,那么__CFPortSet是什么类型呢?这个集合涉及哪些操作呢?

typedef mach_port_t __CFPortSet;
...
...
CF_INLINE kern_return_t __CFPortSetInsert(__CFPort port, __CFPortSet portSet) {
    if (MACH_PORT_NULL == port) {
        return -1;
    }
    return mach_port_insert_member(mach_task_self(), port, portSet);
}

也就说__CFPortSet的类型也是mach_port_t,即无符号整数。那么__CFPortSetInsert操作猜测应该就是按bit位来操作,不同bit位表示不同的port类型。__CFRunLoopServiceMachPort的参数入参__CFPort类型自然也可以传入waitSet,在其内部遍历各个bit位来监听各个port的消息。
另外,runloop休眠阶段的轮询的port集合是如何确定的呢?通过源码发现,正是__CFRunLoopFindMode方法中将各个port插入到waitSet中的:

static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create)
 {
    ...
    ...
    kern_return_t ret = KERN_SUCCESS;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    rlm->_timerFired = false;
    rlm->_queue = _dispatch_runloop_root_queue_create_4CF("Run Loop Mode Queue", 0);
    mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
    if (queuePort == MACH_PORT_NULL) CRASH("*** Unable to create run loop mode queue port. (%d) ***", -1);
    rlm->_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, rlm->_queue);
    
    __block Boolean *timerFiredPointer = &(rlm->_timerFired);
    dispatch_source_set_event_handler(rlm->_timerSource, ^{
        *timerFiredPointer = true;
    });
    
    // Set timer to far out there. The unique leeway makes this timer easy to spot in debug output.
    _dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 321);
    dispatch_resume(rlm->_timerSource);
    
    ret = __CFPortSetInsert(queuePort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
    
#endif
#if USE_MK_TIMER_TOO
    rlm->_timerPort = mk_timer_create();
    ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
    
    ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret);
  
    CFSetAddValue(rl->_modes, rlm);
    CFRelease(rlm);
    __CFRunLoopModeLock(rlm);   /* return mode locked */
    return rlm;
}

从上面的三个__CFPortSetInsert可以发现分别插入了queuePort、_timerPort、_wakeUpPort;另外在CFRunLoopAddSource方法中还将source1的port插入其中:

......
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port) 
{
    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
    __CFPortSetInsert(src_port, rlm->_portSet);
}
......

再加上__CFRunLoopRun方法中加入的dispatchPort ,至此,waitSet中已经包含了可以唤醒runloop的所有port。

6.3 RunLoop的超时处理

在runloop源码的核心方法__CFRunLoopRun中,在进入核心的 do while循环之前,先使用 dispatch启动了一个计时器。

超时时间是根据 __CFRunLoopRun 的入参seconds计算的,而__CFRunLoopRun入参从哪里来,顺着源码可以找到 CFRunLoopRunSpecific。调用方CFRunLoopRun设置的默认超时时间是1.0e10,CFRunLoopRunInMode则需要自定义。

dispatch计时器达到超时时间,会调用dispatch_source_set_event_handler_f 中配置的回调函数(也可以用dispatch_source_set_event_handler配置block),这里的回调函数是__CFRunLoopTimeout,源码如下:

static void __CFRunLoopTimeout(void *arg) {
    struct __timeout_context *context = (struct __timeout_context *)arg;
    context->termTSR = 0ULL;
    CFRUNLOOP_WAKEUP_FOR_TIMEOUT();// 没啥X用
    CFRunLoopWakeUp(context->rl);
    // The interval is DISPATCH_TIME_FOREVER, so this won't fire again
}
void CFRunLoopWakeUp(CFRunLoopRef rl) {
    ......
    ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0);
    if (ret != MACH_MSG_SUCCESS && ret != MACH_SEND_TIMED_OUT) CRASH("*** Unable to send message to wake up port. (%d) ***", ret);
    ......
}
static uint32_t __CFSendTrivialMachMessage(mach_port_t port, uint32_t msg_id, CFOptionFlags options, uint32_t timeout) {
    kern_return_t result;
    mach_msg_header_t header;
    ......
    result = mach_msg(&header, MACH_SEND_MSG|options, header.msgh_size, 0, MACH_PORT_NULL, timeout, MACH_PORT_NULL);
    if (result == MACH_SEND_TIMED_OUT) mach_msg_destroy(&header);
    return result;
}

从上面源码可以看出超时到达时,主要做的就是通过 __CFSendTrivialMachMessage再调用mach_msg发送消息,mach_msg参数已经配置了“发送模式”、“超时时间”、唤醒的端口为“rl->_wakeUpPort”,基于文章《runloop你理解对了吗》, 我们可以知道,runloop在休眠时,接收到mach发来的消息,会判断port,决定作何判断和处理:

if (MACH_PORT_NULL == livePort)
{
      CFRUNLOOP_WAKEUP_FOR_NOTHING();
}
else if (livePort == rl->_wakeUpPort)
{
      CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort)
{
      // 处理timer
}
else if (livePort == dispatchPort) 
{
      ......
      // 处理主线程队列中事件
      __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
      ......
}
else 
{
      ......
      // 处理Source1
      sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
      ......
}

也就是我们通过dispatch 定时器的可以将超时的消息通过mach ,唤醒runloop,然后执行else if (livePort == rl->_wakeUpPort)分支来处理超时(目前源码中CFRUNLOOP_WAKEUP_FOR_WAKEUP 只是一个空的宏定义,未做任何处理)。所以这个计时器是用来在指定时间后将context->termTSR = 0ULL然后唤醒runloop。在后面的代码中根据context->termTSR来判断是否超时,如果超时就退出RunLoop。

###6.4 RunLoop的观察者
所谓 Runloop,简而言之,是 Apple 所设计的,一种在当前线程,持续调度各种任务的运行机制。

每一次 loop 执行,主要做三件事:

  • callout_to_observer()
  • sleep()
  • performTask()

sleep前面我们已经讨论过了,performTask放到后面部分继续讨论,这一部分主要讨论observer。

前面我们已经知道了RunLoop有六种状态,runloop使用callout_to_observer来通知外部 observer,用来告知外部某个任务已被执行,或者是runloop 当前处于什么状态。

DoObservers-Timer

故名思义,在 DoTimers执行之前,调用 DoObservers-Timer来告知感兴趣的observer。runloop 是通过如下函数来通知 observer:

__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

DoObservers-Source0

同理,是在执行source0之前,调用 DoObservers-Sources来告知感兴趣的 observer。runloop 通过如下函数来通知 observer:

__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

这是上述五种执行任务方式中,两种可以注册 observer的,其他几个都不支持,mainQueue,source1,block都不行。但是就目前看,kCFRunLoopBeforeTimers通知发出后,是否执行DoTimers是不确定的。

DoObservers-Activity
这是 runloop 用来通知外部自己当前状态用的,当前 runloop 正执行到哪个activity,那么一共有几种 activity 呢?看源码一清二楚:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

啰嗦下,再一个个讲解:

kCFRunLoopEntry

每次 runloop 重新进入时的 activity,runloop 每一次进入一个 mode,就通知一次外部 kCFRunLoopEntry,之后会一直以该 mode 运行,直到当前 mode 被终止,进而切换到其他 mode,并再次通知 kCFRunLoopEntry。

kCFRunLoopBeforeTimers

这就是上面提到的 DoObservers-Timer,Apple 应该是为了代码的整洁,将timer callout也归为了一种 activity,其含义上面已经介绍,不再赘述。

kCFRunLoopBeforeSources

同理,Apple将该调用source callout归为了一种 runloop的activity。

kCFRunLoopBeforeWaiting

这个 activity 表示当前线程即将可能进入睡眠,如果能够从内核队列上读出 msg 则继续运行任务,如果当前队列上没多余消息,则进入睡眠状态。读取 msg 的函数为:

__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), poll ? 0 : TIMEOUT_INFINITY);

其本质是调用了mach_msg 内核函数,注意 timeout 值,TIMEOUT_INFINITY 表示有可能无限进入睡眠状态。

kCFRunLoopAfterWaiting

这个 activity 是当前线程从睡眠状态中恢复过来,也就是说上面的 mach_msg 终于从队列里读出了 msg,可以继续执行任务了。这是每一次 runloop 从 idle 状态中恢复必调的一个 activity,如果你想设计一个工具检测 runloop 的执行周期,那么这个 activity 就可以作为周期的开始。

kCFRunLoopExit

exit 不必多言,退出当前mode的RunLoop的时候会调用到这个activity。

activity 的回调并不是单单给开发者用的,事实上,系统也会通过注册相关 activity 的回调来完成一些任务,比如我看到过如下的 callstack:

...
__CFRunLoopDoObservers
...
[UIView(Hierarchy) addSubview:] 
...

显然系统在观测到 runloop 进入某个 activity 之后,会进行一些 UIView 的布局工作。

再看这个:

...
__CFRunLoopDoObservers
...
[UIViewController __viewWillDisappear:] 
...

这是系统在使用 DoObservers 传递 viewWillDisappear 回调。

###6.5 RunLoop的任务处理

callout_to_observer和sleep我们以前已经讨论,performTask我们前面也有所谈及,这里我们继续分析这些Task。从源码中我们可以看到一类Do函数:

__CFRunLoopDoBlocks(内部调用callout_to_block)
__CFRunLoopDoSources0(内部循环调用callout_to_source0_perform_function)
__CFRunLoopDoSource1(内部调用callout_to_source1_perform_function)
__CFRunLoopDoTimers(内部调用__CFRunLoopDoTimer)

__CFRunLoopDoObservers(观察者,非任务,内部callout_to_observer)

下面这个不是Do系列的,但是因为和callout系列并列,是任务的一种,也列出来
_dispatch_main_queue_callback_4CF(被servicing_main_dispatch_queue所调用)

DoBlocks()
这种方式可以被开发者使用,使用方式很简单。可以先通过 CFRunLoopPerformBlock 将一个 block 插入目标队列,函数签名如下:

voidCFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void(^block)( void));

详细使用方式可参考文档:https://developer.apple.com/documentation/corefoundation/1542985-cfrunloopperformblock?language=objc

可以看出该 block 插入队列的时候,是绑定到某个 runloop mode 的。调用上面的 api 之后,runloop 在执行的时候,会通过如下 API 执行队列里所有的 block:

__CFRunLoopDoBlocks(rl, rlm);

很显然,执行的时候也是只执行和某个 mode 相关的所有 block。至于执行的时机点有多处,后面也会标注。

DoSources0()
我们已知source是用于产生异步事件的,Runloop里有两种source,source0 和 source1,虽然名称相似,二者运行机理并不相同。source0需要被应用手动管理,当source0准备触发的时候,需要调用CFRunLoopSourceSignal告诉RunLoop已做好触发准备。CFSocket目前是以 source0实现的。

详细文档可参考:https://developer.apple.com/documentation/corefoundation/1542679-cfrunloopsourcecreate?language=objc

绑定好之后,runloop 在执行的时候,会通过如下 API 执行所有的 source0:

__CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

同理,每次执行的时候,也只会运行和当前 mode 相关的 source0。

DoSource1()
source1是由RunLoop和内核管理的。source1 的实现原理是基于 mach_msg 函数。当一个消息到达source1的Mach端口时,内核会主动发出信号,通过读取某个port上内核消息队列上的消息来决定执行的任务。CFMachPort和CFMessagePort是以source1实现的。

详细文档可参考:https://developer.apple.com/documentation/corefoundation/1542679-cfrunloopsourcecreate?language=objc

绑定好之后,runloop 在执行的时候,会通过如下 API 执行某个source1:

__CFRunLoopDoSource1(rl, rlm, stopAfterHandle);

同理,每次执行的时候,也只会运行和当前 mode 相关的 source0。

DoTimers()

这个比较简单,开发者使用 NSTimer 相关 API 即可注册被执行的任务,runloop 通过如下 API 执行相关任务:

__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());

同理,每次执行的时候,也只会运行和当前 mode 相关的 timer。

DoMainQueue()

开发者调用 GCD 的 API 将任务放入到 main queue 中,runloop 则通过如下 API 执行被调度的任务:

_dispatch_main_queue_callback_4CF(msg);

注意,这里就没有 rlm 参数了,也就是说 DoMainQueue 和 runloop mode 是不相关的。msg是通过mach_msg函数从某个 port上读出的msg。

前面我们列举出了RunLoop中的五种执行任务方式,可见苹果会在不同的场景下使用它们。这里我们不进行总结,只是简单罗列下苹果的例子:

source0:
...
__CFRunLoopDoSources0     
...
[UIApplication sendEvent:] 
...
显然是系统用 source0 任务来接收硬件事件。
...
__CFRunLoopDoSources0
...
CA::Transaction::commit() 
...
这是系统在使用 doSource0 来提交 Core Animation 的绘制任务。

mainqueue:
...
_dispatch_main_queue_callback_4CF
...
[UIView(Hierarchy) _makeSubtreePerformSelector:withObject:withObject:copySublayers:]
...
系统在使用 doMainQueue 来执行 UIView 的布局任务。

timer:
...
__CFRunLoopDoTimer
...
[UICollectionView _updateWithItems:tentativelyForReordering:animator:]
...
这是系统在使用 doTimers 来 UI 绘制任务。

doBlocks:
...
__CFRunLoopDoBlocks
...
CA::Context::commit_transaction(CA::Transaction*)
...
这是系统在使用 doBlocks 来提交 Core Animation 的绘制任务。

7.苹果用 RunLoop 实现的功能

7.1 AutoreleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

7.2 事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

7.3手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

7.4 界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

7.5定时器

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。

7.6 PerformSelector

当调用 NSObject 的 performSelector:afterDelay: 后,实际上其内部会创建一个Timer并添加到当前线程的 RunLoop中。所以如果当前线程没有RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个source0加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

7.7关于GCD

实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop可以通过dispatch_source_t 实现 Timer(尽管事实上NSTimer是优先使用XNU 内核的 mk_timer实现的,但源码中也包含了使用GCD实现的方式)。同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

7.8关于网络请求

iOS 中,关于网络请求的接口自下至上有如下几层:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire

CFSocket 是最底层的接口,只负责 socket 通信。
• CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
• NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 1.0工作于这一层。
• NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking 2,3 和 Alamofire 工作于这一层。

下面主要介绍下 NSURLConnection 的工作过程。

通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

8.RunLoop 的实际应用举例

8.1 AFNetworking

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (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 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:…] 将这个任务扔到了后台线程的 RunLoop 中。

8.2 AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:

UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。

排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。

ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
具体的代码可以看这里:_ASAsyncTransactionGroup。

前面看了很多RunLoop的系统应用和一些知名第三方库使用,那么除了这些究竟在实际开发过程中我们自己能不能适当的使用RunLoop帮我们做一些事情呢?

8.3其他

思考这个问题其实只要看RunLoopRef的包含关系就知道了,RunLoop包含多个Mode,而它的Mode又是可以自定义的,这么推断下来其实无论是Source1、Timer还是Observer开发者都可以利用,但是通常情况下不会自定义Timer,更不会自定义一个完整的Mode,利用更多的其实是Observer和Mode的切换。
例如很多人都熟悉的使用performSelector在默认模式下设置图片,防止UITableView滚动卡顿([[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。还有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。再有老谭的PerformanceMonitor关于iOS实时卡顿监控,同样是利用Observer对RunLoop进行监视。

本文的内容来自以下博文的总结:

iOS刨根问底-深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/
iOS开发 - 啰嗦讲解 Runloop https://www.cnblogs.com/CrazyD0u/p/6481092.html
iOS底层原理总结 - RunLoop https://www.jianshu.com/p/de752066d0ad
iOS RunLoop详解 https://www.jianshu.com/p/23e3ff9619c3
https://www.jianshu.com/p/18e45cbd564f
关于runloop,好多人都理解错了! https://www.jianshu.com/p/ae0118f968bf
runloop源码解读笔记 https://www.jianshu.com/p/288e8abc80f1
解密 Runloop http://mrpeak.cn/blog/ios-runloop/

推荐阅读:

iOS runloop 学习笔记(一) - 官方文档 https://www.jianshu.com/p/8e40a7b16357
iOS runloop 学习笔记(二) - sunnyxx 大神视频 https://www.jianshu.com/p/929d855c5a5a
iOS runloop 学习笔记(三) - 彦祖叶孤城视频总结 https://www.jianshu.com/p/924cb2b218f5
iOS runloop 学习笔记(四) - 总结 https://www.jianshu.com/p/6a41cd354dc6
关于performSelector的一些小探讨https://juejin.im/post/5c637a1ff265da2dbc596ec2
runloop嵌套理解https://www.jianshu.com/p/318328c0a2d1
多线程-被遗弃的线程http://sindrilin.com/2018/04/14/abandoned_thread.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值