RunLoop 源码探索

RunLoop

文章目录

在正式了解之前,我们先了解一下 CF(CoreFoundation)对象这个概念,Core Foundation 是 Apple 的底层 C 语言框架,提供字符串,数组,字典,RunLoop,Timer 等

这些对象通常使用这种类型 CFRunLoopRef ,CFRunLoopSourceRef ,CFRunLoopTimerRef...

本文提到的 CFRunLoopRef runLoop; 它指向 CoreFoundation 内部对象 struct __CFRunLoop

objc 复制代码
// OC 对象
NSObject *object = [[NSObject alloc] init];
// CF 对象
// Create / Copy / Retain 最终需要 CFRelease
// Get 通常是非拥有引用,不需要释放
CFRunLoopSourceRef source = CFRunLoopSourceCreate(...);
CFRelease(source);

ARC 主要管理 Objective-C 对象,不会自动管理所有 CoreFoundation 对象,CoreFoundation 遵循 Create/Copy 规则

如果你想把 CF 对象的所有权交给 ARC,可以使用 id object = CFBridgingRelease(source);

反过来,如果只是把 Objective-C 对象传给 CF API,则使用 CFTypeRef cfObject = (__bridge CFTypeRef)object;

RunLoop 前言

在 iOS 开发中,我们经常会接触到主线程、定时器、触摸事件、网络回调和界面刷新等内容。它们看起来来自不同的系统模块,但最终都需要依靠线程来执行;线程执行完任务后通常就会结束,那么主线程为什么能够一直响应用户操作?后台线程又如何在没有任务时保持等待,并在有任务时及时处理呢?

这就要依靠 RunLoop

RunLoop 可以理解为运行在线程上的一套事件循环机制,它会不断等待事件,有事件到来时就唤醒线程进行处理,没有事件时就让线程进入休眠,从而避免线程持续占用 CPU;用户的触摸、定时器触发、端口消息以及其他线程投递过来的任务,都可以成为 RunLoop 等待和处理的事件

RunLoop 并不是线程本身,而是线程上的一个事件管理器;一个线程可以拥有自己的 RunLoop,RunLoop 中又可以包含多个 Mode;每个 Mode 负责管理一组 Source、Timer 和 Observer,决定当前线程在某种场景下应该处理哪些事件

公开 API 通常只暴露 CFRunLoopRef ,外部代码不会直接访问结构体成员;而在 CoreFoundation 内部,CFRunLoopRef 实际指向一个 struct __CFRunLoop

__CFRunLoop 是 CoreFoundation 内部真正表示 RunLoop 对象的结构体,它的结构如下,源码看这里:

objc 复制代码
struct __CFRunLoop {
    CFRuntimeBase _base;            // 对象的基础信息,包含类型、引用计数等运行时数据
    pthread_mutex_t _lock;          // 保护 RunLoop 内部 Mode 列表的互斥锁
    __CFPort _wakeUpPort;           // 用于唤醒休眠中的 RunLoop
    Boolean _unused;                // 当前版本中预留或未使用的字段
    volatile _per_run_data *_perRunData; // reset for runs of the run loop
    pthread_t _pthread;                  // RunLoop 所属的线程
    uint32_t _winthread;                 // Windows 平台对应的线程标识
    CFMutableSetRef _commonModes;        // Common Modes 的名字集合
    CFMutableSetRef _commonModeItems;    // 被加入 Common Modes 的 Timer、Source、Observer 等对象
    CFRunLoopModeRef _currentMode;       // 当前正在运行的 Mode
    CFMutableSetRef _modes;              // RunLoop 管理的所有 Mode
    struct _block_item *_blocks_head;    // 待执行 Block 链表的头部
    struct _block_item *_blocks_tail;    // 待执行 Block 链表的尾部
    CFAbsoluteTime _runTime;             // RunLoop 开始运行的时间
    CFAbsoluteTime _sleepTime;           // RunLoop 进入休眠的时间
    CFTypeRef _counterpart;              // 与其他平台或对象体系关联的内部对象
};

CFRunLoopMode

RunLoop 中可能有很多事件,比如定时器 A,定时器 B,Source0,Source1,Block...但一次运行时,RunLoop 不会处理全部事件,而是只处理当前 Mode 中的事件,所以 Mode 可以理解为 RunLoop 的一个工作场景或事件过滤器

CFRunLoop 管理多个 CFRunLoopMode,而每个 CFRunLoopMode 管理当前模式下的事件

