RunLoop是干啥的
官翻: Run loops是与线程相关的基础设施的一部分,一个Runloop就是一个事件处理的循环。Runloop的目标是让线程在有事情做的时候保持忙碌、没有事情做的时候保持睡眠。
疑问???
- Runloop的运行机制
- Runloop是如何唤醒和睡眠线程的,与线程到底是什么关系
- Runloop的Source、Timer、Observer和Mode与Runloop到底是什么关系
- iOS系统怎么使用Runloop的
- 如何自定义
庖丁解🐂
一个Runloop接收2中不同类型的sources.Input source 发送异步事件,一般是来自其他线程或者是其他应用的;Timer source发送同步事件,会在预设好的时间或者重复间隔内触发。这2种source都会通过特殊的处理程序处理事件。
Input source发送异步事件,这种source runloop在处理的时候会调用runUnitlDate:方法开启一个runloop,并且在运行date时间后退出。Timer source则不会使runloop退出。
在处理input source时,runloops 也会生成相关的通知,你可以注册成为观察者在当前线程来处理其他时间。
#####(补充) runloop 的运行方式
|
|
run方法对应CFRunloopRef中的CFRunLoopRun并不会退出,除非手动调用CFRunLoopStop();通常如果想要永远不会退出RunLoop才会使用此方法,否则可以使用runUntilDate。
runMode:beforeDate:则对应CFRunLoopRunInMode(mode,limiteDate,true)方法,只执行一次,执行完就退出;通常用于手动控制RunLoop(例如在while循环中)。
runUntilDate:方法其实是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false),执行完并不会退出,继续下一次RunLoop直到timeout。
Modes
一个runloop mode是input source和timer sources的集合,同时包含一个runloop观察者的合集。每次运行一个runloop时,需要指定一个特别的mode。在runloop 运行的过程中,只有和这个mode相关联source才能发送事件(同样,只有和这个mode关联的观察者才能收到通知)。关联了其他mode的source会被暂停一切新事件的发送直到runloop运行在该mode上。
|
|
modes通过name来区分,可用通过指定mode的name来自定义一个mode,虽然name在自定义的时候可以任意指定,但是其他内容却不行,当你自定义一个mode的时候,必须添加一个或者多个source(input source 或 timer source) ,或者观察者。
Input Sources
Input sources 给线程发送异步事件,事件的类型和input source的类型相关。一般有2种:Port-based input sources 用来监控Mach ports和自定义 input sources 用来监控自定义事件。2者的唯一不同在于如何发送信号:Port-based input sources是kernel内核自动发送信号;自定义 input sources是从其他线程手动发送信号。
Port-Based Sources
- NSMachPort
- NSMessagePort
mach 操作系统微内核 在mac os和ios系统中采用。虚拟内存的分配,进程间的通信(基于port)。
每一种服务都是一个进程(http 8080 https 443 ftp 20 21 rtmp 1935),每一个进程都分配一个port(虚拟端口),
Custom Input Sources
自定义一个Input source需要满足如下定义:
- 需要input source处理的信息
- 让感兴趣的客户端知道怎么联系你的input source
- 一个处理程序例程来执行任何客户端的发送请求
- 取消程序
Cocoa Perform Selector Sources
Perform Selector Sources 在执行完以后会从runloop的mode中移除。
当调用这个方法时,target线程必须包含一个活跃的runloop,runloop会在一次loop中处理所有的排队Perform事件而不是只处理一个。
Timer Sources
timer需要关联一个mode,如果timer不在当前活动的mode上,则不会被触发。如果timer所在的mode执行过程中被切换,则该timer也不会暂停直到mode重新切换回来,这就决定了NSTimer的触发并不是十分可靠的。
Run Loop Observers
|
|
Runloop使用场景
- 使用mach port或者自定义input source 来与其他线程通信
- 使用NSTimer
- 使用performSelector
- 线程保活
- 系统默认使用
源码解析
源码下载https://opensource.apple.com/tarballs/CF/
Input Source && CFRunLoopSource 的结构
- source0/自定义 source
|
|
- source1/(mach port source+自定义 source)
|
|
- CFRunLoopSource
|
|
observer && CFRunLoopObserver 的结构
|
|
NSTimer && CFRunloopTimer 的结构
|
|
Mode 的结构
|
|
runloop 的结构
|
|
Runloop的创建和销毁 CFRunLoopCreate() && CFRunLoopGet0() && CFRunLoopGetMain() && CFRunLoopGetCurrent() && CFFinalizeRunLoop()
|
|
总结
- runloop的创建是懒加载的方式创建,在第一次获取该runloop的时候才会去创建,所以子线程如果没有手动的去获取并运行runloop,是不会自动创建的。
- runloop和线程是一一对应的关系,保存在一个全局的字典中。key是线程的指针,value是对应的runloop.
- runloop的销毁发生在线程退出时。
Runloop的运行 CFRunLoopRun && CFRunLoopRunSpecific && CFRunLoopRun
|
|
runloop运行流程总结入下图
runloop内部其实就是一个do while()的循环。只是在没有事件需要处理的时候,runloop会调用__CFRunLoopSetSleeping 方法将当前线程置为睡眠状态,同时会调用 __CFRunLoopServiceMachPort 方法来睡眠线程并等待接收mach发来的唤醒消息。收到消息后,该线程会被唤醒,唤醒后runloop除了处理唤醒它的事件,还需要处理一遍所有等待处理的事件,包括(timer,observer,source,block)。
runloop唤醒事件总结
- mach port source
- 手动唤醒Custom Input Source(CFRunLoopSourceSignal(source)/CFRunLoopWakeUp(runloop))
- dispatch_async(dispatch_get_main_queue)/dispatch_sync(dispatch_get_main_queue)(不要在主线程同步任务,会死锁);
- timer
Mode和它的四大大王(Source,Timer,Observer,Block)
Mode
Mode的创建也是懒加载
|
|
Source
|
|
source总结
通过对比source0和source1的处理函数发现,source0有2种情况,单个事件和事件列表,而source1只有单个事件的处理,个人理解是因为source1的事件是即时处理的,因为source1可以唤醒runloop,只要有source1事件runloop就会去处理,所有不存在处理事件列表的情况,而source0有可能只是标记为待处理而没有手动唤醒runloop,当标记为待处理source0事件后,只会被添加到mode的source0集合中,在runloop被唤醒后统一处理。
Timer
|
|
timer总结
- 对于重复的NSTimer,其多次触发的时刻不是一开始算好的,而是timer触发后计算的。但是计算时参考的是上次应当触发的时间_fireTSR,因此计算出的下次触发的时刻不会有误差。
- 设置了tolerance的NSTimer,对于iOS和MacOS系统,实质上会采用GCD timer的形式注册到内核中,GCD timer触发后,再由RunLoop处理其回调逻辑。对于没有设置tolerance的timer,则是用mk_timer的形式注册。
- RunLoopMode中timer的排序是按照_fireTSR,也就是应当触发的时间排序的。
Observer
|
|
Block
|
|
block总结
- 可以直接给runloop添加block.添加成功后,block会在下一次runloop运行时被触发。
- block不能唤醒runloop,只会被添加到链表中,等待下一次runloop被唤醒后才会被执行。
Runloop实践
查看Main Runloop的结构
|
|
具体结构如下:
|
|
给系统mode添加自定义items(timer && source && observer)
为了方便观察添加是否成功,我们不在主线程的runloop中添加,自定义一个新的线程。
添加Custom Input Source注意事项
- custom input source 默认状态为不处理,需要手动唤醒,手动唤醒需要先标记为待处理状态,每次runloop处理完后状态会被置回不处理。
- 在子线程添加source需要在runloop run的代码之前添加,因为run后该线程会马上休眠(当前runloop中没有能唤醒自己的source),不再这行run后面的代码。
添加Mach Port Input Source注意事项
- 需要同时记录主线程端口和子线程端口号,需要唤醒对应线程时直接使用该端口发送消息即可,不需要像Custom Input Source那样做标记。
给runloop添加自定义mode和items
- 需要注意的是不能直接调用run这个方法,因为这个方法是运行在DefaultMode下的,不会触发自定义mode中的source,需要调用runMode:beforeDate方法开启runloop.
Runloop在iOS中的应用
这块直接看YY博客吧,已经没法再补充更多了。。。
博客链接https://blog.ibireme.com/2015/05/18/runloop/
最后的总结
Runloop是一个事件处理的循环。Runloop的目标是让线程在有事情做的时候保持忙碌、没有事情做的时候保持睡眠。
Runloop使用Mach内核实现线程的睡眠,通过Source和Timer来唤醒线程,Source和Timer最终都是通过Mach Port来唤醒的线程。
Source有CustomSource和Mach Port Source2种,CustomSource唤醒线程前需要先被标注为待处理,MachPortSource则直接使用端口号发送唤醒消息。
Timer有两种实现方式分别是MK_Timer和GCD Timer,在runloop中Timer被转为了一个存了触发时间的列表,这个触发时间是一个绝对时间,会按时间大小升序排序,在最小的时间被触发后,Runloop会更新列表保证时间始终是升序排列。如果Runloop在某次运行中阻塞了很长时间,Timer的触发会受到影响。过期的时间点会被移除而不会去触发。
Runloop的状态都可以通过添加Observer来得到。Observer在Runloop中是按order优先级升序排序的,排在前面的通知会先被触发(order越小优先级反而越高)。
Mode表示当前Runloop运行在哪个模式上,Runloop必须运行在一种Mode上,Runloop在创建时会有一个DefaultMode,可以通过runmode:beforeDate 切换Runloop当前运行的Mode。
Mode中包含了上面的Source,Timer和Observer三种Items。Runloop运行的时候其实就是在处理这些Items中的内容,如果这三种Items都没有要处理的内容,Runloop就要开始睡了,而当Mode的items为空时,当前Runloop会退出。
CommonMode不是一种真正的Mode,它是一个所有Mode的集合,如果把上面三种Items添加到CommonMode,那这些Items会被添加到集合中所有的Mode中。所以添加到CommonMode中Items,不管当前Runloop运行在哪种Mode上,Items中需要处理的事件都会被触发。(前提是这个Mode已经被添加到了CommonMode集合中)。