RunLoop 01 - 原理
RunLoop 的概念
- 一个 RunLoop 就是一个处理事件的循环,用来不停的调度工作及处理输入事件。
- RunLoop 是线程基础框架的一部分,可以让一个线程保持运行状态并能随时处理事件,当没有事件需要处理时进行休眠以避免占用资源。
- iOS 程序主线程中的 RunLoop 可以保持程序的持续运行,并处理各种事件(触摸事件、定时器事件等)。
事件循环模型:
function loop() { initialize(); do { var message = get_next_message(); } while (message != quit); }
RunLoop 应用范畴
- Timer、PerformSelector
- GCD Async Main Queue
- 事件响应、手势识别、界面刷新
- 网络请求
- AutoreleasePool
获取 RunLoop
- NSRunLoop
- 获取当前线程的 RunLoop:
[NSRunLoop currentRunLoop]
- 获取主线程的 RunLoop:
[NSRunLoop mainRunLoop]
- 获取当前线程的 RunLoop:
- CFRunLoopRef(源码)
- 获取当前线程的 RunLoop:
CFRunLoopGetCurrent()
- 获取主线程的 RunLoop:
CFRunLoopGetMain()
- 获取当前线程的 RunLoop:
CFRunLoopRef 在 CoreFoundation 框架内,提供了一套纯 C 函数的 API,所有 API 都是线程安全的。
NSRunLoop 在 Cocoa 框架内,是对 CFRunLoopRef 的封装,提供了一套面向对象的 API,但是这些 API 不是线程安全的。
RunLoop 与线程
- 线程与 RunLoop 之间是一一对应的,对应关系保存在一个全局的 Dictionary 中,key 为线程,value 为 RunLoop。
- 线程刚创建时没有 RunLoop,第一次获取时会创建,iOS 程序启动后会默认为主线程创建 RunLoop。
- RunLoop 的销毁发生在线程结束时。
- 只能在线程内部获取其 RunLoop(主线程除外)。
CoreFoundation 中 RunLoop 相关的类
- CFRunLoopRef
- CFRunLoopModeRef(没有对外暴露,通过 CFRunLoopRef 的接口进行了封装)
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
它们之间的关系如下:
CFRunLoopRef & CFRunLoopModeRef 结构
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
pthread_t _pthread; // RunLoop 对应的线程
CFMutableSetRef _commonModes; // Set<String - Mode Name>
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current RunLoop Mode
CFMutableSetRef _modes; // Set<Mode>
};
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name,例如 "kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set<Source>
CFMutableSetRef _sources1; // Set<Source>
CFMutableArrayRef _observers; // Array<Observer>
CFMutableArrayRef _timers; // Array<Timer>
};
- Mode Item:
- Source/Timer/Observer 被统称为 Mode Item。
- 一个 Item 可以被同时加入多个 Mode。
- 一个 Item 被重复加入同一个 Mode 时在某一次循环中是不会被重复处理的。
- Mode:
- CFRunLoopModeRef 代表 RunLoop 的运行模式。
- 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Mode Item。
- 每次调用 RunLoop 主函数时只能指定其中一个 Mode 作为 _currentMode。
- 如果需要切换 Mode,只能退出当前 Loop,再重新指定一个 Mode 进入。这样可以分隔开不同组的 Mode Item,让其互不影响。
- 如果 Mode 中没有 Item,则 RunLoop 会直接退出,不进入循环。
Cocoa 预定义的 RunLoop Mode
- Default:
- NSDefaultRunLoopMode、kCFRunLoopDefaultMode。
- 默认设置,一般情况下使用。
- Event Tracking:
- NSEventTrackingRunLoopMode、UITrackingRunLoopMode。
- 用于处理拖拽、用户交互事件。
- Connection:
- NSConnectionReplyMode。
- 用于处理 NSConnection 相关事件,开发者一般用不到。
- Modal(Only macOS):
- NSModalPanelRunLoopMode。
- 用于处理 modal panels 事件。
- Common:
- NSRunLoopCommonModes、kCFRunLoopCommonModes。
- 模式集合,包括 Default、Event Tracking、Modal(Only macOS),几乎可以处理所有事件。
- 并不是一个具体的 Mode,相当于将第二条中提到的模式分别标记为 “Common”。
iOS 中公开提供的 Mode 有 Default 和 Event Tracking,可以通过 Mode Name 来操作对应的 Mode。
除以上列出的 Mode 外系统框架还自定义了很多 Mode,例如 GSEventReceiveRunLoopMode 处理系统事件,UIInitializationRunLoopMode 在 App 启动过程中使用。
Common Modes 补充
- 一个 Mode 可以将自己标记为 “Common”,通过将其 Mode Name 添加到 RunLoop 的 _commonModes中。
- 每当 RunLoop 的内容发生变化时,会自动将 _commonModeItems 里的 Source/Timer/Observer 同步到标记为 “Common” 的 Mode 里。
iOS 主线程的 RunLoop 里有两个预置的 Mode(Default 和 Event Tracking),且都已被标记为 “Common”。
Default 是 App 平时所处的 Mode,Event Tracking 是追踪 ScrollView 滑动时的 Mode。
- 应用场景举例:
- 问题:以 scheduledTimerWithTimeInterval 方式触发的 Timer(在主线程中),在滑动列表时为什么会暂停?
- 原因:滑动列表时,主线程 RunLoop 的 Mode 由 Default 切换到了 Event Tracking,Timer 默认运行在 Default 中,Default 被退出后 Timer 自然就停止工作了。
- 解决方法:
- 将 Timer 分别加入 Default 和 Event Tracking 两种 Mode 中。
- 将 Timer 加入 _commonModeItems 中,_commonModeItems 会被自动同步到所有标记为 “Common” 的 Mode 中。
- 将 Timer 放到子线程中,开启子线程的 RunLoop,可以保证与主线程互不干扰。
将 Timer 加入 _commonModeItems 中的方法:
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes);
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
RunLoop Mode 相关的 API
-
CFRunLoop 对外暴露的管理 Mode 的接口有两个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName); CFRunLoopRunInMode(CFStringRef modeName, ...);
-
Mode 暴露的管理 Mode Item 的接口有下面几个:
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 来操作 RunLoop 内部的 Mode。
当传入一个新的 Mode Name 时,RunLoop会自动创建对应的 CFRunLoopModeRef。
RunLoop 内部的 Mode 只能增加不能删除。
CFRunLoopSourceRef
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
CFRunLoopSourceRef 是事件产生的地方。Source 有两个版本:Source0 和 Source1。
- Source0:
- 只包含了一个回调(函数指针),它并不能主动触发事件。
- 使用时,需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理。
- 然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
- Source1:
- 包含了一个 mach_port 和一个回调(函数指针)。
- 被用于通过内核和其他线程相互发送消息。
- 这种 Source 能主动唤醒 RunLoop 对应的线程。
CFRunLoopTimerRef
typedef struct __CFRunLoopTimer * CFRunLoopTimerRef;
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是 toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行回调。
CFRunLoopObserverRef
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
/* Run Loop Observer Activities */
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
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
RunLoop 的运行逻辑
- 通知 Observers:即将进入 Loop
- 通知 Observers:即将处理 Timers
- 通知 Observers:即将处理 Sources0
- 执行 Blocks
- 处理 Sources0
- 执行 Blocks
- 如果存在 Sources1,跳转到第 11 步,goto handle_msg
- 通知 Observers,即将休眠
- 进入休眠,等待消息唤醒
- 通知 observers,结束休眠
- handle_msg: 处理接收到的消息
- 处理 Timer
- 处理 Dispatch 到 Main Queue 的 Block
- 处理 Sources1
- 执行 Blocks
- 根据前面的执行结果,决定如何操作
- 回到第 2 步
- 退出 Loop
- 进入 Loop 时通过参数指定了处理完事件就返回
- 超出进入 Loop 时传入参数标记的超时时间
- 被外部调用者强制停止
- Mode 中没有 Source/Timer/Observer
- 通知 observers:退出 Loop
- sources0
- 处理触摸事件
- performSelector:onThread:
- sources1
- 基于 Port 的线程间通信
- 捕捉系统事件
- timers
- NSTimer
- performSelector:withObject:afterDelay:
- observers
- 用于监听 RunLoop 的状态
- UI 刷新(BeforeWaiting)
- AutoreleasePool(BeforeWaiting)
RunLoop 休眠的实现原理
参考资料
官方 RunLoop 文档
ibireme - 深入理解RunLoop
reallychao - iOS刨根问底-深入理解RunLoop
MJ - iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化