Runloop

Runloop从语法上分析

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

UIApplicationMain内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码(不是简单的for循环或while循环),UIApplicationMain函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。
NSRunLoop(Foundation)是CFRunLoop(CoreFoundation)的封装,提供了面向对象的API
RunLoop 相关的主要涉及五个类:

CFRunLoop:RunLoop对象
CFRunLoopMode:运行模式
CFRunLoopSource:输入源/事件源
CFRunLoopTimer:定时源
CFRunLoopObserver:观察者

1、CFRunLoop

由pthread(线程对象,说明RunLoop和线程是一一对应的)、currentMode(当前所处的运行模式)、modes(多个运行模式的集合)、commonModes(模式名称字符串集合)、commonModelItems(Observer,Timer,Source集合)构成

2、CFRunLoopMode

由name、source0、source1、observers、timers构成

3、CFRunLoopSource

分为source0和source1两种

source0:
即非基于port的,一般是APP内部的事件,只包含了一个回调(函数指针)。需要手动唤醒线程,将当前线程从内核态切换到用户态,它并不能主动触发事件。需要先调用 (source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
source1:
基于port的,包含一个 mach_port 和一个回调(函数指针),可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件。具备唤醒线程的能力。

4、CFRunLoopTimer

基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。 因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。

5、CFRunLoopObserver

监听以下时间点:CFRunLoopActivity

kCFRunLoopEntry
RunLoop准备启动
kCFRunLoopBeforeTimers
RunLoop将要处理一些Timer相关事件
kCFRunLoopBeforeSources
RunLoop将要处理一些Source事件
kCFRunLoopBeforeWaiting
RunLoop将要进行休眠状态,即将由用户态切换到内核态
kCFRunLoopAfterWaiting
RunLoop被唤醒,即从内核态切换到用户态后
kCFRunLoopExit
RunLoop退出
kCFRunLoopAllActivities
监听所有状态
6、各数据结构之间的联系
线程和RunLoop一一对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的

v2-da6cedc8c4b7694d3ac9375e832f57ac_1440w.jpg

三、RunLoop的Mode

关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。

v2-d82d496424b7b3831308f287ed4daff9_1440w.jpg

当RunLoop运行在Mode1上时,是无法接受处理Mode2或Mode3上的Source、Timer、Observer事件的,

总共是有五种CFRunLoopMode:

kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行
UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案

四、RunLoop的实现机制

对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。

v2-92661053325d53c83e6328b0991ca336_1440w.jpg

RunLoop通过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。在用户态调用 mach_msg_trap()时会切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作。
即基于port的source1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待处理和手动唤醒RunLoop

Mach消息发送机制
大致逻辑为:
1、通知观察者 RunLoop 即将启动。
2、通知观察者即将要处理Timer事件。
3、通知观察者即将要处理source0事件。
4、处理source0事件。
5、如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。
6、通知观察者线程即将进入休眠状态。
7、将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。

  • 一个基于 port 的Source1 的事件(图里应该是source0)。
  • 一个 Timer 到时间了。
  • RunLoop 自身的超时时间到了。
  • 被其他调用者手动唤醒。

8、通知观察者线程将被唤醒。
9、处理唤醒时收到的事件。

  • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
  • 如果输入源启动,传递相应的消息。
  • 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2

10、通知观察者RunLoop结束。

Runloop从执行上分析

所谓 Runloop,简而言之,是 Apple 所设计的,一种在当前线程,持续调度各种任务的运行机制。说起来有些绕口,我们翻译成代码就非常直白了。

while (alive) {

performTask() //执行任务

callout_to_observer() //通知外部

sleep() //休眠
}

每一次 loop 执行,主要做三件事:
performTask()
callout_to_observer()
sleep()

performTask

每一次 runloop 的运行都会执行若干个 task,执行 task 的方式有多种,有些方式可以被开发者使用,有些则只能被系统使用。逐一看下:

DoBlocks()

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

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

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

可以看出该 block 插入队列的时候,是绑定到某个 runloop mode 的,runloop mode 的概念后面会详细解释,也是理解 runloop 运行机制的关键。

调用上面的 api 之后,runloop 在执行的时候,会通过如下 API 执行队列里所有的 block:

__CFRunLoopDoBlocks(rl, rlm);

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

DoSources0()
CFRunloopSource
struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
    pthread_mutex_t _lock;
    CFIndex _order;         /* immutable */
    CFMutableBagRef _runLoops;
    union {
        CFRunLoopSourceContext version0;     /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

Runloop 里有两种 source,CFRunLoopSource 是对 input sources 的抽象,它要么是 source0,那么是 source1。source0 和 source1,虽然名称相似,二者运行机理并不相同。source0 有公开的 API 可供开发者调用,source1 却只能供系统使用,而且 source1 的实现原理是基于 mach_msg 函数,通过读取某个 port 上内核消息队列上的消息来决定执行的任务。

typedef struct {
    CFIndex version;
    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, CFStringRef mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

作为开发者要使用 source0 也很简单,先创建一个 CFRunLoopSourceContext,context 里需要传入被执行任务的函数指针作为参数,再将该 context 作为构造参数传入 CFRunLoopSourceCreate 创建一个 source,之后通过 CFRunLoopAddSource 将该 source 绑定的某个 runloop mode 即可。

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

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

__CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

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

DoSources1()
typedef struct {
    CFIndex version;
    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 并不对开发者开放,系统会使用它来执行一些内部任务,比如渲染 UI。

公司内部有个厉害的工具,可以将某个线程一段时间内所执行的函数全部 dump 下来,上传到后台并以流程图的形式展示,很直观。得益于这个工具,我可以清楚的看到 DoBlocks,DoSources0, DoSources1 被使用时的 call stack,也就能知道系统是处于什么目的在使用上述三种任务调用机制,后面解释。

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 里一共有 5 种方式来执行任务,那么问题来了,苹果为什么要搞这么多花样,他们各自的使用场景是什么?

timer 和 mainqueue 无需多说,开发者大多熟悉其背后设计宗旨。至于 DoBlocks,DoSources0,和 DoSources1,我原先以为系统在使用时,他们各有分工,比如某些用来接收硬件事件,有些则负责渲染 Core Animation 任务,但实际观摩过一些主线程运行样本之后,我发现并无类似的 pattern。

比如我在 doSource0 里看到了这个 callstack:

..__CFRunLoopDoSources0     ...[UIApplication sendEvent:] ...

显然是系统用 source0 任务来接收硬件事件。

又比如这个使用 mainqueue 的 callstack:

..._dispatch_main_queue_callback_4CF...[UIView(Hierarchy) _makeSubtreePerformSelector:withObject:withObject:copySublayers:]...

系统在使用 doMainQueue 来执行 UIView 的布局任务。

再比如这个 callstack:

...__CFRunLoopDoBlocks...CA::Context::commit_transaction(CA::Transaction*)...

这是系统在使用 doBlocks 来提交 Core Animation 的绘制任务。
继续看这个:

...__CFRunLoopDoSources0...CA::Transaction::commit() ...

这是系统在使用 doSource0 来提交 Core Animation 的绘制任务。
不知道大家看出什么 pattern 没,我没,唯一比较有规律的是硬件事件都是通过 doSource0 来传递的,总体感觉系统在使用的时候有点 free style。

callout_to_observer

这一分类主要是 runloop 用来通知外部 observer 用的,用来告知外部某个任务已被执行,或者是 runloop 当前处于什么状态。我们也来逐一看下:

DoObservers-Timer

故名思义,在 DoTimers 执行完毕之后,调用 DoObservers-Timer 来告知感兴趣的 observer,怎么注册 observer 呢?在介绍完各种 callback 机制之后,再统一说下。runloop 是通过如下函数来通知 observer:
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);复制代码
DoObservers-Source0
同理,是在执行完 source0 之后,调用 DoObservers-Source0 来告知感兴趣的 observer,怎么注册后面统一介绍。runloop 通过如下函数来通知 observer:

__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

这是上述五种执行任务方式中,两种可以注册 observer 的,其他几个都不支持,mainQueue,source1,block 都不行。所以理论上,是没有办法准确测量各个任务执行的时长的。

DoObservers-Activity

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

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};

啰嗦下,再一个个讲解:

kCFRunLoopEntry

每次 runloop 重新进入时的 activity,runloop 每一次进入一个 mode,就通知一次外部 kCFRunLoopEntry,之后会一直以该 mode 运行,知道当前 mode 被终止,进而切换到其他 mode,并再次通知 kCFRunLoopEntry。runloop mode 的切换也是个很有意思的话题,后面会提到。

kCFRunLoopBeforeTimers

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

kCFRunLoopBeforeSources

同理,Apple 也将该 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 的时候可能会调用到这个 activity,为什么说可能呢?这和 mode 切换的方式有关,后面会提及。

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

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

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

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

这是系统在使用 DoObservers 传递 viewWillDisappear 回调。
以上即为 observer 的全部内容,一般开发者对 runloop 的 activity 感兴趣,多半是想分析主线程的业务代码执行情况,事实上,这些 activity 的回调不怎么可靠,也就是说有可能 runloop 哼哧运行来半天的代码,你一个 activity 的回调也收不到,或者收到了,但顺序也是完全出乎你的意料,后面会详细解释。

sleep

一言以蔽之,有任务就执行,没任务就 sleep。这部分逻辑就这么简单。

只是有个小细节需要注意,一般人印象里感觉 runloop 的每次 loop 总是按顺序执行上面的各种 performTask 和 callout_to_observer,执行完就 sleep,而实际上,这些任务的执行相互糅合在一起,还有 goto 的跳转逻辑,显得非常凌乱,而且 activity 的 callback 也可能不是按照 kCFRunLoopEntry->kCFRunLoopBeforeWaiting->kCFRunLoopAfterWaiting->kCFRunLoopExit 来的,后面我会画个流程图来解释下。

Runloop 的 loop 主函数为 __CFRunLoopRun,里面的这行调用会决定是否 sleep:

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

其内部无非是使用了我们开篇所提到的 mach_msg 函数。

完整流程

至此,我们已将 runloop 中的关键代码分为了三类,并就这三类进行了展开,接下来我们看下完整的流程。

Apple 工程师提到 runloop 的实现可能会随着 iOS 版本而变化,我在对比 Objective C 和 Swift 版本代码之后,发现关键流程没多少区别,下面这张图是我阅读代码时顺手绘制的,希望能让读者对 runloop 的运行机制有更直观形象的认识:

1545361200714037.png

我将 performTask 和 callout_to_observer 用不同的颜色加以了区分,从图中可以直观的看到 5 种 performTask 和 6 种 callout_to_observer 是在一次 loop 里如何分布的。

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

Poll?

每次 loop 如果处理了 source0 任务,那么 poll 值会为 true,直接的影响是不会 DoObservers-BeforeWaiting 和 DoObservers-AfterWaiting,也就是说 runloop 会直接进入睡眠,而且不会告知 BeforeWaiting 和 AfterWaiting 这两个 activity。所以你看,有些情况下,可能 runloop 经过了几个 loop,但你注册的 observer 却不会收到 callback。

两次 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 里去执行。

Runloop Mode

接下来是关键里的重点,重点里的核心,关于 runloop mode 的理解。

开始之前,再回顾下 runloop 在一次 loop 里可能会做的事情,代码如下:

while (alive) {
  //执行任务
  DoBlocks();
  DoSources0();
  DoSources1();
  DoTimers();
  DoMainQueue();
  
  //通知外部
  DoObservers-Timer();
  DoObservers-Source0();
  DoObservers-Activity();
    
  //休眠
  sleep() }

Runloop mode 的设计就是为了执行上述的逻辑服务,我反复提到过,大部分的任务和回调是和 mode 绑定的,那么我们来看下 mode 的数据结构是如何体现这部分功能的:

struct __CFRunLoopMode {
    ...
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    CFIndex _observerMask;
...};

为了阅读方便,我略去了一些不太相关的细节。很容易看出,执行任务和通知外部所需要的信息全都定义在了 mode 的数据结构里,基本上都是一个 array 来持有相关引用,比如当前 loop 需要 DoTimers() 的时候,只需要将 _timers 遍历并 invoke 即可:

static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, int64_t limitTSR) {/* DOES CALLOUT */
    for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; idx < cnt; idx++) {
...
    }
    return timerHandled;}

