RunLoop 01 - 原理

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]
  • CFRunLoopRef(源码)
    • 获取当前线程的 RunLoop:CFRunLoopGetCurrent()
    • 获取主线程的 RunLoop:CFRunLoopGetMain()

CFRunLoopRef 在 CoreFoundation 框架内,提供了一套纯 C 函数的 API,所有 API 都是线程安全的。
NSRunLoop 在 Cocoa 框架内,是对 CFRunLoopRef 的封装,提供了一套面向对象的 API,但是这些 API 不是线程安全的。

RunLoop 与线程

  • 线程与 RunLoop 之间是一一对应的,对应关系保存在一个全局的 Dictionary 中,key 为线程,value 为 RunLoop。
  • 线程刚创建时没有 RunLoop,第一次获取时会创建,iOS 程序启动后会默认为主线程创建 RunLoop。
  • RunLoop 的销毁发生在线程结束时。
  • 只能在线程内部获取其 RunLoop(主线程除外)。

CoreFoundation 中 RunLoop 相关的类

  1. CFRunLoopRef
  2. CFRunLoopModeRef(没有对外暴露,通过 CFRunLoopRef 的接口进行了封装)
  3. CFRunLoopSourceRef
  4. CFRunLoopTimerRef
  5. CFRunLoopObserverRef

它们之间的关系如下:


RunLoop 01 - 原理_第1张图片

CFRunLoopRef & CFRunLoopModeRef 结构

typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
    pthread_t _pthread;               // RunLoop 对应的线程
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set
    CFRunLoopModeRef _currentMode;    // Current RunLoop Mode
    CFMutableSetRef _modes;           // Set
};
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name,例如 "kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
};
  • 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 启动过程中使用。

更多系统框架 Mode 可以查看这里

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 自然就停止工作了。
    • 解决方法:
      1. 将 Timer 分别加入 Default 和 Event Tracking 两种 Mode 中。
      2. 将 Timer 加入 _commonModeItems 中,_commonModeItems 会被自动同步到所有标记为 “Common” 的 Mode 中。
      3. 将 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 对应的线程。
RunLoop 01 - 原理_第2张图片

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 的运行逻辑

  1. 通知 Observers:即将进入 Loop
  2. 通知 Observers:即将处理 Timers
  3. 通知 Observers:即将处理 Sources0
  4. 执行 Blocks
  5. 处理 Sources0
  6. 执行 Blocks
  7. 如果存在 Sources1,跳转到第 11 步,goto handle_msg
  8. 通知 Observers,即将休眠
  9. 进入休眠,等待消息唤醒
  10. 通知 observers,结束休眠
  11. handle_msg: 处理接收到的消息
    • 处理 Timer
    • 处理 Dispatch 到 Main Queue 的 Block
    • 处理 Sources1
  12. 执行 Blocks
  13. 根据前面的执行结果,决定如何操作
    • 回到第 2 步
    • 退出 Loop
      • 进入 Loop 时通过参数指定了处理完事件就返回
      • 超出进入 Loop 时传入参数标记的超时时间
      • 被外部调用者强制停止
      • Mode 中没有 Source/Timer/Observer
  14. 通知 observers:退出 Loop
RunLoop 01 - 原理_第3张图片
  • sources0
    • 处理触摸事件
    • performSelector:onThread:
  • sources1
    • 基于 Port 的线程间通信
    • 捕捉系统事件
  • timers
    • NSTimer
    • performSelector:withObject:afterDelay:
  • observers
    • 用于监听 RunLoop 的状态
    • UI 刷新(BeforeWaiting)
    • AutoreleasePool(BeforeWaiting)

RunLoop 休眠的实现原理

RunLoop 01 - 原理_第4张图片
15662181178751.jpg

参考资料

官方 RunLoop 文档
ibireme - 深入理解RunLoop
reallychao - iOS刨根问底-深入理解RunLoop
MJ - iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化

你可能感兴趣的:(RunLoop 01 - 原理)