RunLoop 的学习涉及到 CoreFoundation 层次的源码分析,以我目前的知识贮备只能了解下相关概念,所以这篇文章更多的是从大神们的博客或视频中进行一些关键知识点的抽取,给自己建立起相关的知识框架,为今后的进阶打点基础。

主要参考了:

孙源@sunnyxx 的分享视频

ibireme 的博客

概念

一般来讲,一条线程一次只能执行一个任务,执行完成后线程就会退出。但我们也需要一种机制,保证一条线程(比如主线程)能随时待命不退出。通常来说实现逻辑可以简化成一个 do while 循环,循环中没有事件输入时线程处于睡眠状态,反之则立即唤醒处理事件,然后再次进入睡眠状态等待下次事件输入,直到接收到退出指令。

这种模型被称作事件循环(Event Loop),比如 Node.js 的事件处理,Windows 程序的消息循环都属于此类,当然也包括 RunLoop。所以,RunLoop 实际上就是一个对象,用来执行事件循环的逻辑。

OSX/iOS 系统中,提供了两个这样的类:NSRunLoop 和 CFRunLoopRef。

  • CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。

  • NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

RunLoop与线程

程序启动时会在 main() 函数内调用 UIApplicationMain() 函数,它会为主线程设置一个 NSRunLoop 对象。因此,在主线程中 RunLoop 是默认启动的。而对于其它线程,刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。

苹果并没有提供直接创建 RunLoop 的方法,我们只能通过两个获取方法获得:

对于子线程,RunLoop 将在第一次调用上述方法时创建。

需要注意每一条线程只能对应一个 RunLoop,不过在这个 RunLoop 中可以嵌套多个子 RunLoop。

RunLoop 的构造

CFRunLoopRef 是开源的(在这里查看),我们可以通过分析其源码一探 RunLoop 的内部构造。

在 CoreFoundation 里面关于 RunLoop 有 5 个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

他们的关系是:一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。

RunLoop Mode

同一时间一个 RunLoop 只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode。如果需要切换,只能退出 RunLoop,再重新指定一个 Mode 启动。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

App 启动后,系统默认注册了 5 个 Mode:

  • CFRunLoopDefaultMode: App的默认 Mode,通常主线程都是在这个 Mode 下运行的;
  • UITrackingRunLoopMode: 界面跟踪 Mode,当 ScrollView 滑动时调用,保证此时不受其它 Mode 影响;
  • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用;
  • GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到;
  • CFRunLoopCommonModes:一个由不同 Mode 组成的数组。当 RunLoop 的内容发生变化时,会自动将 Source/Observer/Timer 同步到数组内的所有Mode里。主线程 RunLoop 中的 RunLoopCommonModes 默认包含 RunLoopDefaultMode 和 TrackingRunLoopMode。

RunLoop ModeItem

CFRunLoopMode 中包含的 CFRunLoopSource/CFRunLoopTimer/CFRunLoopObserver 统称为 mode item。一个 item 可以被同时加入多个 mode,但重复加入同一个 mode 时是不会有效果的。如果 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

CFRunLoopSourceRef

事件产生的地方。Source有两个版本:Source0 和 Source1。

  • Source0 :处理的是App内部的事件、App自己负责管理,如按钮点击事件等。它只包含了一个回调(函数指针),并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1:包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。

CFRunLoopTimerRef

基于时间的触发器。它和 NSTimer 是可以相互混用的,包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserverRef

观察者。每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化并告知外界。可以观测的时间点有以下几个:

RunLoop 的内部逻辑

可以根据上面 Observer 给出的时间点将 RunLoop 不同的状态大致区分如下:

  1. 进入:通知 Observer;
  2. 执行阶段:按顺序通知 Observer 并执行 timer,source0;若有 source1 执行 source1;
  3. 休眠阶段:利用 mach_msg 判断进入休眠,通知 Observer;被消息唤醒通知 Observer;
  4. 执行阶段:回到第 2 阶段按消息类型处理事件;
  5. 判断退出条件:如果符合退出条件(一次性执行,超时,强制停止,modeItem 为空)则退出,否则回到第 2 阶段;
  6. 退出:通知 Observer。

ibireme 大神也制作了一张简图进行说明:

RunLoop_1

系统框架中的 RunLoop

AutoreleasePool

App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer。

第一个 Observer 监视事件 kCFRunLoopEntry(即将进入 Loop),会调用 objcautoreleasePoolPush() 方法创建自动释放池。其优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视两个事件:kCFRunLoopBeforeWaiting(即将进入休眠)时调用 objcautoreleasePoolPop() 方法和 objcautoreleasePoolPush() 方法释放旧的池并创建新池;kCFRunLoopExit(即将退出 Loop)时会调用 objcautoreleasePoolPop() 来释放自动释放池。这个 Observer 的优先级最低,保证其释放池子发生在其他所有回调之后。

NSTimer 实际上是对 CFRunloopTimerRef 的封装,如上文所说,它们之间是可以相互混用的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。

为了节省资源,RunLoop 并不会在非常准确的时间点回调这个Timer。可以通过 NSTimer 中的 Tolerance (宽容度)属性设置时间点到后,容许的最大误差。

CADisplayLink 是一个和屏幕刷新率一致的定时器。和 NSTimer 相似,它也会跳过被错过的时间点,表现在屏幕上就是有一帧被跳了过去,在界面快速滑动的情况下会造成卡顿的感觉。

PerformSelecter 系列方法

当调用 NSRunLoop 类的 performSelecter:afterDelay: 方法后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

调用 NSThread 的 performSelector:onThread: 方法时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

RunLoop 的实际应用

设置 NSTimer 的 RunLoop Mode

这算是最常见的一种应用方式了。NSTimer 对象创建后默认是以 NSDefaultRunLoopMode 在 RunLoop 中执行,在页面滑动(即切换到 UITrackingRunLoopMode )时会暂停回调。因此如有需要,我们可以调用 NSRunLoop 的方法:

手动将 NSTimer 对象添加到 NSRunLoopCommonModes 中保证在页面滑动时也能够继续执行。

简化处理页面滑动时的性能优化代码

主要应用到 NSRunLoop 中的方法:

在 scrollView 及其子类处于滑动状态下的数据刷新,如果有图片之类大容量数据的话容易卡顿。相比通过代理方法监听状态然后需要再构建属性或方法作为设置的判断条件,可以通过这个方法直接搞定。比如对一个自定义 tableViewCell 中的 imageView:

发表评论

电子邮件地址不会被公开。 必填项已用*标注