一般写的程序都是像下面这样的,代码一行行执行,到return的时候代码都已经执行完毕,程序退出。
1 | int main(int argc, char * argv[]) { |
但实际的应用中,我们的软件不可能像这样线性的执行,执行完我们的软件就退出,所以就需要一种技术,让我们的软件始终保持运行状态。在iOS系统中,这项技术就是Runloop。
0x01 主要的结构信息
1. CFRunloop
Runloop都会对应一个线程。而且Runloop可以有多种模式,但是当前使用的只能是一种模式。我们的CFRunloop结构如下
1 | struct __CFRunLoop { |
2. CFRunLoopMode
Mode管理着各种事件,我们的source0、source1、observer、timer都归mode管理。每个模式下又包含多个source0,source1,observer和timer。不同mode下的source0、source1、observer、timer都是隔离开的。
如果Mode里面没有source0,source1,observer和timer,Runloop会立马退出。
模式的结构如下:
1 | struct __CFRunLoopMode { |
3. CFRunLoopSource
source分为source0和source1。
1 | struct __CFRunLoopSource { |
source0一般都是应用的内部事件,比如触摸事件、CFSocket等。
1 | typedef struct { |
source1一般与mach_port通信,所以接收的是内核态的信息。
1 | typedef struct { |
4. CFRunLoopObserver
观察runloop的各种状态。
1 | struct __CFRunLoopObserver { |
可被观察的状态有如下几种
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
5. CFRunLoopTimer
可以在设定的时间到达后触发回调。
1 | struct __CFRunLoopTimer { |
0x02 获取线程
1. CFRunLoopGetMain
内部调用了_CFRunLoopGet0
函数,传入的参数是主线程。
1 | CFRunLoopRef CFRunLoopGetMain(void) { |
2. CFRunLoopGetCurrent
跟CFRunLoopGetMain
函数一样,调用的同样是_CFRunLoopGet0
函数,只是参数不同,这里的入参是当前所在线程。
1 | CFRunLoopRef CFRunLoopGetCurrent(void) { |
3. CFRunLoopGet0
既然CFRunLoopGetMain
和CFRunLoopGetCurrent
都调用了_CFRunLoopGet0
函数,我们看下这个函数的实现。
1 | CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { |
获取线程流程总结如下:
- 有个全局变量保存着各个线程与各个runloop对象的关系,该变量初始化的时候会同时创建一个主线程对应的runloop对象。
- 子线程的runloop默认是获取的时候才开始创建。所以多线程环境中,只有主线程的runloop是一开始就创建出来的,其他线程被创建的时候并不会一起创建一个runloop对象。
- runloop对象的生命周期和线程的生命周期同步。
0x03 创建RunLoop
上面我们的RunLoop是在获取的时候被创建的,创建源码如下
1 | static CFRunLoopRef __CFRunLoopCreate(pthread_t t) { |
构建一个mode的时候会同时创建一个GCD Queue,用来处理时间相关的任务。
0x04 运行RunLoop
runloop内部其实就是一个do…while()循环。
1 | void CFRunLoopRun(void) { /* DOES CALLOUT */ |
调用的是CFRunLoopRunSpecific
函数,用kCFRunLoopDefaultMode
模式进行启动
1 | SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ |
看下最重要的__CFRunLoopRun
函数
1 | static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { |
启动runloop主要流程
- 通知observers即将进入runloop
- 通知observers即将处理timers事件
- 通知observers即将处理source事件
- 处理blocks
- 处理source0
- 通知observers即将休眠(内部有个循环接收消息)
- 通知observers结束休眠,并回到第2步
- 处理消息
- 处理timer事件
- 处理主线程任务
- 处理blocks
- 回到第2步
- 通知observers即将退出runloop
0x05 使用
NSTimer
在RunLoop中,关于定时器经常遇到的问题就是滑动的时候定时器就不走动了。解决方案也是很熟悉,把timer加入NSRunLoopCommonModes
或者同时加入到NSDefaultRunLoopMode
和UITrackingRunLoopMode
。
1 | [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; |
其中NSRunLoopCommonModes
其实是一个mode集合,里面包括所有的被标记为common
的mode,比如NSDefaultRunLoopMode
、UITrackingRunLoopMode
。
RunLoop同一时刻下只能跑在一个mode上,所以Timer不走动就是因为如果只加入到一种mode下,切换到别的mode,比如滑动页面就会切换到UITrackingRunLoopMode
,Timer就因为在该模式下没有注册,所以不会响应Timer事件。
线程保活
比如子线程内请求一个网络数据,但是等待完成需要等一会,这就会导致得到数据的时候子线程就被回收掉了。要想子线程一直存在,我们可以利用RunLoop,在子线程写下如下代码就可以使得线程保活。
1 | NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; |
但是这样又会有一个问题,我们看下对于run
方法的描述
1 | Puts the receiver into a permanent loop, during which time it processes data from all attached input sources. |
简单来说就是run
方法内部是个无限循环,无限调用runMode:beforeDate:
方法,并且即时删除mode下所有的source和timer也不能停止。如果创建多个线程,并都通过run启用RunLoop,就会造成内存泄露的问题,如果想要其变得可控,官方也给了建议,自己手动写了while,并且通过一个全局变量来控制什么时候结束这个循环。
1 | BOOL shouldKeepRunning = YES; // global |
停止RunLoop的伪代码可以写成这样的
1 | - (void)stop { |
我们总结一下启动RunLoop的几种方式
run
无限循环,终止不掉
runUntilDate:
内部同样重复调用
runMode:beforeDate:
,但是时间到了就结束,不再调用;或者通过CFRunLoopStop
结束runMode:beforeDate:
只调用一次;或者通过
CFRunLoopStop
结束
AutoreleasePool
我们知道[NSRunLoop currentRunLoop]
用来获取当前RunLoop,如果没有就会创建一个RunLoop,我们看下其内部汇编代码,发现会自动开启AutoreleasePool。
1 | Foundation`+[NSRunLoop(NSRunLoop) currentRunLoop]: |