RunLoop一个老生常谈的东西,网上文章也是一搜一大把,看来看去好像说的东西都差不多,估计看的多的还是大佬的《深入理解RunLoop》这篇文章,看完之后总是觉得缺点什么好像都是在说原理,对于背景的描述的较少了点,感觉有点知识板块不完整,于是看了一下官方文档,总算是一些困惑也得以解决,才发现官方文档是个宝库。
基本概念
先回顾一下基本概念,想理解一个概念我觉得应该首先要了解这个概念诞生的背景,然后再定义概念这样似乎理解起来更加的深刻也更加容易,所以知其然还要之其所以然。
现在有这样一个问题,一个函数执行行完毕之后想再次执行该怎么办?办法当然是再次调用一次这个函数就行了,现在我们延伸一下把函数换成一个线程,问题就变成了一个线程执行完毕后,想再次使用这个线程该怎么办?办法可以是再次创建这个线程然后执行,那有没有一种手段不要创建线程新的线程,最容易想到的办法也就是让这个线程永远不要结束,也就是写一个循环在线程内部运行。但是如果只用循环这样会照成很多的资源浪费,理想状态下是有一种手段可以让线程不销毁的前提下,有事的时候出来做事,没事的时候进入休眠状态不消耗资源。这里就有几个关键的技术问题,首先是怎么做事做什么事,然后是怎么休眠、再次是怎么唤醒。为了解决上述的这问题苹果提供了RunLoop这种技术,背景我想大概是这样的。
RunLoop
有了上述的背景,我想给出我对runloop的理解,它是一个可以让线程保活,并且让线程有事件的时候处理事件,没事件的时候进入休眠状态,提供了接收事件、自身状态通知API的一个对象。通过它的结构我们看出它内部是一个循环,而且可以接收的事件有输入源、定时器源。如果一个RunLoop没有接收源那么它也就失去了存在的意义,它设计之初就是为了接收事件的,这也解释为什么RunLoop内部必须要有一个源才能运行。
RunLoop与线程
RunLoop是为线程而生与线程的关系是一一对应的,苹果官方没有提供我们创建RunLoop的方法,通过阅读源码可以知道,简单来说系统会维护一个字典,以key&value的形式保存RunLoop和线程的对应关系,当我们在线程内调用[NSRunLoop currentRunLoop]时函数内部会先去字典中查找是否有对应的RunLoop如果没有就创建一个并且把它存入字典中,如果有就取出来。值得注意的是主线程的RunLoop会再字典创建的时候同时创建。具体可以参考CFRunLoopRef源码。
RunLoop之源
通过官方文档我们知道运行循环从两种不同类型的源接收事件,也正是有了这些可以接收的源解决了上面提到的怎么做事和做什么事的问题。
输入源传递异步事件,通常是来自另一个线程或来自不同应用程序的消息。
计时器源传递同步事件,这些事件在计划的时间或重复的时间间隔发生。
输入源
输入源将事件异步传递到线程。事件的来源取决于输入来源的类型,通常是两个类别之一。基于端口的输入源监视您的应用程序的Mach端口。定制输入源监视事件的定制源。两种信号源之间的唯一区别是它们的信号发送方式。基于端口的源由内核自动发出信号,而自定义源必须从另一个线程手动发出信号,详细的使用参见官方文档,关于源我觉得才是RunLoop精髓的地方,而多数情况下我们更多是利用观察者进行一些辅助的功能的实现。
计时器源
计时器源在将来的预设时间将事件同步传递到您的线程。计时器是线程通知自己执行某项操作的一种方式。
RunLoop之状态
RunLoop除了提供了接收源的接口外,还提供了几个可以观察的状态以及回调,以作为一些辅助功能的实现,主要的状态如下:
- 运行循环的入口。
- 当运行循环将要处理计时器时。
- 当运行循环将要处理输入源时。
- 当运行循环即将进入睡眠状态时。
- 当运行循环醒来但在处理该事件之前将其唤醒。
- 运行循环的退出。
RunLoop之观察者
观察者是可以订阅RunLoop状态变化的对象,通过订阅状态变化可以做一些而外的辅助工作。
RunLoop之模式
RunLoop的模式是指是对输入源和定时器的集合进行监测和通知观察的一种集合,每次运行循环时,都可以(显式或隐式)指定运行的特定“模式”。在运行循环的整个过程中,仅监视与该模式关联的源,并允许其传递事件。类似地,仅将与该模式关联的观察者通知运行循环的状态。
RunLoop内部循环顺序
每次运行它时,线程的运行循环都会处理未决事件,并为所有附加的观察者生成通知。它执行此操作的顺序非常具体,如下所示:
- 通知观察者已进入运行循环。
- 通知观察者任何准备就绪的计时器即将触发。
- 通知观察者任何不基于端口的输入源都将被触发。
- 触发所有准备触发的非基于端口的输入源。
- 如果基于端口的输入源已准备好并等待启动,请立即处理事件。转到步骤9。
- 通知观察者线程即将进入睡眠状态。
- 使线程进入睡眠状态,直到发生以下事件之一:
- 事件到达基于端口的输入源。
- 计时器触发。
- 为运行循环设置的超时值到期。
- 运行循环被明确唤醒。
- 通知观察者线程刚刚醒来。
- 处理未决事件。
- 如果触发了用户定义的计时器,请处理计时器事件并重新启动循环。转到步骤2。
- 如果触发了输入源,则传递事件。
- 如果运行循环被明确唤醒,但尚未超时,请重新启动循环。转到步骤2。
- 通知观察者运行循环已退出。
由于计时器和输入源的观察者通知是在这些事件实际发生之前传递的,因此通知时间和实际事件时间之间可能会有差距。如果这些事件之间的时间间隔很关键,则可以使用睡眠和从睡眠中唤醒通知来帮助您关联实际事件之间的时间间隔。
困惑探索
对于上面描述的执行顺序,是否有不少困惑,光想是想不通的,跑一下代码就知道了。觉得主要的困惑是以下两个,主要原因是源的概念有点抽象,感觉摸不到那就动手写一下源。
- 上面的第二步和第三步一定会通知吗?假如只有定时器源或者Source0源其中一种的情况会怎么样?
我们知道RunLoop存在的意义就是接收事件处理事件,并且合理的利用资源,所以一个RunLoop要运行必须至少要有一个源。当向RunLoop只添加一个基于端口的源时运行结果如下,定时器源通知和source0的通知都发出了。
当只添加一个定时器源时运行结果如下,定时器源通知和source0的通知都发出了,而定时器的实际执行时机是在线程刚刚唤醒的时候,也是上图的第9步。
当只添加一个source0的时候运行结果如下,定时器源通知和source0的通知都发出了,通过触发source0可以看见执行的时机是在通知之后。
可以看出无论是否有定时器源或者非端口的源第2、3步都会进行通知,而实际处理定时源的时机是被唤醒的时候,而处理source0的时机如上图再发出通知后才开始执行。
- 第5步到第9步再到第2步怎么理解
先添加一个只添加一个source1时,触出这个源运行结果如下,唤醒线程后开始执行处理收到的事件。
当同时添加suroce1和source0两种源,并且先触source0时运行结果如下,source1的源没有在唤醒时执行,而是到了source0的源执行完成后再执行。这要也就模拟了第5步的场景,可以理解为触发source0后又触发了source1。
如果先触发source1再触发source0运行结果如下。
总结
总结一下对于RunLoop的理解关键在于,理解它是为了解决什么问题。它是为了保活线程,并且有事的时候做事没事的时候休眠,这就包括了两个方面。一.保活,其内部是通过一个循环来实现的。二.做事和休眠,做事指的是事件输入源,休眠是指不浪费系统资源。延伸一下输入源也包含了一种线程间通信的机制source1和source0。理解了事件输入源以及RunLoop解决的问题,在于理解其内部的实现原理,执行顺序就水到渠成了。值得注意的是无论RunLoop内部是否有定时器源或者source0源通知都会触发。