0%

RunLoop

一般来说,一个线程只能执行一个任务,执行完成后线程就会退出。而事件循环(即一个while循环)能让线程能随时处理事件但不退出。

1
2
3
4
5
while (alive) {
performTask() //执行任务
callout_to_observer() //通知外部
sleep() //休眠
}

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在首次获取时创建(通过currentmain获取),在线程结束时销毁。
  • 主线程的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
2
3
4
5
6
7
8
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
};

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:kCFRunLoopDefaultModeUITrackingRunLoopMode。这两个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中。

参考

欢迎关注我的其它发布渠道