【OC源码】Runloop | 二. Runloop 的数据结构

二. runloop 的数据结构

参考文章:
深入理解RunLoop | Garan no dou
Run Loop 记录与源码注释

我对 runloop 源码的解析:
数据结构源码解析
CFRunloopRun 源码解析
一些其他函数解析

所使用的源码版本:
CF-1151.16.zip

结合前面提到一.runloop 是什么 Runloop 中所具有的几个元素, 这里来谈谈数据结构
对于具体代码的阅读见 数据结构源码解析
这篇文章聊聊从源码中得出的结论


我们前文中得出了几个元素:

  • Thread
  • Runloop
  • Mode
  • Item(Source,Timer,Observer)

其中的 Thread 在 CF 源码中调用的是 pthread_t 不做过多深入
剩余的元素, 关系如图:(省略了部分字段的)

ThreadRunloop

CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);  
  • 全局的 Runloop 被一个 CFMutableDictionaryRef 类型的字典存放起来
  • 该字典以 Thread 的 id 作为 key, 以 Runloop 作为 value
  • RunloopThread 为一对一的关系
  • 对外的创建方式只有获取, 而不能手动创建 CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)
    _CFRunLoopGet0

RunloopMode

  • RunloopCFMutableSetRef 集合的方式, 存放了很多 modes
  • 包含三个字段 _modes, _currentMode, _commonModes
  • commonMode 在新增 ItemRunloop执 行过程都会被多次访问. 对于在 _commonModes 中的 modes, 会在合适的时机,将 _commonModeItems 的内容都同步进去
// CFRunLoopAddSource  
if (modeName == kCFRunLoopCommonModes) {  
		CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;  
		CFSetAddValue(rl->_commonModeItems, rls);  
}  
// CFRunLoopAddTimer	  
if (modeName == kCFRunLoopCommonModes) {  
    CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;  
    CFSetAddValue(rl->_commonModeItems, rlt);  
}  
  
// CFRunLoopAddObserver  
if (modeName == kCFRunLoopCommonModes) {  
    CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;  
    CFSetAddValue(rl->_commonModeItems, rlo);  
}  

Mode

  • 以集合 CFMutableSetRef 存放 Source
  • Source 又被分为 source0, source1
  • ObservesTimer 均以数组存放
  • ❓其构造函数调用的, 暂时还没找到外部手动创建对应的逻辑
    (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL);  
    
  • runloop 执行各种 item (CFRunLoopDo)的过程中, 都会先对当前 mode 及 item 所属 mode 进行判断, 只有相符的 item 才会执行
    其他: 一些其他函数解析(CFRunLoopDoObservers__CFRunLoopDoBlocks__CFRunLoopDoSources0)
  • 如果要切换 mode, 得先退出 loop 再重新进入, 单纯的切换 mode 没有自动停止功能.
    • 执行过程中, 如果 mode 中没有内容, 也会直接退出
      // CFRunLoopRunSpecific  
       if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {  
        Boolean did = false;  
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;  
      }  
      

CFRunloop 对外提供 Mode 接口用于处理:(可以看到,没有删除):

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

Item

Item 包含三个东西Source,Timer,Observer, 都是在 runloop 循环过程中会触发的实体.
其函数名很统一前缀都是__CFRunLoopDo, 在 __CFRunLoopRun 运行过程中,分别会调用:

  • __CFRunLoopDoObservers
  • __CFRunLoopDoSources0
  • __CFRunLoopDoTimers

其他: 一些其他函数解析(CFRunLoopDoObservers__CFRunLoopDoBlocks__CFRunLoopDoSources0)
其内部实现也很类似:

  • 先从当前 mode 取出需要的 item
  • 核心都是调用 __CFRUNLOOP_IS_CALLING_OUT_TO 的函数, 这些函数最终都是拿到入参的函数指针, 然后执行
    • DoObservers: __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
    • DoSources0 : __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
    • DoTimers: __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
  • 内部都会先判断是否是集合(因为其指针可能是单个元素),是的话执行遍历,不是的话,当做单个元素处理:
    • DoSources0 : CFGetTypeID(sources) == CFRunLoopSourceGetTypeID()

总结来说: item 是带着函数指针的集合, 这个集合的函数指针, runloop 执行过程中会从当前 mode 里拿出来, 然后执行对应函数
ps.其实还有个block, 不是这儿的重点, 略

对于这些 Item, 由 Mode 暴露接口提供添加方法:

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);  

Source

  • source0, source1 两个 Source 集合, 对应前文的Custom Input Sources, Port-Based Sources
  • 内部是两个不同字段的联合体(union), 也就是 2 选 1,都是在同一块内存空间
    • 其中 source0 中的内容不会自动唤起线程, 需要手动调用唤醒
      • performselector 是 iOS 为用户实现的系列函数, 也是 source0,
    • source 1 由系统内核触发, 会自动唤起线程
  • 执行函数: source0 的具体过程在这里做分析__CFRunLoopDoSources0), 内部校验等结束后调用 CFRUNLOOP_IS_CALLING_OUT 前缀的函数
    (__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__, 其内部最终执行一个函数)

  • 如何加入自定义 source 主要参考官方文档 Configuring Run Loop Sources

Timer

对应的上层就是 NSTimer, 这也是为什么 NSTimer 运行必须依赖 runloop 的原因
据一些文章介绍 NSTimerCFRunLoopTimerRef 可以相互转换

执行函数: 在 __CFRunLoopDoTimer, 内部核心也是一个CFRUNLOOP_IS_CALLING_OUT
(__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__, 其内部最终执行一个函数)

Timer 依赖 Runloop 的特性导致其不准时, 其运行与否基于 runloop 的状态

ps.我们常见的 NSTimer 在滑动时失效问题, 是因为 NSTimer 和滑动的 Mode 不是同一个

Observer

我们看执行函数 其他: CFRunloopRun 源码解析
会发现 Observer 的触发非常简单,状态都是直接写在代码里的,在 run 函数中, 特定位置的某一行, 加入一个判断, 然后调用执行函数

if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);  
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);  

Observer 可以监听的类型:

typedef unsigned long CFOptionFlags;  
  
/* Run Loop Observer Activities */  
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {  
    kCFRunLoopEntry = (1UL << 0),  
    kCFRunLoopBeforeTimers = (1UL << 1),  
    kCFRunLoopBeforeSources = (1UL << 2),  
    kCFRunLoopBeforeWaiting = (1UL << 5),  
    kCFRunLoopAfterWaiting = (1UL << 6),  
    kCFRunLoopExit = (1UL << 7),  
    kCFRunLoopAllActivities = 0x0FFFFFFFU  
};  

执行函数: CFRunLoopDoObservers, 内部校验等结束后调用 CFRUNLOOP_IS_CALLING_OUT 前缀的函数
(__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__, 其内部最终执行一个函数)