Runloop

Runloop 是一种事件处理模型;在很多系统和框架中都有实现。它的作用是保证线程能不停的处理新的任务,而不是处理完任务之后就退出。

通常一个线程在处理完任务之后就会立刻退出,避免占用资源。但是对于 App 来说,主线程需要等待用户的操作并且能立刻处理事件做出响应。就需要一种机制满足以下需求:

  • 处理完之后不会退出
  • 线程能随时响应并处理事件
  • 线程等待时不占用太多资源

线程处理完事件不退出并且能随时响应的需求可以通过一个循环功能实现:

1
2
3
4
5
void run() {
do {
// handle new task
} while(True)
}

循环不会占用太多的资源;这种实现的重点就在于:

  • 如何让线程在没有新的事件时休眠,避免浪费资源
  • 如果管理事件,让有新的事件时通知到线程并将它唤醒

Cocoa 中的实现

Cocoa 中的 Runloop 实现是一个对象,通过个对象管理其需要处理的事件和消息;并提供一个入口函数来开启事件循环的逻辑。

线程调用 Runloop 对象的入口函数后就会一直处于这个函数内部的 接收消息 –> 处理事件 –> 继续等待 的循环中,直到循环结束(收到退出的消息)为止

  • CFRunloopRefCoreFoundation 中的 Runloop 实现,提供了纯 C 的 Api 接口;所有 Api 都是线程安全的
  • NSRunloop 是对 CFRunloopRef 的面向对象封装,该对象的 Api 不是线程安全的

Runloop 与线程的关系

CFRunloopRef 是基于 pthread 来管理的,Runloop 不能直接创建,只能通过 CFRunLoopGetMain() & CFRunLoopGetCurrent() 两个函数获取

  • 线程和 Runloop 之间是一一对应的。系统维护了一个全局的 hash 表,pthread 作为键值,对应的 Runloop 是 value
  • 线程对应的 Runloop 并不会随线程一起创建,只有第一次获取 Runloop 时才会创建
  • 当线程被销毁时,对应的 Runloop 则会随之销毁

由于 Runloop 不主动获取不会创建,所以一些需要在 Runloop 中执行的方法在子线程中调用时可能会无效;主线程的 Runloop 会被系统早早的创建,所以不存在这个问题

Runloop 实现

CoreFoundation 中关于 Runloop 有 5 个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

其中 CFRunLoopRef 类没有对外暴露,它们之间的关系结构如下:

CFRunLoopRef

  • 一个 Runloop 包含多个 Mode;一个 Mode 又包含多个 Source/Timer/Observer;Source/Timer/Observer 被抽象为 Mode Item
  • 一个 Item 可以同时加入多个 Mode 中
  • 一个 Item 被多次加入到同一个 Mode 中是无效的
  • 如果当前的 Mode 中没有 Item,Runloop 会直接退出,不进入循环

Mode

每次调用 Runloop 的主函数开启一次循环时,只能指定其中一个 Mode 作为 currentMode。如果需要切换 Mode,只能退出此次循环,再重新指定一个 Mode 并开启一个新的循环。这样做的目的是将不同 Mode 下的 Source/Timer/Observer 分隔,让其不要互相影响

CFRunLoopMode 的代码结构如下

1
2
3
4
5
6
7
8
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...... // Others
};

CommonModes

Runloop 中有个抽象叫 CommonModes:一个 Mode 可以将自己标记为 Common,它的 name 会被添加到 Runloop 的 _commonModes 属性中。之后每当 Runloop 的内容发生变化时,Runloop 都会将 _commonModeItems 中的 item 同步到 _commonModes 中记录的 Mode 中

例如主线程中的 kCFRunLoopDefaultMode & UITrackingRunLoopMode 两个 Mode 被标记为 Common;kCFRunLoopDefaultMode 是默认的 Mode,UITrackingRunLoopMode 是 ScrollView 滑动时的 Mode;当新的 item 被添加到 _commonModeItems 中时,会被同步到这两个 Mode 中,意味着它能在 App 默认状态和 ScrollView 滚动状态同时生效

  • Runloop 的结构
1
2
3
4
5
6
7
struct __CFRunLoop {
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...... // Others
};

Source

CFRunLoopSourceRef 是事件的封装。Source 有两个版本:

  • Source0:只包含一个回调(函数指针),不能主动触发事件。使用时需要手动唤醒 Runloop 来处理它
  • Source1:包含了一个 mach_port 和一个回调(函数指针),用于通过内核和其他线程通信,可以主动唤醒 Runloop

Timer

CFRunLoopTimerRef基于时间的触发器,包含一个时间长度和一个回调(函数指针);当其被加入到 Runloop 时,Runloop 会注册到对应的时间点,当时间到时会唤醒 Runloop 执行回调

Observer

CFRunLoopObserverRef 是 Runloop 观察者,每个 Observer 包含一个回调(函数指针),当 Runloop 的状态发生变化时,会调用对应观察者的回调

1
2
3
4
5
6
7
8
9
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 进入 Runloop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠等待
kCFRunLoopAfterWaiting = (1UL << 6), // 从休眠中被唤醒
kCFRunLoopExit = (1UL << 7), // 退出
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有事件
};

Runloop 接口

  • Mode 接口

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