源码中,Mode 的结构是

objc 复制代码
struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

所以,Mode 不是一个简单的字符串,它是一个真正保存事件集合的对象

一个 Mode 主要包含四类东西

objc 复制代码
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
  1. _sources0

Source0 是应用主动触发型事件源,它不会自己从休眠中唤醒 RunLoop,通常需要额外调用

复制代码
CFRunLoopWakeUp(runLoop);
  1. _sources1

Source1 一般与底层端口、Mach 消息、系统事件有关,它能够通过底层端口事件唤醒 RunLoop 处理 Source1

  1. _observers

Observer 不负责产生事件,而是观察 RunLoop 运行到哪个阶段

  1. _timers

Timer 是时间驱动的事件,Timer 不会单独运行,也不会自己创建线程,它只是被放入某个 Mode 的 _timers 数组中,由 RunLoop 在循环过程中检查是否到期

在 __CFRunLoop 结构里有几种不同的 "Mode"

  1. _currentMode 指一个具体的 Mode 对象,它本身拥有 _sources0,_sources1,_observers,_timers

    objc 复制代码
    // RunLoop 一次只能运行一个具体 Mode
    // CFRunLoopRunSpecific 中会先根据 Mode 名称查找 Mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    // 然后把它设置成当前 Mode
    rl->_currentMode = currentMode;
  2. _modes 是 RunLoop 保存的所有 Mode,是所有 Mode 的总目录,源码中,RunLoop 创建新 Mode 后会把它加入 _modes:CFSetAddValue(rl->_modes, rlm);

  3. commonModes 更像一个 Mode 分组,它本身不是一个真正执行事件的 Mode,而是一个集合,里面保存多个具体 Mode 的名字,所以它的类型是 CFMutableSetRef ,而不是 CFRunLoopModeRef

    RunLoop 创建时,默认会把 kCFRunLoopDefaultMode 加入这个集合

    c 复制代码
    loop->_commonModes = CFSetCreateMutable(...);
    CFSetAddValue(
        loop->_commonModes,
        kCFRunLoopDefaultMode
    );
    // 因此初始关系大致是 commonModes = { DefaultMode }
    // 后来系统可能再把某个 Mode 加入 Common Modes

    Common Modes 相当于批量注册机制,能避免手动注册导致重复注册的问题,未来新增 Common Mode 时自动同步,也就是说后续如果 CFRunLoopAddCommonMode(runLoop, CustomMode); CoreFoundation 就会把 CustomMode 加入 _commonModes,然后把已有的 _commonModeItems 全部复制到这个新 Mode

  4. commonModeItems 保存的是被标记为"共同项目"的对象,Timer,Source ,Observer 这些

例如当把 Timer 加入 Common Modes 时

objc 复制代码
// 源码会先把 Timer 放入 _commonModeItems
CFSetAddValue(rl->_commonModeItems, rlt);
// 然后遍历 _commonModes,把 Timer 实际加入每一个具体 Mode:
CFSetApplyFunction(
    set,
    (__CFRunLoopAddItemToCommonModes),
    context
);
  • 一个 RunLoop 为什么要管理多个 Mode,因为同一个线程在不同场景下,需要处理不同类型的事件

如果所有事件都放在同一个列表里,滚动时就无法优先处理触摸和滚动事件,也无法暂时忽略某些普通事件

Mode 的作用就是隔离事件,当主线程进入 TrackingMode 时 DefaultMode 中的 Timer 可能暂时不执行

TrackingMode 中的事件继续执行

  • RunLoop 不会因为 Mode 对象存在就一直运行

它会检查当前 Mode 中是否有可处理内容

objc 复制代码
if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) {
    return false;
}
if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) {
    return false;
}
if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) {
    return false;
}

如果都没有,CFRunLoopRunSpecific 可能直接返回:

objc 复制代码
if (NULL == currentMode ||  __CFRunLoopModeIsEmpty(...)) {
    return kCFRunLoopRunFinished;
}
  • 为什么一次只能运行一个 Mode
objc 复制代码
static int32_t __CFRunLoopRun(
    CFRunLoopRef rl,     
    CFRunLoopModeRef rlm,   // 它表示这一次循环具体处理哪个 Mode
    CFTimeInterval seconds,
    Boolean stopAfterHandle,
    CFRunLoopModeRef previousMode
) {
  ...
}

