一般来说,一个线程只能执行一个任务,执行完成后线程就会退出。而事件循环(即一个while循环)能让线程能随时处理事件但不退出。
1 | while (alive) { |
RunLoop是一种事件循环。它可以让线程有任务时忙碌,没有任务时睡眠。RunLoop提供一个入口函数执行事件循环,执行后,就一直处于“接受消息->等待->处理” 的循环中,直到这个循环结束,该函数返回。
RunLoop作用:
- 保持程序持续运行,而不是执行完任务退出;
- 处理事件,这是保持运行的目的;
- 节省CPU资源。当RunLoop休眠的时候,CPU可以吧时间片分配给其他事务。如果RunLoop在某次循环之后,发现程序突然没有收集到更多事件供它处理,它就会休眠,停在RunLoop循环里面的某段代码上。过一会程序为RunLoop接收到了新来的事件,其循环就被系统重新激活以继续运行。
CFRunLoopRef是在CoreFoundation框架中,提供了C函数API,都是线程安全的。
NSRunLoop是CFRunLoopRef的封装,但不是线程安全的。
RunLoop的线程休眠是通过__CFRunLoopServiceMachPort
函数实现的,内部使用了mach_msg
函数,这是内核提供的API,实现内核层面的线程休眠。而一般while循环,CPU还是会一直执行指令,占用CPU资源。
与线程的关系
- RunLoop就是用来管理线程的,当线程RunLoop开启后,线程在执行完任务后不会退出,而是处于休眠状态,随时等待接受新的任务。没有RunLoop,就不可能执行多任务,延时任务也不会执行。
- 线程和RunLoop一一对应,其关系存在一个全局字典中。
- 只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的。
- RunLoop在首次获取时创建(通过
current
和main
获取),在线程结束时销毁。 - 主线程的RunLoop时系统已经创建好了;但子线程的则要自己主动创建,并启动。
API使用
CFRunLoopSourceRef
事件产生的地方。包含两个版本:
- Source0:只包含一个回调函数指针,不能主动触发事件。使用时需先调用
CFRunLoopSourceSignal(source)
标记Source为待处理,然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop处理这个事件。- 包含触摸事件处理、
performSelector
。
- 包含触摸事件处理、
- Source1:包含一个mach_port和回调函数指针,被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop线程。
- 包含给予port的线程间通信、系统事件捕捉。
如触摸事件,手指点击屏幕,首先产生一个系统事件,通过Source1来接受捕捉,然后由Springboard程序包装成Source0分发到App处理,因此在App内接收到的触摸事件就是Source0的。
CFRunLoopTimerRef
基于时间的触发器。包含一个时长和回调函数指针。
CFRunLoopObserverRef
观察者,每个Observer包含一个回调函数指针,当RunLoop状态发生变化时,观察者能通过回调接收到这个变化。可以监听:
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
Mode
一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer。每次调用RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。Source/Timer/Observer称为mode item,一个item可以被同时加入多个mode。如果一个mode中没有item,则RunLoop会直接退出,不进入循环。
CommonModes:一个Mode把自己标记为“Common”属性(通过将其ModeName添加到RunLoop的commonModes
中)。每当RunLoop内容发生变化时,RunLoop都会自动将_commonModeItems
里的Source/Observer/Timer同步到具有“Common”标记的所有mode里。
主线程的RunLoop有两个预设的mode:kCFRunLoopDefaultMode
、UITrackingRunLoopMode
。这两个Mode都被标记为“Common”属性。defaultMode是App平时所处的状态,trackingRunLoopMode是追踪ScrollView滚动时的状态。所以把Timer添加到defaultMode时,在滚动列表时,RunLoop会将mode切换为trackingRunLoopMode,使得Timer不会回调。
要让Timer在滚动时也能回调,可以把Timer分别添加到defaultMode和trackingRunLoopMode中,或者加入到顶层的commonModeItems中。
具体应用
dispatch_get_main_queue
在主线程中转交给RunLoop调起该方法。注意,只是回到主线程这一步是交给RunLoop处理。
AutoreleasePool
NSAutoreleasePool是对象引用计数自动处理器。当对象加入到NSAutoreleasePool时,会对其retain
,当NSAutoreleasePool结束时,会对其所有对象发送一次release
消息。NSAutoreleasePool可以以栈的方式组织。
使用容器的block版本的枚举器时会自动添加AutoreleasePool。for循环则没有。
iOS在主线程的RunLoop中注册了2个Observer:
- 第1个Observer监听
kCFRunLoopEntry
(即将进入RunLoop)事件,会调用objc_autoreleasePoolPush()
创建自动释放池,使用最高优先级保证创建在其他回调之前进行。 - 第2个Observer
- 监听
kCFRunLoopBeforeWaiting
(即将进入休眠)事件,会调用objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
释放旧的池并创建新的池。 - 监听
kCFRunLoopExit
(即将退出Runloop)事件,会调用objc_autoreleasePoolPop()
释放自动释放池,使用最低优先级保证释放池在其他所有回调之后进行。
- 监听
AutoreleasePool的释放时机
系统在每个runloop中都创建一个Autorelease Pool,并在runloop的末尾进行释放,所以,一般情况下,每个接受autorelease消息的对象,都会在下个runloop开始前被释放。也就是说,在一段同步的代码中执行过程中,生成的对象接受autorelease消息后,一般是不会在作用域结束前释放的。Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放。
所以在AutoreleasePool声明的局部变量,在外面就释放了。
子线程会默认包裹一个AutoreleasePool,当线程退出时才释放其中的变量。
事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
手势识别
界面更新
定时器
CFRunLoopTimerRef。CADisplayLink。
performSelecter:afterDelay:
内部会创建定时器添加到当前的RunLoop中。