_observerMask 包含所有 observer 感兴趣的 activity,每次 observer 通过如下 API 创建一个新的 activity callback 并注册的时候, mask 也会随之更新:

CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block) (CFRunLoopObserverRef observer, CFRunLoopActivity activity))

而 mainQueue 任务的执行和 mode 无关,所以 mode 的结构定义里并无 mainQueue 相关的信息。

其他都比较直白,无须多言。

Runloop Mode 的种类

关于 Runloop Mode 的种类以及其背后设计思想,没有太多的文档可以参考,但这部分信息却至关重要。

简单来说 mode 分为两类,common mode 和 private mode。

比如我们所熟知的 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 是属于 common mode,kCFRunLoopDefaultMode 是默认模式下 runloop 使用的 mode,scrollView 滑动的时候切换到 UITrackingRunLoopMode。

除此之外,系统还定义了其他一些 private mode,比如 UIInitializationRunLoopMode 和 GSEventReceiveRunLoopMode。如果你在 app 启动的时候通过 CFRunLoopCopyAllModes 打印出所有的 runloop mode,就可以看到这两个 mode 了。我们简单探讨下 GSEventReceiveRunLoopMode 的使用场景。

GSEventReceiveRunLoopMode 以 GS 开头,是属于 GraphicsServices 这个并不公开的 framework,GSEvent 封装了系统传递给 app 的重要事件,比如音量按钮事件,屏幕点击事件等,而我们所熟知的 UIEvent 也不过是 GSEvent 的封装。我曾一度怀疑 apple 会使用 GSEventReceiveRunLoopMode 来传递各类系统事件,可惜的是,我在线上代码里设置里一段捕捉逻辑,上报所有未知的 runloop mode,却并没有捕获到 GSEventReceiveRunLoopMode 的使用场景。之后出于好奇,使用了一次召唤神龙的机会,给 Apple 工程师提了个 TSL,接我单的小哥只是隐晦的承认了 GSEventReceiveRunLoopMode 的存在,并表示这事不能说太细,Apple 的确会在一些场景下基于需要使用一些 private mode,事实上,开发者自己也可以创建 private mode 来实现一些功能,比如这个 post 里的例子:https://forums.developer.apple.com/message/187122#187122。除此之外,我并没有得到其他什么有用的信息,有点想退货。

