iOS中RunLoop

初识iOS RunLoop

iOS中应用程序app从开始运行一直处于等待的状态,接收到类似点击事件交互后执行相应的操作,完成后继续等待状态,直到我们将程序杀死。那么这个过程是怎么发生的呢?是如何通过相应的方式实现这种机制的呢?通过下面的代码探析:

1
2
3
4
5
6
7
- (void)loop {
BOOL message;
do {
message = [self getMessage];
[self processMessage];
} while (!message)
}

如上面的代码就可以达到这种机制。可以推测而出app从启动开始创建的主线程一定是在一个死循环中,没有任务的时候进行休眠,接收到任务后被激活执行任务。我们可以理解,这样一个管理线程执行任务就是RunLoop机制,线程在执行中的休眠与激活就是RunLoop对象进行管理的。

RunLoop与线程

RunLoop既然是用来管理线程的,那么他们直接有着怎样的关系。实际上,每一个线程中都有一个RunLoop对象,可以通过具体的方法获得。需要注意的是,除了主线程中RunLoop是默认创建并运行激活的,但并不是每一个线程中都有这个实例对象,如果我们不主动获取runloop,这个runloop就不存在,如果我们去获取不存在就去创建并返回。可以通过[NSRunLoop currentRunLoop]方法获取当前线程的runloop。

Cocoa中RunLoop

NSRunLoop是Cocoa框架中runloop的类非线程安全,关于NSRunLoop的方法如下:
1
2
3
4
+ (NSRunLoop *)currentRunLoop;//获取当前线程的runloop,没有则创建并返回
+ (NSRunLoop *)mainRunLoop NS_AVAILABLE(10_5, 2_0)//获取当前主线程的runloop

- (CFRunLoopRef)getCFRunLoop;//返回CFRunLoopRef
1
2
3
4
5
6
7
- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode;//将定时器添加到runloop中

- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode;//添加输入源端口到runloop中
- (void)removePort:(NSPort *)aPort forMode:(NSString *)mode;//将某个输入源端口移除

- (nullable NSDate *)limitDateForMode:(NSString *)mode;//获取下个响应时间
- (void)acceptInputForMode:(NSString *)mode beforeDate:(NSDate *)limitDate;//在某个时间期限前接收响应
1
2
3
- (void)run; //开始运行
- (void)runUntilDate:(NSDate *)limitDate;//到某个时间点运行
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;//某个期限前运行

注意上面提供的方法:输入源被注册进Runloop中时会有方法进行remove,但是定时器却没有,但是定时器中的invalidate方法可以将其从runloop中移除,正如官方文档的说明:invalidate是重要也是唯一的可以将定时器从runloop的注销的方法,所以如果我们创建了定时器,就一定要在不使用时调用invalidate方法。

CoreFoundation中RunLoop

CFRunLoopRef是CoreFoundation框架中runloop的类线程安全,关于CoreFoundation中关于runloop的有一下类:
  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

Markdown preferences pane

CFRunLoopModeRef

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响

CFRunLoopSourceRef事件产生的地方。分为Source0 和 Source1
  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程
CFRunLoopTimerRef基于时间的触发器

当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserverRef

是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化:

1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5),// 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),// 即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
CFRunLoopModeRef

苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)默认情况 和 UITrackingRunLoopMode追踪 ScrollView 滑动时的状态,你可以用这两个 Mode Name 来操作其对应的 Mode。

同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Item。需要注意的是一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
关于Mode更多点击这里

RunLoop 的内部逻辑

具体的流程如下图:

Markdown preferences pane

关于其中的逻辑讲解见参考博文中内容。

参考

1.iOS线下分享《RunLoop》

2.iOS中RunLoop机制浅探

3.深入理解RunLoop