概念
RunLoop是通过内部维护的
事件循环
来对事件/消息进行管理
的一个对象
。
- 事件循环:CPU-内核态和用户态
① 没有消息需要处理的时候会休眠以减少资源占用(由用户态转向内核态);
② 有消息需要处理的时候会被立刻唤醒(有内核态转向用户态)。- main函数为什么可以一直运行而不退出
UIApplicationMain方法在内部维护了一个主线程RunLoop,RunLoop又是一种对事件的维护机制,可以做到有事做就唤醒做事,没事做就休眠等待,也就是用户态到内核态来回的转换。
数据结构
NSRunLoop(Foundation)是CFRunLoop(CoreFoundation)的封装,提供了面向对象的API
CFRunLoop
:RunLoop对象
CFRunLoopMode
:运行模式
CFRunLoopSource
:输入源/事件源
CFRunLoopTimer
:定时源
CFRunLoopObserver
:观察者
- CFRunLoopTimer
基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。 因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。- 各数据结构之间的联系
线程和RunLoop一一对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的
各数据结构之间的联系- RunLoop的Mode
关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。
当RunLoop运行在Mode1上时,是无法接受处理Mode2或Mode3上的Source、Timer、Observer事件的
RunLoop的Mode
总共是有五种CFRunLoopMode:
kCFRunLoopDefaultMode
:默认模式,主线程是在这个运行模式下运行
UITrackingRunLoopMode
:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
UIInitializationRunLoopMode
:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode
:接受系统内部事件,通常用不到
kCFRunLoopCommonModes
:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案
事件循环机制
这张图在网上流传比较广
对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。
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与NSTimer
- RunLoop与NSTimer的关系
NSTimer在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。 因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。- 一个比较常见的问题:滑动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一一对应的
自己创建的线程默认是没有RunLoop的。
怎样实现一个常驻线程
① 为当前线程开启一个RunLoop
② 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环
③ 启动该RunLoop怎样实现子线程数据回来更新UI的时候不打断用户的滑动操作
用户滑动操作时,当前RunLoop时处在UITrackingRunLoopMode模式下的,将子线程数据刷新UI的相关部分可以分配到RunLoop的kCFRunLoopDefaultMode模式下,这样用户在滑动操作时,是不会处理kCFRunLoopDefaultMode模式下的任务的。