这篇文档列举了一些公开的 mode:http://iphonedevwiki.net/index.php/CFRunLoop。

我设置的捕捉代码也捕获到了另一些有意思的 mode,比如这个 _kCFStreamBlockingOpenMode,google 一下,这是 CFStream 里用来调度网络任务所使用的 private mode,源码 https://opensource.apple.com/source/CF/CF-476.19/CFStream.c.auto.html。

问题来了

runloop mode 分为 common 和 private 对我们日常生活有哪些影响呢?影响很大。

当我们对 runloop 的 activity 感兴趣,并通过如下 API 注册 observer 的时候

CF_EXPORT void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);

大多数时候我们都会传入 kCFRunLoopCommonModes 作为参数,这也就意味着你的 observer 只会在 common mode 被运行的时候 call back,如果当前 loop 是以 private mode 运行的,那么你的 observer 将对 runloop 当前的 activity 浑然不觉。如果你的代码强依赖于 runloop activity 的监测,这显然会成为一个关键缺陷。private mode 使用的场景之多可能超过你的想象。
简而言之,每次 loop 只会以一种 mode 运行,以该 mode 运行的时候,就只执行和该 mode 相关的任务,只通知该 mode 注册过的 observer。

runloop mode 是如何切换的呢?

这个问题涉及到 runloop 的 mode 到底是如何使用的,显然我们无法得知系统是如何使用的,就如同那些 Apple 讳莫如深的 private mode。好在我们还是可以从代码得出分析。