RunLoop 内部不是把所有 Mode 的事件混在一起处理,而是选择一个 Mode,只检查这个 Mode 的 Source、Timer、Observer进入等待或处理事件,本轮循环结束

一次 __CFRunLoopRun 调用只接收一个 CFRunLoopModeRef,如果要运行另一个 Mode,不能在当前循环中简单地把 rlm 指针改成另一个对象,而是需要结束当前 Mode 的本轮运行,设置另一个 Mode 为 currentMode,重新进入 RunLoop

苹果文档中提到的 Mode 有五个,分别是

CFRunLoopSource

objc 复制代码
struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;  // 用于标记 Signaled 状态,source0 只有在被标记为 Signaled 状态才会被处理
    pthread_mutex_t _lock;
    CFIndex _order;
    CFMutableBagRef _runLoops;

    union {
        CFRunLoopSourceContext version0;  // Source0 主动触发型
        CFRunLoopSourceContext1 version1;  // Source1 底层端口/消息驱动型 
    } _context;
};
  1. Source0 是由代码主动触发
objc 复制代码
// source0 的结构体
typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*perform)(void *info);  // Source0 没有 getPort,因此它本身不知道如何监听内核端口
} CFRunLoopSourceContext;

Source0 通常没有自己的底层端口,它的流程是

任务准备好 ---> 将 Source0 标记为 signaled CFRunLoopSourceSignal(source); ---> 唤醒 RunLoop CFRunLoopWakeUp(runLoop); ---> RunLoop 检查 Source0 ---> 调用 perform 回调

objc 复制代码
// RunLoop 会先收集当前 Mode 的 Source0
CFSetApplyFunction( rlm->_sources0, (__CFRunLoopCollectSources0), &sources);
// 检查 Source 是否被标记为 signaled
if (__CFRunLoopSourceIsSignaled(rls)) {
    __CFRunLoopSourceUnsetSignaled(rls);

    __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(
        rls->_context.version0.perform,
        rls->_context.version0.info
    );
}
  1. Source1 由底层端口消息触发

Source1 通常和 Mach port 等底层端口关联

objc 复制代码
// Source1 多了两个关键函数
mach_port_t (*getPort)(void *info);   // 告诉 RunLoop 监听哪个端口,可以接收内核消息并触发回调
// 收到消息后如何处理
void *(*perform)( void *msg, 
                 CFIndex size, 
                 CFAllocatorRef allocator, 
                 void *info
);

RunLoop 休眠时会监听 _portSet,如果Mach port 收到消息,RunLoop 被 mach_msg 唤醒,根据 livePort 查找 Source1,然后调用 version1.perform

触摸事件的底层输入通常通过某种 port/source1 机制进入进程,UIKit 可能先把它们放入自己的事件队列,

再通过 Source0 在主线程上统一处理

  • 如果你希望线程一直存在,就可以通过让线程函数持续阻塞在 RunLoop 中的方法,但是不是调用了 RunLoop,线程就永远不会结束,而是必须让当前 Mode 中存在有效的 Source、Timer 或其他可调度对象

例如让线程拥有一个 RunLoop Source,Source 没有任务时,RunLoop 休眠,有任务时,Signal + WakeUp

或许你想问 Source 的任务处理完了,为什么 RunLoop 不马上结束?

因为 Source 处理完,不等于 Source 被删除,Source0 被处理时,RunLoop 会 __CFRunLoopSourceUnsetSignaled(rls);,这只是清除 Source 当前有任务的标记,但 Source 仍然留在,RunLoop 就仍然有理由继续运行,只是进入休眠等待下一次事件

只有执行 CFRunLoopRemoveSource 或者 CFRunLoopSourceInvalidate(source); 使 Source 从 Mode 中移除,并且当前 Mode 中没有其他可处理对象时,RunLoop 才可能退出

CFRunLoopObserver

Observer 是来观察 RunLoop 当前运行到了哪个阶段

objc 复制代码
struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;   // 保护 Observer 内部状态

    CFRunLoopRef _runLoop;   // Observer 当前关联的 RunLoop
    CFIndex _rlCount;  // 当前关联的 RunLoop 数量

    CFOptionFlags _activities;   // 关心哪些 RunLoop 阶段
    CFIndex _order;

    CFRunLoopObserverCallBack _callout;     // 触发时执行的回调函数
    CFRunLoopObserverContext _context;   // 传给回调的上下文数据
};

