离久的小站

简述RunLoop

2018/04/10 Share

相关知识

RunLoop 是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。

简单描述 RunLoop 的结构:

RunLoop 与 线程 的关系

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

Cocoa和CoreFundation都提供了RunLoop对象方便配置和管理线程的RunLoop(以下都已 Cocoa 为例)。每个线程,包括程序的主线程(main thread)都有与之相应的RunLoop对象。

注意:
Cocoa 中的 NSRunLoop 类并不是线程安全的

我们不能再一个线程中去操作另外一个线程的 RunLoop 对象,那很可能会造成意想不到的后果。不过幸运的是 CoreFundation 中的不透明类CFRunLoopRef 是线程安全的,而且两种类型的 RunLoop 完全可以混合使用。Cocoa中的 NSRunLoop 类可以通过实例方法:

1
- (CFRunLoopRef)getCFRunLoop;

获取对应的CFRunLoopRef类,来达到线程安全的目的。

RunLoop 接口

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

1
2
3
4
5
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

图形结构:

代码体现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
};

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。

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

CFRunLoopSourceRef

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

Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。

CFRunLoopTimerRef

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

CFRunLoopObserverRef

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

1
2
3
4
5
6
7
kCFRunLoopEntry             // 即将进入Loop
kCFRunLoopBeforeTimers // 即将处理 Timer
kCFRunLoopBeforeSources // 即将处理 Source
kCFRunLoopBeforeWaiting // 即将进入休眠
kCFRunLoopAfterWaiting // 刚从休眠中唤醒
kCFRunLoopExit // 即将退出Loop
kCFRunLoopAllActivities // 以上所有状态

CFRunLoopMode

以下是苹果文档列出的 Mode :

我们常用的是这三个 Mode :

1
2
3
kCFRunLoopDefaultMode   //默认
UITrackingRunLoopMode //追踪UI
kCFRunLoopCommonModes //占位符

这里有个概念叫 CommonModes :
一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

常见情况:我们常使用的 NSTimer ,若其在 NSDefaultRunLoopMode 中,则在我们操作UI时(例如滑动 tableView )会停止计时。因为我们操作UI时RunLoop已经切换到 UITrackingRunLoopMode。若 NSTimer 在 NSRunLoopCommonModes 中,则在 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 中都会保持计时。

CFRunLoop对外暴露的管理 Mode 接口只有下面2个:

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

Mode 暴露的管理 mode item 的接口有下面几个:

1
2
3
4
5
6
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

CFRunLoopObserverRef

RunLoop 还可以通过添加 Observer(观察者)来监听 RunLoop 所处的状态变化。

RunLoop 相关

AutoreleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显式创建 Pool 了。

GCD

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

PerformSelecter

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

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

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

CATALOG
  1. 1. 相关知识
  2. 2. RunLoop 与 线程 的关系
  3. 3. RunLoop 接口
    1. 3.1. CFRunLoopSourceRef
    2. 3.2. CFRunLoopTimerRef
    3. 3.3. CFRunLoopObserverRef
    4. 3.4. CFRunLoopMode
    5. 3.5. CFRunLoopObserverRef
  4. 4. RunLoop 相关
    1. 4.1. AutoreleasePool
    2. 4.2. GCD
    3. 4.3. PerformSelecter
    4. 4.4. 界面更新