每次如果要切换 mode,为了保证多线程安全,必会先通过如下代码 lock:

__CFRunLoopLock(rl);__CFRunLoopModeLock(rlm);

切换完之后再 unlock。
而整个runloop 关键流程函数里,主要有三处 unlock 的调用。

一处是在 sleep 之前,runloop 可能一觉醒来,发现 mode 已经物是人非。

另一处是在 doMainQueue 之前,执行完 GCD main queue 中的任务后,mode 也能会发生变化。

最后一处是在 CFRunLoopRunSpecific 函数,也就是 runloop exit 之后。

所以我们可以得出结论,runloop 有两种切换 mode 的方式,一是在 loop 的中途切换,二是按顺序在当前 mode 结束之后切换。

如果你也对 mode 的使用比较感兴趣,真相都在下面这三个可供开发者使用的函数里:

CF_EXPORT CFRunLoopMode CFRunLoopCopyCurrentMode(CFRunLoopRef rl);CF_EXPORT CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl);CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode)

RunLoop相关应用场景

RunLoop与NSTimer

一个比较常见的问题:滑动tableView时,定时器还会生效吗?
默认情况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就无法接受处理Timer的事件。
怎么去解决这个问题呢?把Timer添加到UITrackingRunLoopMode上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。
所以我们需要把Timer同时添加到UITrackingRunLoopMode和kCFRunLoopDefaultMode上。
那么如何把timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes了

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Timer就被添加到多个mode上,这样即使RunLoop由kCFRunLoopDefaultMode切换到UITrackingRunLoopMode下,也不会影响接收Timer事件