只能通过 modeName 来管理内部的 Mode;当 Add 一个 modeName 而内部没有对应的 Mode 时系统会自动创建对应的 Mode;Runloop 中的 Mode 只能增加不能删除

  • Source 接口

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);

  • Timer 接口

CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode) ;
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mo de);

  • Observer 接口

CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFString Ref modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStr ingRef modeName);

Runloop 与 AutoreleasePool

App 启动后会在主线程 Runloop 中注册两个 Observer,回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

在这个回调处理函数中,会区分事件的类型然后调用不同的逻辑处理 AutoreleasePool

  • 第一个 Observer 只监听一个 kCFRunLoopEntry 事件;该事件发生时会通过 _objc_autoreleasePoolPush() 创建 AutoreleasePool;它的优先级最高,保证在所有回调执行之前创建 AutoreleasePool
  • 第二个 Observer 监听了 kCFRunLoopBeforeWaitingkCFRunLoopExit 两个事件;它的优先级最低,保证在所有回调之后执行逻辑
    • kCFRunLoopBeforeWaiting 休眠时会调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的 AutoreleasePool 并立刻创建新的 AutoreleasePool
    • kCFRunLoopExit 会调用 _objc_autoreleasePoolPop() 释放 AutoreleasePool

之所以要在即将休眠 kCFRunLoopBeforeWaiting 的回调中销毁并立刻创建一个新的 AutoreleasePool;而不是在休眠唤醒 kCFRunLoopAfterWaiting 的回调中再创建 AutoreleasePool。是因为可能收到的信号是退出信号,这时 Runloop 不会触发休眠唤醒回调;而是直接触发 kCFRunLoopExit 回调

Runloop 与事件响应

每个 App 进程注册了一个 Source1(主动唤醒 Runloop)的回调来接受系统的事件,回调函数为 __IOHIDEventSystemClientQueueCallback()

当设备硬件的事件(触摸、旋转、锁屏等)发生后,会经历这样几个流程:

  1. IOKit.framework 框架对事件进行处理,并封装成 IOHIDEvent 对象并发送到 SpringBoard
  2. SpringBoard 是主屏幕(同时它还处理动画和屏幕绘制)。SpringBoard 只会处理触摸、加速、按键等几种事件;处理后会将事件通过 mach port 发送到对应的 App 进程中
  3. 事件发送到 App 进程中后会触发事件回调,在回调中调用 _UIApplicationHandleEventQueue() 方法
  4. _UIApplicationHandleEventQueue() 方法将事件转为 UIEvent 对象并按不同的类型进行处理或分发

其中触摸、旋转等会发送给 UIWindow;通常的点击事件、touchesBegin/Move/End 触摸回调都是在这个回调中完成的

_UIApplicationHandleEventQueue 中的手势识别

当进程内的回调接收到 Event 并判别它是一个手势之后,首先会取消当前的触摸回调 touchesBegin/Move/End,并将对应的手势标记为待处理,等待下一步的触摸动作以识别出具体的手势。

这个地方也用到了 Runloop 的 Observer,系统注册了一个 BeforeWaiting(即将休眠)的回调,在当次 Runloop 休眠之前通过 _UIGestureRecognizerUpdateObserver() 回调函数获取到所有被标记为待处理的手势,通过一系列手势识别出具体的手势类型并执行对应手势的逻辑

当手势发生了变化(创建、销毁、状态改变)时,这个回调都会进行相应的处理。例如:有些手势(长按)跨越了不止一个 Runloop,就需要在休眠前改变手势的状态等待下一个 Runloop 处理

Runloop 与界面更新

在 UI 更新(Frame 改变、UIView/CALayer 层级改变、手动调用 setNeedsLayout/setNeedsDisplay)时,对应的 UIView/CALayer 会被标记为待处理,然后打包成事务提交到一个全局容器中(SpringBoard)。

系统注册了一个 Observer 监听 BeforeWaiting (即将进入休眠) 和 Exit (即将退出Loop) 事件,该 Observer 的回调会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面

Runloop 与动画

CoreAnimation 同样在 RunLoop 中注册了两个个 Observer,分别监听了 EntryBeforeWaiting & Exit 事件(与 AutoreleasePool 类似,会在一个 Runloop 中自动开启一个 Transaction)

CALayer 会捕获可动画的视图变化(例如 改变 frame,修改透明度,以及添加自定义动画等),之后通过 CATransaction 将所有变化打包提交到一个中间状态栈中。

当 RunLoop 即将进入休眠(或退出)时,Observer 回调会被触发将所有中间状态中的变化提交到 GPU 中进行渲染,之后 CoreAnimation 会通过 DisplayLink 等机制多次触发相关流程展示动画

Runloop 与定时器

iOS 中的定时器主要有 NSTimerCADisplayLink 两种

  • NSTimer: 其实就是 CFRunLoopTimerRef,注册到 Runloop 中后,RunLoop 会为其在指定的时间点注册事件,到点触发回调。但是 RunLoop 为了节省资源,并不会在非常准确的时间点执行这个回调;在资源紧张的时候回调甚至会被跳过不触发
  • CADisplayLink:内部实质是通过 Source 设置的,是一个和屏幕刷新率一致的定时器;同样会在资源紧张的时候会被 Runloop 丢弃某次回调
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2017-2021 HonQi

请我喝杯咖啡吧~