RunLoop是通过内部维护的事件循环
来对事件和消息进行管理的对象
- 没有消息需要处理时,Runloop将线程控制器交给系统,即从
用户态->内核态
,休眠以避免资源占用 - 有消息需要被处理时,立即唤醒,又从
内核态->用户态
。
Runloop核心
Runloop可以简单地理解成一个while(true)
的循环,但又不是while循环这么简单,因为这样的实现会使CPU进行大量无谓的空转。所以,Runloop机制的核心就是保证线程在有events需要处理时能唤醒,在没有events时能进行休眠。
而实现真正的休眠,是靠没有events时从用户态->内核态
实现的,当有事件时,系统内核通过mach_msg()
或者mach port
方法将事件发送给对应的Runloop,Runloop收到事件后从休眠状态切换到唤醒状态,并从内核态->用户态
。
数据结构
NSRunLoop是CFRunLoop的封装,提供了面向对象的API
- CFRunLoop
- CFRunLoopMode
- Source/Timer/Observer
CFRunLoop{
pthread -----> 线程和runloop是一一对应的
currentMode ---> CFRunLoopMode
modes ---> NSMutableSet<CFRunLoopMode*>
commonModes ---> NSMutableSet<NSString*>
commonModeItems ---> {多个Observer,Timer, Source}
}
CFRunLoopMode
CFRunLoopMode内部数据结构如下
CFRunLoopMode {
name (如NSDefaultRunLoopMode)
sources0 ---> NSMutableSet
sources1 ---> NSMutableSet
observers ---> NSMutableArray
timers ---> NSMutableArray
}
mode是管理着Runloop与source/timer/observer之间的桥梁,在一开始会注册五个mode
- kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
- UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。默认NSTimer是被加入到default mode中的,所以当滑动时Runloop切换到tracking mode,这时default mode中的Timer回调不会被调用,所以NSTimer的精度没有CADisplayLinker高。
- UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
-
kCFRunLoopCommonModes/NSRunLoopCommonModes:
1.这是一个占位的 Mode,不是实际存在的
一种mode。
2.是同步Source/Timer/Observer到多个Mode中的一种技术方案
如果需要将事件加入到多个mode中,则将它注册到commonMode中,该mode实际上是多个mode的集合。
出于将source/timer/observer分隔开的目的,RunLoop一次只能运行在一个mode下,当运行时在RunLoop的currentMode属性中会标记当前运行的mode。而当要切换mode时,RunLoop必须先退出,并选中一个mode重新进入,达到切换mode的目的。在切换mode时,被加入到commonModes中的事件会被拷贝一次到运行的mode中。
CFRunloopObserver
观测时间点
- kCFRunLoopEntry 将进入runloop
- kCFRunLoopBeforeTimers
- kCFRunLoopBeforeSouces
- kCFRunLoopBeforeWaiting 将要进入休眠状态(即将从用户态切换到内核态
- kCFRunLoopAfterWaiting 开始唤醒(从内核态进入到用户态不久
kCFRunLoopExit runloop退出的通知
各个数据结构之间的关系?
RunLoop和线程一一对应--->多个mode--->{多个Source,Timer,Observer}
如何唤醒Runloop
1. CFRunLoopSource
source0: 需要手动唤醒线程
该类Source是App的内部事件,不具有独立唤醒Runloop的能力。一个Source0需要被处理时,他需要被CFRunLoopSourceSignal()
函数标记为待处理,并调用CFRunLoopWakeUp
函数来唤醒Runloop,CFRunLoopWakeUp
函数内部通过一个_wakeUpPort
成员变量来唤醒Runloop,推测该变量是一个mach port,Runloop只有通过mach port
与mach_msg()
才可以唤醒。唤醒后通过调用__CFRunLoopDoSources0函数来处理Source0事件,并在之后将该事件标记为已处理。
source1: 具备唤醒线程的能力
一般是由硬件生成的source,如触摸、点击、摇晃、旋转等。此类Source可唤醒Runloop。
2.Timer
使用NSTimer API注册执行的任务,就属于这一类
3. Observer
某个Observer可以监听runloop的状态变化,并作出反应