CoreFoundation 定义了这些 Activity

objc 复制代码
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry        = (1UL << 0),   // 即将进入一次 RunLoop 运行
    kCFRunLoopBeforeTimers = (1UL << 1),     // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2),  // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5),  // RunLoop 当前这一轮暂时没有要立刻处理的事情,即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6),  // RunLoop 从休眠中醒来,准备继续处理事件
    kCFRunLoopExit          = (1UL << 7),  // RunLoop 即将退出当前这次运行
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Observer 的回调类型是

objc 复制代码
typedef void (*CFRunLoopObserverCallBack)(
    CFRunLoopObserverRef observer,
    CFRunLoopActivity activity,
    void *info
);
// 因此我们可以这样写回调函数
void observerCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (activity == kCFRunLoopBeforeWaiting) {
        NSLog(@"RunLoop 即将休眠");
    }
}

_activities 本质上是一个位掩码

RunLoop 判断某个 Observer 是否关心当前阶段

objc 复制代码
if (0 != (rlo->_activities & activity))

CFRunLoopTimer

到达指定时间后,由 RunLoop 在当前线程、当前 Mode 中调用一次回调,它不是线程,也不会自己执行;它必须先被加入某个 RunLoop 的某个 Mode

objc 复制代码
struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;

    CFRunLoopRef _runLoop;    // Timer 所属的 RunLoop
    CFMutableSetRef _rlModes;   // Timer 被加入了哪些 Mode

    CFAbsoluteTime _nextFireDate;  // 下一次触发时间
    CFTimeInterval _interval;    // 重复间隔
    CFTimeInterval _tolerance;   // 允许延迟的时间范围

    uint64_t _fireTSR;         // 内部高精度时间表示
    CFIndex _order;           // 同一时间多个 Timer 的执行顺序

    CFRunLoopTimerCallBack _callout;   // Timer 触发时执行的回调
    CFRunLoopTimerContext _context;
};

注意创建 Timer 不等于 Timer 会运行,它还没有被 RunLoop 管理,必须进行添加 CFRunLoopAddTimer

RunLoop 如何知道 Timer 什么时候到期

RunLoop 不会每隔一小段时间不断轮询,这样会浪费 CPU,它会先查看当前 Mode 中最早到期的 Timer,这个函数会遍历 Timer 列表,计算 nextSoftDeadline 和 nextHardDeadline

Timer 不会创建一个新线程,比如 [NSTimer scheduledTimerWithTimeInterval:...] 只是把 Timer 加入当前线程的 RunLoop,Timer 回调在哪个线程执行,取决于它被加入哪个 RunLoop

如果线程结束,RunLoop 也不存在了,Timer 就不会继续触发

RunLoop 实现

获取 RunLoop

公开 API 主要有

  • CFRunLoopGetCurrent(void);
  • CFRunLoopGetMain(void);

通过 Objective-C 封装的有

  • +[NSRunLoop currentRunLoop];
  • +[NSRunLoop mainRunLoop];

我们来看一下这些函数是怎么实现的

  1. CFRunLoopGetCurrent
objc 复制代码
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) {
        return rl;
    }
  // 获取当前线程的 RunLoop
    return _CFRunLoopGet0(pthread_self());
}

先从当前线程的线程本地存储中查找,如果已经有了直接返回,如果没有根据当前线程 pthread_self() 创建或查找

因此 [NSThread currentThread] 和 CFRunLoopGetCurrent() 之间存在绑定关系

  1. CFRunLoopGetMain
objc 复制代码
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; 
  // 获取主线程的 RunLoop
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np());
    return __main;
}

不管在哪个线程调用,返回的都是主线程 RunLoop

你会发现这两个函数中真正负责找 RunLoop 的函数是 _CFRunLoopGet0() 感兴趣的同学可以自行查找源码阅读

RunLoop 什么时候创建

RunLoop 通常是懒创建的

主线程 RunLoop:通常在第一次访问或系统初始化过程中创建

子线程 RunLoop:第一次调用 CFRunLoopGetCurrent 时创建

CoreFoundation 内部维护一个类似这样的全局映射,线程 pthread <----> CFRunLoop

创建 Mode

没有公开的 CFRunLoopModeCreate(...) API

开发者一般通过添加 Mode Item 来间接创建 Mode

CFRunLoopAddSource(runLoop, source, customMode); 该函数内部会调用 __CFRunLoopFindMode(rl, modeName, true);

