RunLoop是ios中一个非常重要的机制,ios系统底层很多模块都是通过RunLoop机制实现的,例如界面更新、事件响应等。本质上RunLoop是一种用于循环处理事件,而又不至于使CPU无意义空转的方式。
一、基本概念(了解过的可以跳过这一节)
1、NSRunLoop对象
(1)CFRunLoopRef
NSRunLoop对象是OC对象,是对CFRunLoopRef的封装,可以通过getCFRunLoop方法获取其对应的CFRunLoopRef对象。注意,NSRunLoop不是线程安全的,但CFRunLoopRef是线程安全的。
(2)RunLoopMode
NSRunLoop对象是一系列RunLoopMode的集合,每个mode包括有这个模式下所有的Source源、Timer源和观察者。每次RunLoop调用的时候都只能调用其中的一个mode,接收这个mode下的源,通知这个mode下的观察者。这样设计的主要目的就是为了隔离各个模式下的源和观察者,使其不相互影响。
其中系统默认注册的5个mode有:
- kCFRunLoopDefaultMode : App默认的mode,一般情况下App都是运行在这个mode下的。
- UITrackingRunLoopMode : 界面跟踪时的mode,一般用于ScrollView滚动的时候追踪的,保证滑动的时候不受其他事件影响。
- UIInitializationRunLoopMode : 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode : 接受系统事件的内部 Mode,一般用不到。
- kCFRunLoopCommonModes : 占位mode,可以向其中添加其他mode用以检测多个mode的事件
(3)CFRunLoopSourceRef
CFRunLoopSourceRef是事件源产生的地方,主要有两种:
Source0 :只包含一个函数指针(回调方法),不能自动触发,只能手动触发,触发方式是先通过CFRunLoopSourceSignal(source)将这个Source标记为待处理,然后再调用CFRunLoopWakeUp(runloop) 来唤醒RunLoop处理这个事件。
Source1 :基于port的Source源,包含一个port和一个函数指针(回调方法)。该Source源可通过内核和其他线程相互发送消息,而且可以主动唤醒RunLoop。
(4)CFRunLoopTimerRef
CFRunLoopTimerRef是基于事件的触发器,其中包含一段时间长度、延期容忍度和一个函数指针(回调方法)。当其加入到RunLoop中时,RunLoop会注册一个时间点,当到达这个时间点后,会触发对应的事件。
(5)performSEL
performSEL其实和NSTimer一样,是对CFRunLoopTimerRef的封装。因此,当调用performSelecter:afterDelay: 后,实际上内部会转化成CFRunLoopTimerRef并添加到当前线程的RunLoop中去,因此,如果当前线程中没有启动RunLoop的时候,该方法会失效。
(6)CFRunLoopObserverRef
CFRunLoopObserverRef是RunLoop的观察者。每个观察者都可以观察RunLoop在某个模式下事件的触发并处理。可以观察的时间点包括以下几点:
- kCFRunLoopEntry:即将进入RunLoop
- kCFRunLoopBeforeTimers:即将处理Timer
- kCFRunLoopBeforeSources:即将处理Source
- kCFRunLoopBeforeWaiting:即将进入休眠
- kCFRunLoopAfterWaiting:刚从休眠中被唤醒
- kCFRunLoopExit:即将退出RunLoop
(7)modeItem
上面的Source/Timer/Observer被统称为mode item,一个item可以被同时加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,则RunLoop会直接退出,不进入循环。
2、RunLoop的驱动
BOOL isRunning = NO;
do {
isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]];
} while (isRunning);
RunLoop本身是不能循环的,要通过外部的while循环循环驱动。
3、RunLoop内部的基本流程
每次运行RunLoop,内部都会处理之前没有处理的消息,并且在各个阶段通知相应的观察者。大致步骤如下:
(1)通知观察者RunLoop启动
(2)通知观察者即将处理Timer
(3)通知观察者即将处理Source0
(4)触发Source0回调
(5)如果有Source1(基于port)处于ready状态,直接处理该Source1然后跳转到()去处理消息
(6)如果没有待处理消息,则通知观察者RunLoop所在线程即将进入休眠。
(7)休眠前,RunLoop会添加一个dispatchPort,底层调用mach_ msg接收mach_ port的消息。线程进入休眠,直到下面某个事件触发唤醒线程
- 基于port的Source1事件到达
- Timer时间到达
- RunLoop启动时设置的最大超时时间到了
- 手动唤醒