1、什么是Run Loop?
(1)、Run Loop是线程的一项基础配备,它的主要作用是来让某一条线程在有任务的时候工作、没有任务的时候休眠。
(2)、线程和 Run Loop 之间的关系是一一对应的,但是并不是说新开一条线程就会自动生成这条线程对应的Run Loop,每一条线程里的Run Loop都是需要主动去获取,并且启动它,它才会开始运作的。主线程的Run Loop之所以不用我们手动去获取启动它,是因为在App启动的时候它已经默认启动了。
(3)、Run Loop的核心是__CFRunLoopRun()函数,它的主要结构是一个do-while循环,当一个Run Loop启动后,它在这个do-while循环里的基本运作如下:
如果有事件源(见后文详解),它就会一直在这个do-while循环中运作:当事件源有发出消息,它就处理消息并继续循环;当事件源没有发出消息时,它就停在休眠的代码处,等待事件源发出消息来唤醒它处理消息并继续循环;
如果没有事件源,它就会直接退出循环。
(4)、线程里的Run Loop是在第一次获取的时候才会去创建,而它的销毁发生在以下3种情况下:
线程结束;
因为没有任何事件源而退出循环;
手动结束了这个Run Loop。
2、Run Loop的结构?
(1)、Run Loop的结构需要涉及到以下4个概念:Run Loop Mode、Input Source、Timer Source和Run Loop Observer。
(2)、Run Loop Mode是Run Loop的模式。一个Run Loop可以有很多种mode,但是每次启动一个Run Loop的时候只能选择一种mode,如果要切换mode,需要先退出当前Run Loop再重新使用新的mode进入Run Loop。关于这个的演示详见后文5(8)。
最常见的Run Loop Mode有两个:NSDefaultRunLoopMode和UITrackingRunLoopMode,主线程的RunLoop 里预置了这两个 Mode。其中NSDefaultRunLoopMode在App平常状态下主线程Run Loop的mode,UITrackingRunLoopMode是在拖动UIScrollView的时候主线程Run Loop的mode。即是说,在App平常状态下,主线程的NSDefaultRunLoopMode的Run Loop在运作;在拖动UIScrollView的时候,切换成了主线程的UITrackingRunLoopMode的Run Loop在运作。
每个Run Loop Mode里面都可以有3种mode item,即是上文所说的Input Source、Timer Source和Run Loop Observer。当一个Run Loop在某个mode下有Input Source或Timer Source的时候,这个Run Loop就不会退出,它会一直循环并处理或等待Input Source或Timer Source发出来的消息,也即是1(3)所描述的运作。
Run Loop、Run Loop Mode和mode items的关系如下图:
(2)、Input Source是用来传送event给线程的,它是线程中的Run Loop最主要的事件源。在苹果官方文档中将Input Source分为3种类型:Port-Based Sources、Custom Input Sources和Cocoa Perform Selector Sources。而在实际中,根据函数调用栈,Input Source实际只被区分为两种:Source0和Source1。
Source1是基于Port的,即是苹果官方分类的Port-Based Sources,这一类事件源通过内核和其他线程进行通信,它处理的消息是系统事件。它接收或分发系统事件,比如屏幕触摸之类的硬件操作产生的事件,就会先由Source1接收再进行分发等处理;
Source0是非基于port的,比如performSelector:onThread: withObject: waitUntilDone:之类方法产生的事件,便归结到Source0里。
有一个需要注意的地方:按钮点击事件,它从函数调用栈来看是Source0事件。但是实际上这个事件是从触摸屏幕开始产生的,触摸屏幕产生了event,Source1接收了这个event, 再由Source1将事件分发给了Source0,所以最终在函数调用栈里可以看到Source0的调用。
(3)、Timer Source会在预设的时间点向Run Loop发送消息,并且可以重复,其实就是NSTimer产生的事件。所以同时可以发现,NSTimer所在的线程必须有正在运行的Run Loop才能生效。由于主线程有默认启动的Run Loop,所以主线程上的NSTimer有时可以不考虑Run Loop相关的操作;但是如果NSTimer运作在新开的线程上,那就必须要在当前线程上启动Run Loop,不然NSTimer不会生效。
(4)、Run Loop Observer并不是事件源,它是用在Run Loop本身运作的时候往外发送消息的,Run Loop通过Observer随时告知外界它现在所处的状态。Run Loop会在以下几个事件点触发Observer:
(5)、由2(2)我们可以知道,Run Loop每次只能运行在一个mode 下,要切换mode必须先退出再换一个mode 进入。假设我们在主线程有一个NSTimer对象,它默认被添加到NSDefaultRunLoopMode这个mode中,那么当页面中有UIScrollView被拖动的时候,主线程中NSDefaultRunLoopMode的Run Loop退出,换成UITrackingRunLoopMode的Run Loop进入,这时候这个NSTimer对象就不起作用了,因为它所添加到的Run Loop已经退出了。
处理这种情况,就需要用到commonModes,一个mode可以被标记为commonModes,Run Loop会把所有标记为commonModes的mode涉及到的所有Input Source、Timer Source和Run Loop Observer同步到这些mode里,于是这些mode就会共享Input Source、Timer Source和Run Loop Observer,当Run Loop切换了一种mode重新启动的时候,Source和Observer就仍然有效。
所以可以把这个NSTimer对象添加到NSRunLoopCommonModes中,由于NSDefaultRunLoopMode和UITrackingRunLoopMode默认已被标记为commonModes,所以这时不管主线程的Run Loop切换到哪种模式,这个NSTimer对象都可以正常起作用了。
(6)、这时我们再回头来看一看1(3)所描述的Run Loop运行循环,结合mode items,可以这么解释:
如果有Input Source或Timer Source,Run Loop就会一直在do-while循环中运作:当Input Source或Timer Source有发出消息,Run Loop就处理消息并继续循环;当Input Source或Timer Source没有发出消息时,Run Loop就停在休眠的代码处,等待Input Source或Timer Source发出消息来唤醒它处理消息并继续循环;
如果没有Input Source或Timer Source,Run Loop就会直接退出循环。
3、Run Loop的运作过程?
(1)、由前面已经知道,Run Loop的核心是__CFRunLoopRun()函数,这个函数的主要结构是一个do-while循环,并且知道了这个循环的大致运作方法,那么在这个循环里具体做了什么呢?在苹果的官方文档里,对于循环的运作过程是这么描述的:
Each time you run it, your thread’s run loop processes pending events and generates notifications for any attached observers. The order in which it does this is very specific and is as follows:
每次线程中的Run Loop开始运行,它会先处理正在等待处理的事件(从事件源发出的消息),同时通知相关的观察者。它的具体处理顺序如下:
①、Notify observers that the run loop has been entered.
通知观察者Run Loop已经进入。
②、Notify observers that any ready timers are about to fire.
通知观察者即将开始处理已就绪的timer的事件。
③、Notify observers that any input sources that are not port based are about to fire.
通知观察者即将开始处理Source0的事件。
④、Fire any non-port-based input sources that are ready to fire.
处理Source0的事件。
⑤、If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
如果Source1有事件在等待处理,那么就处理这些事件,然后跳到第⑨步。
⑥、Notify observers that the thread is about to sleep.
(来到这一步说明Source1没有等待处理的事件)通知观察者线程要开始休眠了。
⑦、Put the thread to sleep until one of the following events occurs:
线程开始休眠,直到出现以下的任意情况:
* An event arrives for a port-based input source.
Source1有事件发出来了。
* A timer fires.
有timer要启动了。
* The timeout value set for the run loop expires.
Run Loop已超时 (do-while循环是有时限的,不过时限非常非常大)。
* The run loop is explicitly woken up.
Run Loop被手动唤醒。
⑧、Notify observers that the thread just woke up.
通知观察者线程要被唤醒了。
⑨、Process the pending event.
处理正在等待处理的事件。
* If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
如果有timer启动了,处理timer的事件然后跳到第②步重新进入循环。
* If an input source fired, deliver the event.
如果有input source启动了,分发input source产生的事件。
* If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
如果Run Loop是被手动唤醒的,并且还没到Run Loop的超时事件,那么也跳到第②步重新进入循环。
⑩、Notify observers that the run loop has exited.
(来到这一步说明Run Loop的循环结束了)通知观察者Run Loop结束了。
这就是一个Run Loop的详细运作过程。
4、Run Loop有什么作用?
(1)、根据官方文档,Run Loop的作用如下:
The only time you need to run a run loop explicitly is when you create secondary threads for your application.
你只有在创建了子线程中这种场景下才需要去启动一个Run Loop。
For example, if you use a thread to perform some long-running and predetermined task, you can probably avoid starting the run loop. Run loops are intended for situations where you want more interactivity with the thread. For example, you need to start a run loop if you plan to do any of the following:
(但是并不是说所有子线程都需要启动Run Loop)比如说,如果你要使用一条线程去处理一些耗时长但是可以预先确定操作内容的任务,这时候你是不需要启动Run Loop的(处理完任务让线程自己挂掉就行了)。Run Loop是用在那些你希望和线程能有更多(无法预先确定的)交互的场景里(即是一些要让线程一直保持活着的场景里)。比如说,你需要在以下场景里去启用Run Loop:
Use ports or custom input sources to communicate with other threads.
需要通过ports或者custom input sources去和其他线程做交互。
Use timers on the thread.
在这条子线程上使用timer。
Use any of the performSelector… methods in a Cocoa application.
(要对这条子线程)使用performSelector开头的那些方法。
Keep the thread around to perform periodic tasks.
要让这条子线程去周期性地处理一些任务(那就必须使用Run Loop让它变成常驻线程)。
(2)、如何做一条常驻线程?
要制造一条常驻线程可以在线程里这么处理:
这样为Run Loop添加一个空的port,由于有port,Run Loop启动后就不会退出,会一直在do-while循环里等待port的事件,这条线程就一直活着了。
如果不添加这个空的port的话,Run Loop在启动后就会因为没有Source而直接退出了。
5、其他相关:
(1)、Run Loop在每次启动的时候会创建AutoreleasePool,然后每次即将进入休眠的时候会释放旧的AutoreleasePool并创建新的AutoreleasePool,最后在退出Run Loop的时候会最终释放AutoreleasePool。
(2)、NSTimer计时并不是绝对精确的,如果到了某个需要进行操作的时间点,而Run Loop正在处理一项长时间的任务,那么这个时间点的操作任务就有可能被推迟或者跳过(取决于NSTimer的tolerance 属性)。如果要使用精确(也有可能会有0.001s量级的误差)的计时器,可以使用GCD的计时器:
在这个过程中有Run Loop模式的切换,通过打印可以看到它的计时仍然是很精准的:
(3)、还有另一种制造常驻线程的方法,不过不推荐使用,它是通过不断地启动Run Loop来实现的,直到有Source添加到Run Loop中,Run Loop才进入循环。这种方式和4(2)所描述的方法的差别在于:4(2)所描述的方法主动地为Run Loop添加了Source,这种方式不主动添加Source,被动等待Source:
输出结果如下,直到点击屏幕才停止打印“Run Loop已启动”:
(4)、一个mode item重复加入同一个 mode 时是不会有多重效果的,相当于只加入一次的效果。
(5) 、CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
(6)、一条线程在一个时间内只能有一个Run Loop,一个Run Loop对应一个mode。当mode改变的时候,Run Loop需要先退出,换成新mode的Run Loop重新进入,这时候线程里仍然只有一个Run Loop。
(7)、在使用NSTimer的时候,下面两个方法是完全等价的,记得在子线程的情况下要手动run一下这个Run Loop:
(8)、2(2)提到过切换Run Loop的mode的时候,Run Loop会先退出,换个mode再重新进入,可通过以下代码来验证这个说法(页面上有个UITextView可以拖动):
当UITextView拖动时打印出来的内容如下:
说明Run Loop确实是退出(128)然后重新进入(1)了。
(9)、2(4)提到过 Run Loop Observer并不是事件源,我们试试对一个Run Loop只添加Run Loop Observer不添加Source,看看它能不能保持循环,以此来验证这种说法:
打印的内容如下:
可以发现Run Loop直接就退出了,说明Run Loop Observer确实不是事件源。
(10)、在使用NSTimer重复执行任务的过程中,如果没有其他Source,Run Loop也是不断重复“休眠——唤醒”的,通过下面的打印可以看出:
根据2(4)的状态,Run Loop会通知观察者它即将进入休眠(32),过两秒后再通知观察者它即将被唤醒(64),然后再执行任务。
参考文档:
苹果官方文档
http://blog.ibireme.com/2015/05/18/runloop/#comment-664
http://blog.csdn.net/ztp800201/article/details/9240913
网友评论