参数 true 表示如果找不到,就创建这个 Mode

如果只是调用 CFRunLoopAddCommonMode(runLoop, customMode); 它首先把名字加入 _commonModes;

只有在需要把 Common Mode Items 同步进去时,才可能通过 __CFRunLoopFindMode(..., true) 创建实际的 Mode 对象

添加 Source

我们可以通过以下接口添加/移除各种事件:

  • void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
  • void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
  • void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
  • void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef * mode)
  • void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
  • void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)

RunLoop 运行

我们不能直接调用私有的 __CFRunLoopRun() 来实现 RunLoop,它是 CoreFoundation 内部函数;实际开发中,要通过公开 API 创建 Source、Timer、Observer,然后调用 CFRunLoopRun() 或 CFRunLoopRunInMode()

CFRunLoopRun

默认在 kCFRunLoopDefaultMode 中运行当前线程的 RunLoop

objc 复制代码
do {
  // 传入当前线程,以及 kCFRunLoopDefaultMode
    result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false
    );
} while (
    result != kCFRunLoopRunStopped &&
    result != kCFRunLoopRunFinished
);

CFRunLoopRunInMode

让当前线程的 RunLoop 在指定 Mode 中运行指定时间

比如CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10.0, false); 表示在 DefaultMode 中运行最多 10 秒

处理完 Source 后不要立即返回

CFRunLoopRunSpecific

以上两个公开运行 API 的共同底层入口就是 CFRunLoopRunSpecific(rl, modeName, seconds, returnAfterSourceHandled);

其中 RunLoop有四种常见的返回值

  • kCFRunLoopRunFinished Mode 为空或运行完成
  • kCFRunLoopRunStopped 被 CFRunLoopStop 停止
  • kCFRunLoopRunTimedOut 超过指定运行时间
  • kCFRunLoopRunHandledSource 处理 Source 后返回
c 复制代码
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
  // 检查进程是否发生 fork
  // 检查 RunLoop 是否正在销毁
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
  // 加锁并查找 Mode
  // 这里的 findMode 函数传入 false 表示只查找不创建
    __CFRunLoopLock(rl);
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
  // 检查 Mode 是否为空 ,为空或不存在直接返回 Finished
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
	Boolean did = false;
	if (currentMode) __CFRunLoopModeUnlock(currentMode);
	__CFRunLoopUnlock(rl);
	return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
  // 保存上一次的运行状态
  // previousPerRun 本次运行专用的临时状态
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
  // previousMode 保存进入当前 Mode 前的旧 Mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
  // currentMode 当前 Mode
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;
   // 通知 Entry Observer
  // 注意这里遍历的是 currentMode->_observers,不是 Source
  // 因为此时 RunLoop 还没有进入内部事件循环,只是在通知
	if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
  // 通知 observer 已退出runloop
	if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
	rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

该函数相当于 RunLoop 运行的前置工作,运行的核心函数是 __CFRunLoopRun

__CFRunLoopRun

c 复制代码
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    uint64_t startTSR = mach_absolute_time();
// 检查 Stop 状态,如果 RunLoop 或者 mode 是 stop 状态,则直接 return,不进入循环
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
	return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
	rlm->_stopped = false;
	return kCFRunLoopRunStopped;
    }
    
  // 配置主队列的 dispatch port
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
  // 判断是否为主线程
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
  // 如果在主线程并且 runloop 是主线程的 runloop 并且该 mode 是 commonMode,则给 mach 端口赋值为主线程收发消息的端口
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    if (rlm->_queue) {
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }
#endif
    // 配置 RunLoop 超时,CFRunLoopRunSpecific 支持 seconds,因此 __CFRunLoopRun 需要知道什么时候超时
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    } else if (seconds <= TIMER_INTERVAL_LIMIT) {
	dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
	timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
	timeout_context->ds = timeout_timer;
	timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
	timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
	dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
	dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    } else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }

  // 进入内部 do-while 循环
    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;
        voucher_t voucherCopy = NULL;
#endif
      // 每轮开始准备接收消息
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
      // 收到的底层 Mach 消息
        mach_msg_header_t *msg = NULL;
      //  哪个 port 触发了本轮唤醒
        mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
        HANDLE livePort = NULL;
        Boolean windowsMessageReceived = false;