RunLoop和线程

线程和RunLoop是一一对应的,其映射关系是保存在一个全局的 Dictionary 里
自己创建的线程默认是没有开启RunLoop的
怎么创建一个常驻线程?

1、为当前线程开启一个RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一个RunLoop)
1、向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(如果RunLoop的mode中一个item都没有,RunLoop会退出)
2、启动该RunLoop

@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
输出下边代码的执行顺序

NSLog(@"1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[self performSelector:@selector(test) withObject:nil afterDelay:10];
NSLog(@"3");
});
NSLog(@"4");

  • (void)test
    {
    NSLog(@"5");
    }
    答案是1423,test方法并不会执行。
    原因是如果是带afterDelay的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的RunLoop中。也就是如果当前线程没有开启RunLoop,该方法会失效。
    那么我们改成:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[[NSRunLoop currentRunLoop] run];
[self performSelector:@selector(test) withObject:nil afterDelay:10];
NSLog(@"3");
});
然而test方法依然不执行。
原因是如果RunLoop的mode中一个item都没有,RunLoop会退出。即在调用RunLoop的run方法后,由于其mode中没有添加任何item去维持RunLoop的时间循环,RunLoop随即还是会退出。
所以我们自己启动RunLoop,一定要在添加item后

dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[self performSelector:@selector(test) withObject:nil afterDelay:10];
[[NSRunLoop currentRunLoop] run];
NSLog(@"3");
});

怎么保证子线程数据回来更新UI的时候,不打断用户的滑动操作?

滑动是在UITrackingRunloopMode下,滑动结束了,runloop由UITrackingRunloopMode又回到defaultMode下了。
数据加载一般在子线程下载,下载完毕后在主线程进行UI刷新。
可以将子线程数据,给主线程刷新UI的时候,包装后提交到主线程的defaultModel下,这样两个model不会同时执行,也就不会打断用户的滑动操作。

[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];

参考:
https://blog.ibireme.com/2015/05/18/runloop/
https://zhuanlan.zhihu.com/p/64593559
https://blog.csdn.net/u014795020/article/details/72084735
https://www.jianshu.com/p/fcb271f69038
https://www.cnblogs.com/kenshincui/p/6823841.html
https://blog.csdn.net/gsl111000/article/details/99311163
https://blog.csdn.net/csdn_coder_zxq/article/details/90740239
http://www.cocoachina.com/cms/wap.php?action=article&id=25906

你可能感兴趣的:(Runloop)