线程中的runloop
在开发中,我们会经常接触到线程,比如在主线程中更新UI,在子线程中异步请求等,而线程中最重要的一个组成部分便是runloop,其是用来管理线程的。runloop在线程中有以下两个作用:
- 保证线程不退出
- 监听并处理事件,使得线程有事件时工作,无事件时睡眠
每个线程中都有一个runloop,主线程中的runloop是在应用程序启动时创建的,默认是开启的,子线程中的runloop采用的是延迟加载的方式,如果我们不主动获取,子线程的runloop是不会创建的。
解析runloop
runloop三要素: Modes(模式)、sources(源)、Observers(观察者)。当有事件源触发时,runloop会被唤醒并处理事件源的方法,并通知该runloop运行模式下的所有观察者。
Run Loop Modes(运行循环模式)
运行循环在工作时,会有多种运行模式。我们常用到的模式有:NSDefaultRunLoopMode和NSRunLoopCommonModes 。每个RunLoop Mode都可以看作是一个集合,其中包含了其监听的事件源以及在事件发生时需要通知的RunLoop Observers。在相应的模式下,只有对应于该模式的source会被处理,同样也只有对应于该模式的observer会被通知。其实运行循环模式起到一个过滤作用,可以过滤到我们不关心的其他的source事件。
Cocoa 和 Core Foundation框架定义了一个默认的和一些常用的运行循环模式,如下表:
Mode | Name | Description |
---|---|---|
Default | NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) | 默认模式,一般设置为此模式. |
Connection | NSConnectionReplyMode (Cocoa) | Cocoa用该模式来监听NSConnection请求的回复,该模式为系统使用,我们一般不会用到 |
Modal | NSModalPanelRunLoopMode (Cocoa) | Cocoa 用此模式来区分 modal panels事件. |
Event tracking | NSEventTrackingRunLoopMode (Cocoa) | Cocoa 在该模式下,限制其他source的事件 |
Common modes | NSRunLoopCommonModes (Cocoa)kCFRunLoopCommonModes (Core Foundation) | 这是一个可配置的模式组,在该模式下的source 事件,和模式组里的其他模式都会产生联系。在Cocoa框架中,该模式默认包含Default、Modal和 Event tracking 三种模式,在Core Foundation中默认只包含Default模式,我们可以用CFRunLoopAddCommonMode 函数来添加自定义的模式。 |
Run Loop Sources
先来看runloop和sources的结构图
runloop可以监听并处理事件,其监听的事件源有两种类型:Input sources(输入源) 和** Timer sources(定时器源)**。输入源异步传递事件到线程中,通常消息来自其它线程或不同的应用,定时器源同步传递事件。
- Input sources
输入源异步发送事件到线程中,输入源共有两种类型:一种是基于端口的输入源(Port-based input sources),一种是自定义输入源(Custom input sources)。基于端口的数据源监听应用的Mach端口,自定义输入源监听自定义的事件源,二者的唯一区别是:基于端口的输入源的信号是由内核自动触发的,自定义输入源的信号是由其它线程手动触发的。
-
Port-Based Sources
Cocoa 和 Core Foundation框架给我们提供了一些创建基于端口的源的对象和函数。
在 Cocoa中,我们只需要创建一个 NSPort对象,并将其添加到运行循环中就可以了,NSPort对象会自己创建输入源。NSPort *port = [NSPort port]; [port scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
但是在Core Foundation中,我们就需要手动创建端口和输入源,我们可以用CFMachPortRef, CFMessagePortRef, or CFSocketRef来创建相应的对象
Custom input sources
我们也可以自定义输入源,自定义输入源时,我们需要配置定义输入源的行为,运行循环模式,输入源事件的传递机制,以及销毁输入源等。Cocoa Perform Selector Sources
除了基于端口的源,Cocoa也定义了自定义输入源--Perform Selector Sources,这使得我们可以在任意线程中执行我们的方法,Perform Selector 的方法在目标线程中是串行执行的,这样就解决了多个方法在一个线程中执行的同步问题,和基于端口的源不同,Perform Selector 在方法执行完成之后,会将自己从runloop中移除。
Note: 当我们在目标线程上执行我们的方法时,目标线程的运行循环必须开启,否则方法不会执行。主线程的运行循环默认开启,子线程的运行循环默认不开启,因此我们想要在子线程上运行我们的程序,需要手动开启子线程的运行循环。
- Timer Sources(定时器源)
定时器源发送同步事件到我们的线程中,对于线程来说,定时器是一种通知自己做一些事情的方式。虽然,定时器源会发出基于时间的通知,但是定时器并不是实时机制,跟输入源一样,定时器源也跟runloop的运行模式相关,如果定时器的模式,不在运行循环当前监控的模式中,定时器方法是不会执行的。另外如果定时器触发时,运行循环正在执行操作,定时器将会在下次运行循环调用时触发。
Run Loop Observers
运行循环在运行时,会发出一些通知,我们可以通过注册Observers来监听运行循环当前所处的状态,运行循环状态有以下几种:
- 即将进入运行循环
- 运行循环将要处理定时器
- 运行循环将要处理输入源
- 运行循环将要进入睡眠
- 运行循环被唤醒,但是还未处理事件
- 运行循环退出
The Run Loop Sequence of Events(运行循环执行事件的顺序)
- 通知观察者即将进入运行循环
- 通知观察者将要处理定时器事件
- 通知观察者将要处理输入源(基于端口的源除外)事件
- 处理输入源(基于端口的源除外)事件
- 如果有基于端口的输入源事件,则立即处理。然后跳到第9步。
- 通知观察者线程将要进入休眠
- 线程进入休眠,等待被唤醒,下列事件可以唤醒线程
- 基于端口的源的事件
- 定时器事件
- 设置的runloop超时时间到期
- runloop主动被唤醒
- 通知观察者线程刚被唤醒
- 处理挂起的事件
- 如果有用户定义的定时器事件触发,处理定时器事件,并且重启运行循环,进入第2步
- 如果触发的是输入源事件,传送事件
- 如果runloop被主动唤醒,并且没有超时,重启runloop,进入第2步.
- 通知观察者运行循环将要退出
代码示例后续会整理分享