#endif
	__CFPortSet waitSet = rlm->_portSet;

        __CFRunLoopUnsetIgnoreWakeUps(rl);
     // 通知 BeforeTimers 和 BeforeSources
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
   // 执行 RunLoop Block
	__CFRunLoopDoBlocks(rl, rlm);
  // 处理 Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks(rl, rlm);
	}
    // poll 本轮是否允许立即返回
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            msg = (mach_msg_header_t *)msg_buffer;
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg;
            }
#elif DEPLOYMENT_TARGET_WINDOWS
            if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                goto handle_msg;
            }
#endif
        }

        didDispatchPortLastTime = false;
   // BeforeWaiting 和进入休眠
	if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
	__CFRunLoopSetSleeping(rl);
	// do not do any user callouts after this point (after notifying of sleeping)

        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we
        // don't want these ports to get serviced.

        __CFPortSetInsert(dispatchPort, waitSet);
        
	__CFRunLoopModeUnlock(rlm);
	__CFRunLoopUnlock(rl);

        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        do {
            if (kCFUseCollectableAllocator) {
                // objc_clear_stack(0);
                // <rdar://problem/16393959>
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        if (kCFUseCollectableAllocator) {
            // objc_clear_stack(0);
            // <rdar://problem/16393959>
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
#endif
        
        
#elif DEPLOYMENT_TARGET_WINDOWS
        // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive Windows messages.
        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);

        rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));

        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we
        // don't want them left in there if this function returns.

        __CFPortSetRemove(dispatchPort, waitSet);
        
        __CFRunLoopSetIgnoreWakeUps(rl);

        // user callouts now OK again
	__CFRunLoopUnsetSleeping(rl);
	if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

        handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);

#if DEPLOYMENT_TARGET_WINDOWS
        if (windowsMessageReceived) {
            // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
            __CFRunLoopModeUnlock(rlm);
	    __CFRunLoopUnlock(rl);

            if (rlm->_msgPump) {
                rlm->_msgPump();
            } else {
                MSG msg;
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
            }
            
            __CFRunLoopLock(rl);
	    __CFRunLoopModeLock(rlm);
 	    sourceHandledThisLoop = true;
            
            // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
            // Use 0 for the mask so Windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
            // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
            __CFRunLoopSetSleeping(rl);
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);            
            __CFRunLoopUnsetSleeping(rl);
            // If we have a new live port then it will be handled below as normal
        }
        
        
#endif
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set, e.g. it fired early. If no timer is handled, re-arm it.
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
            void *msg = 0;
#endif
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            
            // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.
            voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);

            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
		mach_msg_header_t *reply = NULL;
		sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
		if (NULL != reply) {
		    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
		    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
		}
#elif DEPLOYMENT_TARGET_WINDOWS
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
	    }
            
            // Restore the previous voucher
            _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
            
        } 
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
        
	__CFRunLoopDoBlocks(rl, rlm);
        

	if (sourceHandledThisLoop && stopAfterHandle) {
	    retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
	} else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
	    retVal = kCFRunLoopRunStopped;
	} else if (rlm->_stopped) {
	    rlm->_stopped = false;
	    retVal = kCFRunLoopRunStopped;
	} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
	    retVal = kCFRunLoopRunFinished;
	}
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);
#endif

    } while (0 == retVal);

    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }

    return retVal;
}
相关推荐
茶底世界之下6 小时前
叶片纹理要多硬?用 Awaking 只加一点清晰度
ios
2501_915106328 小时前
KXApp 的 VS Code 真机调试:按下 Cmd+R 之后发生了什么
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
ii_best9 小时前
安卓手机开发、iOS 手机开发都能用:按键精灵远程调试,设备在千里外也能改脚本
android·ios·智能手机·自动化·按键精灵
开心猴爷1 天前
应对iOS审核4.3:可以使用 IpaGuard 来给属性名混淆的技术细节与平衡之道
后端·ios
iOS开发上架哦1 天前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
后端·ios
_瑞1 天前
APM_KSCrash 原理
ios
茶底世界之下1 天前
窗光静物一定要更亮吗?用 Awaking 把曝光收回一点
ios
ii_best1 天前
ios手机开发安卓脚本开发软件[按键精灵教程]从「找最小值」到「Boss 最近刷新」再到「屏幕范围极值」
android·ios·智能手机·自动化·按键精灵
茶底世界之下2 天前
林间步道要不要加暗角?用 Awaking 把强度收在20%
前端·ios·图片资源