写给自己看的源码系列: runloop的底层数据结构

runloop是iOS中的基本概念,其他系统也有类似的Event Loop、事件循环,这种关键的基础通常都是基于消息机制。而实现的关键也在于如何在消息未到达的时候,让系统休眠以避免资源占用;消息到达就唤醒系统来执行相应的任务。

Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

本文对iOS中的runloop进行了深入解析,从源码角度对其进行了详细分析,并结合使用场景总结了runloop的常见使用姿势。

runloop的基本概念

runloop是通过 内部维护的事件循环,对事件和消息进行管理 的一个对象。

这里主要是操作系统两种状态的切换:用户态 <===> 内核态。无事件/消息时,休眠以避免资源占用;有事件/消息时,立刻被唤醒处理事件。

CFRunLoopRef是CoreFouncation中的C API,提供了runloop相关的相当丰富的接口,且都是线程安全的。NSRunLoop是对CFRunLoopRef的封装,提供了面向对象的API,非线程安全。

thread

runloop对象会保存在一个全局的容器中,key是线程对象,value是runloop对象。runloop与thread是一一对应的,每个thread的runloop在需要的时候会自动创建,线程中调用currentRunloop相关接口([NSRunLoop currentRunLoop])即创建并开启runloop。而主线程在App的main入口中即开启了runloop,子线程默认不会创建runloop。在线程结束的时候,其对应的runloop(如果有的话)也会销毁。

pthread(基于C的线程底层接口)与NSThread(提供面向对象的API)是对应的,是mach thread的上层封装。

Thread-Local Storage:每个线程都维护了一个存储键值对的字段,记录一些贯穿于线程执行过程中的信息,如runloop对象就存储于其中。

Mode

A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified. You must be sure to add one or more input sources, timers, or run-loop observers to any modes you create for them to be useful. You use modes to filter out events from unwanted sources during a particular pass through your run loop.

一个线程有一个runloop,一个runloop包含多个Mode,一个Mode又包含多个Source、Timer、Observer。runloop不能自行切换Mode,只能退出重新指定Mode再运行。

一个Mode可以将自己标记为Common属性(通过将其ModeName添加到runloop的_commonModes集合中即可)。每当runloop的内容变化,runloop会将_commonModeItems集合中的Source/Timer/Observer同步到具有Common属性的所有Mode中。

常见的Mode是DefaultMode和UITrackingMode。除此之外,还有

  1. UIInitializationRunLoopMode是刚启动App时的第一个Mode,启动后就不需要了
  2. GSEventReceiveRunLoopMode用于iOS系统内部,接收系统事件的Mode,由GraphicsServices调用在CFRunLoopRunSpecific前边,通常用不到。
  3. _kCFStreamBlockingOpenMode是CFStream中调度网络任务使用的私有mode,见 CFStream

Mode相关的接口有两个:CFRunLoopAddCommonMode用于添加Mode为Common,CFRunLoopRunInMode使得runloop在指定Mode中运行。

Mode Item

一个runloop的Mode下边包含多个Mode Item,Mode Item的种类有Source、Timer、Observer。一个Mode Item也可以被同时加入到多个runloopMode中,即二者是多对多的关系。所以,一个Mode Item(如NSTimer),可以加到CommonModes(是一个集合,存储了多个Mode的名称,主线程中是DefaultMode和UITrackingMode)中。

runloop的存活必须要伴随着Mode Item的存在,若该Mode中没有了任何Mode Item,则runloop会自动退出。创建一个NSTimer、NSPort、Source,即创建了一个runloopMode item。

Source0

Source0用于接收用户事件,如UIEvent等。Source0有公开的API可以使用,开发者可以通过CFRunLoopSourceCreate函数自行创建一个Source0。UIEvent是Source0事件这一点,也可以通过UI事件的断点堆栈信息来看出来:

而本文也会从runloop的源码来分析。

再结合前面RunLoop核心运行流程可以看出Source0(负责App内部事件,由App负责管理触发,例如UITouch事件)和Timer(又叫Timer Source,基于时间的触发器,上层对应NSTimer)是两个不同的Runloop事件源(当然Source0是Input Source中的一类,Input Source还包括Custom Input Source,由其他线程手动发出),RunLoop被这些事件唤醒之后就会处理并调用事件处理方法(CFRunLoopTimerRef的回调指针和CFRunLoopSourceRef均包含对应的回调指针)。

Source1

Source1是操作系统内核使用的,而开发者不能使用。Source1是基于mach_msg的原理实现的,必须通过读取某个mach port上的mach内核的消息队列中的消息(mach msg)来触发相应的回调任务。通常用于监听系统端口,与其他线程进行消息通信,能够主动唤醒当前线程的runloop。

Source1在处理任务的时候,通常会跟Source0一起配合,即分发一些任务给Source0去执行。如UITouch事件,最初是由Source1处理点击屏幕到事件捕获的任务,之后Source1将事件包装分发给Source0去处理。这一点非常关键。

Timer

NSTimer实际上是基于runloop实现的,其特性受到runloop的极大影响,如NSTimer相关中的持有关系如下:

Current RunLoop -> CFRunLoopMode -> sources数组 -> __NSCFTimer -> _NSTimerBlockTarget -> target
复制代码

Observer

runloop对象有六个可监控的状态(时间点)。开发者可以通过对这些状态添加观察者,在这些状态到来时触发一些回调任务,runloop的大部分使用实际上都体现在这里。

runloop的一些API

runloop的run方法

  1. runMode:beforeDate:,在指定Mode上运行一次runloop循环直至指定时间点。如果没有提前添加source或timer,则该方法立即结束,而线程的runloop状态不会受到影响(若未启动runloop则不会启动);如果有source或timer,一旦处理了首次输入source或者超出指定时间,函数即返回。而此时runloop未必就会退出,因为系统自身可能有添加一些其他的source。正是因为该函数仅运行一次runloop,所以通常搭配while的条件循环(用于控制runloop是否继续运行)来实现特定功能,如常驻子线程。timer并不属于输入source,所以在该函数返回前,timer的回调方法可能会触发多次。
  2. runUntilDate:,使得runloop在DefaultMode上运行到指定时间,运行期间runloop会处理所有已添加的输入source。实际上,该方法是重复调用runMode:beforeDate:来实现其功能的,因此runMode:beforeDate:方法的一些前提条件,这里也生效。
  3. run:,使得runloop在DefaultMode上一直运行下去,即运行一个无限循环来处理输入source或timer事件,永远不会退出。该方法同样是重复调用runMode:beforeDate:来实现其功能的,因此runMode:beforeDate:方法的一些前提条件,这里也生效。AFN 2.x源码中有使用该方法使得后台子线程一直存活。该方法通常要慎重使用。

我们最常使用的接口应该是runMode:beforeDate:,而推荐做法如下,使用一个全局的变量来控制runloop的运行状态。

BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
复制代码

最好是再添加一个@autoreleasepool,以达到更高效的内存管理。

runloop的底层数据结构

runloop相关的底层数据结构有CFRunLoopRef、CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopObserverRef等。

CFRunLoop

__CFRunLoop

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort;			// used for CFRunLoopWakeUp
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};
复制代码

这里有一些跟开发者日常使用比较密切的关键字段:

  1. pthread_t _pthread:runloop对应的线程,二者一一对应。
  2. CFMutableSetRef _commonModes:存储字符串(而非runloopMode对象)的容器,对应着所有标记为common的mode。
  3. CFMutableSetRef _commonModeItems,存储modeItem对象的容器,对应于所有标记为common的mode下的Item(即source、timer、observer)。
  4. CFRunLoopModeRef _currentMode:当前运行的runloopMode对象。
  5. CFMutableSetRef _modes:存储runloopMode对象的容器。

关于runloop的mode相关的字段,这里已经非常直观了。_modes、_commonModes、_commonModeItems都是容器而已,这也验证了之前讲到的:一个runloop对应多个mode,一个mode下可以有多个item。而如果在runloop运行过程中想切换mode,则只能退出runloop,再重新指定一个mode进入。这样做可以使得不同mode下的Item相互隔离,不会互相影响。

除此之外,还有一些字段对于理解runloop原理很有帮助:

  1. CFRuntimeBase _base:runloop可以有一些runtime的特性
  2. __CFPort _wakeUpPort:唤醒runloop的端口,这个是runloop原理的关键所在,可通过port来触发CFRunLoopWakeUp。后面会详细解释。
  3. volatile _per_run_data *_perRunData:这个就是runloop自身的一些存储数据了
  4. _blocks_head、_blocks_tail:一个存储block对象的链表结构。后续runloop执行任务(__CFRunLoopDoBlocks())会用到
  5. _runTime、_sleepTime:runloop的时间相关的

如何创建__CFRunLoop对象

我们通常使用[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()来获取当前线程的runloop,没有则会自动创建。

/// 获取主线程的runloop
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    /// 用static变量存储
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

/// 获取当前线程的runloop
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    /// 也是类似全局存储线程对应runloop对象的地方。
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}
复制代码

对于主线程的runloop对象,使用static变量__main来存储;对于子线程的runloop对象,也会有对应全局存储的地方(注意这里的TSD)。

_CFRunLoopGet0

_CFRunLoopGet0函数,可以通过当前线程pthread_t对象来获取其runloop对象,如果没有则新创建一个runloop对象。创建之后,将runloop对象保存在__CFRunLoops容器中,同时还会保存在当前线程的TSD中。

/// 全局的Dictionary, key是pthread_t, value是CFRunLoopRef。
static CFMutableDictionaryRef __CFRunLoops = NULL;
/// 访问__CFRunLoops时的锁。
static CFLock_t loopsLock = CFLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
/// 根据pthread来获取线程的runloop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
	    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
	    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        /// 首次创建该字典__CFRunLoops,即为主线程创建好了一个runloop。
	    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
	    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
	    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
	        CFRelease(dict);
	    }
        /// 这个局部变量mainLoop要release掉。此时,CFDictionary已经retain了该mainLoop对象。
	    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }

    /// 从字典__CFRunLoops中获取pthread_t对应的runloop。
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        /// 若没有则为该pthread创建一个runloop。
	    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        /// 这里又来一次字典中是否有目标runloop的判断,因为第一次判断后已经解锁了,可能在多线程的场景下已经创建好了该runloop
	    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
	    if (!loop) {
	        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
	        loop = newLoop;
	    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
        /// 此时,Dictionary已经retain了该newLoop对象
	    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        /// 这个_CFSetTSD中也会存储runloop对象
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            /// 注册一个回调__CFFinalizeRunLoop,当线程销毁时,顺便销毁其runloop对象。
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}
复制代码

静态变量__CFRunLoops是一个字典,用于存储runloop对象,key为线程对象(pthread_t),value为runloop对象(CFRunLoopRef)。

值得注意的双重if判断

* OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile )&__CFRunLoops) 将新创建的临时变量dict赋值给到__CFRunLoops,是一个Barrier操作,用于保证安全。

先看一个值得注意的地方:

if (!loop) {
  CFRunLoopRef newLoop = __CFRunLoopCreate(t);
  __CFLock(&loopsLock);
  loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
  if (!loop) {
      CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
      loop = newLoop;
  }
  // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
  __CFUnlock(&loopsLock);
  CFRelease(newLoop);
}
复制代码

注意其中的双重 if(!loop) 判断,也是为了线程安全。因为多线程情况下,可能两个线程都进入了外层if条件。***__CFLock(&loopsLock)*** 操作在同一时间只允许一个线程执行,但并不会取消其执行。如果没有内层的 if(!loop) 判断,可能导致多个线程都执行到 loop = newLoop; 这个赋值操作。所以,这个双层if判断是必不可少的,很多语言中使用单例对象等场景也需要严格考虑这种情况。

这里,也有一个不理解的地方,多线程情况下,依然会重复调用 CFRunLoopRef newLoop = __CFRunLoopCreate(t); 创建多个runloop对象。如果把__CFLock(&loopsLock);操作提前,且将runloop对象的创建放到内层if判断中,改成如下方式使得多线程情况下只创建一次runloop对象,会有什么问题呢?

if (!loop) {
  __CFLock(&loopsLock);
  loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
  if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
    loop = newLoop;
    CFRelease(newLoop);
  }
  // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
  __CFUnlock(&loopsLock);
}
复制代码

这样改动应该是不行的。这句注释 // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it 也说明了源码的意图。当然,多创建newLoop对象的开销,影响其实并不大。

__CFRunLoopCreate

_CFRunLoopGet0函数中的大部分代码比较容易理解,只是其中有个_CFGetTSD和_CFSetTSD比较陌生,这个是用于存储thread对象的私有数据,后边会有详细介绍。thread对象的runloop对象即在这个数据中存储着。继续看关键的__CFRunLoopCreate函数,该函数仅有一个参数就是pthread_t对象。

static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
    CFRunLoopRef loop = NULL;
    CFRunLoopModeRef rlm;
    /// 这里为啥要这样计算runloop对象的内存占用大小
    uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
    /// 说明:runloop对象的创建也是runtime期间创建的。
    loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
    if (NULL == loop) {
	    return NULL;
    }
    /// 初始化_per_run_data
    (void)__CFRunLoopPushPerRunData(loop);
    __CFRunLoopLockInit(&loop->_lock);
    loop->_wakeUpPort = __CFPortAllocate();
    if (CFPORT_NULL == loop->_wakeUpPort) HALT;
    __CFRunLoopSetIgnoreWakeUps(loop);

    /// commonModes确实是一个CFSet容器
    loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    /// commonModes中默认就包含DefaultMode
    CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
    
    /// 剩下就是其他字段的初始化
    loop->_commonModeItems = NULL;
    loop->_currentMode = NULL;
    loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    loop->_blocks_head = NULL;
    loop->_blocks_tail = NULL;
    loop->_counterpart = NULL;
    loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
    loop->_winthread = GetCurrentThreadId();
#else
    loop->_winthread = 0;
#endif
    rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
    if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
    return loop;
}
复制代码

uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase); 这一句很神秘。。。

loop对象创建好了以后,就是对它的一系列属性填充操作了:_perRunData、_wakeUpPort、_commonModes,_modes等。

CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode); 使得runloop对象一经创建,DefaultMode就加上了common标记。

rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true); 对runloop的DefaultMode进行了确认操作。

CFRuntimeBase和_CFRuntimeCreateInstance

这里有两行比较有意思的代码:

/// 这里为啥要这样计算runloop对象的内存占用大小
uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
/// 说明:runloop对象的创建也是runtime期间创建的。
loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
复制代码

__CFRuntimeBase是CoreFoundation的最基础结构,包括__CFBoolean、__CFString、__CFDate、__CFURL、__CFMachPort、__CFBundle等底层结构都有这个 CFRuntimeBase _base; 字段。__CFRuntimeBase中存储的信息包括:是否是系统默认分配的内存、runloop observer相关的fireing或repeats、CFTypeID、是否正在销毁deallocing、是否已经销毁dealloced、是否是自定义引用计数的对象、引用计数,这些在runtime源码中有体现。我们暂时只需要知道,runloop相关的各个数据结构都会有该 CFRuntimeBase _base; 字段,且会使用到该_base字段用于一些信息的存储。

/* All CF "instances" start with this structure.  Never refer to
 * these fields directly -- they are for CF's use and may be added
 * to or removed or change format without warning.  Binary
 * compatibility for uses of this struct is not guaranteed from
 * release to release.
 */
#if DEPLOYMENT_RUNTIME_SWIFT

typedef struct __attribute__((__aligned__(8))) __CFRuntimeBase {
    // This matches the isa and retain count storage in Swift
    uintptr_t _cfisa;
    uintptr_t _swift_rc;
    // This is for CF's use, and must match __NSCFType/_CFInfo layout
    _Atomic(uint64_t) _cfinfoa;
} CFRuntimeBase;

#define INIT_CFRUNTIME_BASE(...) {0, _CF_CONSTANT_OBJECT_STRONG_RC, 0x0000000000000080ULL}

#else

typedef struct __CFRuntimeBase {
    uintptr_t _cfisa;
#if TARGET_RT_64_BIT
    _Atomic(uint64_t) _cfinfoa;
#else
    _Atomic(uint32_t) _cfinfoa;
#endif
} CFRuntimeBase;

#if TARGET_RT_64_BIT
#define INIT_CFRUNTIME_BASE(...) {0, 0x0000000000000080ULL}
#else
#define INIT_CFRUNTIME_BASE(...) {0, 0x00000080UL}
#endif

#endif
复制代码

_CFRuntimeCreateInstance函数的介绍如下,使用参数1传入的CFAllocatorRef,进行CF实例的创建工作。具体的创建工作很复杂,涉及到该字段中存储的各类信息,感兴趣可以去研究runtime源码。

CF_EXPORT CFTypeRef _CFRuntimeCreateInstance(CFAllocatorRef allocator, CFTypeID typeID, CFIndex extraBytes, unsigned char *category);
	/* Creates a new CF instance of the class specified by the
	 * given CFTypeID, using the given allocator, and returns it. 
	 * If the allocator returns NULL, this function returns NULL.
	 * A CFRuntimeBase structure is initialized at the beginning
	 * of the returned instance.  extraBytes is the additional
	 * number of bytes to allocate for the instance (BEYOND that
	 * needed for the CFRuntimeBase).  If the specified CFTypeID
	 * is unknown to the CF runtime, this function returns NULL.
	 * No part of the new memory other than base header is
	 * initialized (the extra bytes are not zeroed, for example).
	 * All instances created with this function must be destroyed
	 * only through use of the CFRelease() function -- instances
	 * must not be destroyed by using CFAllocatorDeallocate()
	 * directly, even in the initialization or creation functions
	 * of a class.  Pass NULL for the category parameter.
复制代码

runloop对象的状态数据_perRunData

_perRunData是runloop的每次迭代(run)的独立数据,添加volatile防止编译器自行进行优化。参考 Volatile变量

volatile _per_run_data *_perRunData;              // reset for runs of the run loop

typedef struct _per_run_data {
    uint32_t a;
    uint32_t b;
    uint32_t stopped;
    uint32_t ignoreWakeUps;
} _per_run_data;
复制代码

_per_run_data的自身结构很简单,也就是用来作一系列状态标记的:

  1. runloop是否停止的标记,对应于_perRunData->stopped属性(停止则设置为0x53544F50,运行则为0x0)。
  2. runloop是否忽略唤醒的标记,对应于_perRunData->ignoreWakeUps(忽略则设置为0x57414B45,不忽略则为0x0)。
/* Bit 0 of the base reserved bits is used for stopped state */
/* Bit 1 of the base reserved bits is used for sleeping state */
/* Bit 2 of the base reserved bits is used for deallocating state */
// 创建runloop对象的_perRunData属性,返回的是旧的(previous)。
CF_INLINE volatile _per_run_data *__CFRunLoopPushPerRunData(CFRunLoopRef rl) {
    volatile _per_run_data *previous = rl->_perRunData;
    rl->_perRunData = (volatile _per_run_data *)CFAllocatorAllocate(kCFAllocatorSystemDefault, sizeof(_per_run_data), 0);
    rl->_perRunData->a = 0x4346524C;
    rl->_perRunData->b = 0x4346524C; // 'CFRL'
    rl->_perRunData->stopped = 0x00000000;
    rl->_perRunData->ignoreWakeUps = 0x00000000;
    return previous;
}

// 将旧的(previous)再赋值过来。
CF_INLINE void __CFRunLoopPopPerRunData(CFRunLoopRef rl, volatile _per_run_data *previous) {
    if (rl->_perRunData) CFAllocatorDeallocate(kCFAllocatorSystemDefault, (void *)rl->_perRunData);
    rl->_perRunData = previous;
}

CF_INLINE Boolean __CFRunLoopIsStopped(CFRunLoopRef rl) {
    return (rl->_perRunData->stopped) ? true : false;
}

CF_INLINE void __CFRunLoopSetStopped(CFRunLoopRef rl) {
    rl->_perRunData->stopped = 0x53544F50;	// 'STOP'
}

CF_INLINE void __CFRunLoopUnsetStopped(CFRunLoopRef rl) {
    rl->_perRunData->stopped = 0x0;
}

CF_INLINE Boolean __CFRunLoopIsIgnoringWakeUps(CFRunLoopRef rl) {
    return (rl->_perRunData->ignoreWakeUps) ? true : false;
}

CF_INLINE void __CFRunLoopSetIgnoreWakeUps(CFRunLoopRef rl) {
    rl->_perRunData->ignoreWakeUps = 0x57414B45; // 'WAKE'
}

CF_INLINE void __CFRunLoopUnsetIgnoreWakeUps(CFRunLoopRef rl) {
    rl->_perRunData->ignoreWakeUps = 0x0;
}
复制代码

这种C语言风格的Push/Pop操作可以说用的非常频繁了。runloop对象创建的时候,就使用 (void)__CFRunLoopPushPerRunData(loop); 进行了_per_run_data的初始化。

CFRunLoopMode

runloopMode是一个非常重要的概念,runloop相关的很多数据结构都是以runloopMode进行区分的,即不同runloopMode下的数据都是相互隔离的。

NSMutableSet

  1. Default Mode:一般处理timer、网络、数据库、IO等事件
  2. UITracking Mode:专门用于处理UI事件

切换Mode的方式是否也是push和pop之类的函数名称???

__CFRunLoopMode

看这个struct CFRunLoopModeRef,其中的 CFRuntimeBase _base; 在创建runloop对象的时候也有使用过。

一个CFRunLoopModeRef对象中,包含了sources0、sources1、observers、timers,以及portSet。每个Mode都有其独有的这些字段,所以同一个runloop的不同Mode的相互隔离是有底层数据结构作为支撑的。

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

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 */
};
复制代码

除了 CFRuntimeBase _base; 之外,还有以下这些字段:

  1. CFStringRef _name:每个mode都指定一个字符串名称。外部访问mode,通常是通过name来进行,若通过name找不到指定的runloopMode则不会有影响,避免了对mode的异常操作导致出错。
  2. Boolean _stopped:很简单的字段,标记了runloop的运行状态,实际上并非如此简单。还有之前讲到的_per_run_data。
  3. source0、source1、observers、timers都是集合类型,里边都是Mode Item,即一个Mode包含多个Mode Item。
  4. CFMutableDictionaryRef _portToV1SourceMap:存储了Source1的port与source的对应关系。key是mach_port_t, value是CFRunLoopSourceRef。
  5. __CFPortSet _portSet:需要监听的port,如_wakeUpPort,_timerPort等
  6. CFIndex _observerMask:runloopMode的状态相关的。

如果使用了dispatch source timers相关(MacOS X才有),则会有对应的GCD timerSource和GCD queue等字段。

#if DEPLOYMENT_TARGET_MACOSX
#define USE_DISPATCH_SOURCE_FOR_TIMERS 1
#define USE_MK_TIMER_TOO 1
#else
#define USE_DISPATCH_SOURCE_FOR_TIMERS 0
#define USE_MK_TIMER_TOO 1
#endif
复制代码

我们通常接触到的runloopMode只有DefaultMode和UITrackingRunLoopMode。实际上,系统还有非常多的其他Mode,一般情况下开发者不会接触到。参考 Run loop modes

如何创建__CFRunLoopMode对象

__CFRunLoopFindMode()函数根据传入的modeName字符串,找到对应的runloopMode对象,若没有,则会新创建一个。

rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL);
rlm->_name = CFStringCreateCopy(kCFAllocatorSystemDefault, modeName);
// 注意这个_portSet。
rlm->_portSet = __CFPortSetAllocate();
// 这个没懂?唤醒runloop时候需要用到的_wakeUpPort。将rl->_wakeUpPort放到rlm->_portSet中。
ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret);

#if USE_MK_TIMER_TOO
    rlm->_timerPort = mk_timer_create();
    ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
复制代码

rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL); 也是同样的套路。

初始化_portSet后,会调用 __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet); 将runloop的_wakeUpPort插入到runloopMode的_portSet中。我们知道,一个runloop可以有多种runloopMode,所以runloop的_wakeUpPort当然要同步到各个runloopMode中去。

另外,有一段使用USE_DISPATCH_SOURCE_FOR_TIMERS包起来的代码,在macOS X上才有的。虽然macOS X上才有,但是看起来好像挺有帮助的?即此时,runloopMode会有一个timer,该timer的回调操作就是将runloopMode的_timerFired字段置为true,即标记为被timer触发。

#if USE_DISPATCH_SOURCE_FOR_TIMERS
    rlm->_timerFired = false;
    // 这个queue,后续有用到?
    rlm->_queue = _dispatch_runloop_root_queue_create_4CF("Run Loop Mode Queue", 0);
    mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
    if (queuePort == MACH_PORT_NULL) CRASH("*** Unable to create run loop mode queue port. (%d) ***", -1);
    rlm->_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, rlm->_queue);

    __block Boolean *timerFiredPointer = &(rlm->_timerFired);
    dispatch_source_set_event_handler(rlm->_timerSource, ^{
        *timerFiredPointer = true;
    });

    // Set timer to far out there. The unique leeway makes this timer easy to spot in debug output.
    _dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 321);
    dispatch_resume(rlm->_timerSource);

    ret = __CFPortSetInsert(queuePort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);

#endif
复制代码

RunloopMode Item

RunloopMode Item通常即理解为runloop source、timer、observer。

NSRunLoopCommonModes

NSRunLoopCommonModes并非一个真正的RunLoopMode,而是一系列runloopMode的name的集合而已。

commonModes中存的是Mode的名称,一个mode可将自己的name添加到commonModes中,即将自身标记为common的。之后,runloop会将commonModeItems中的Source/Timer/Observer同步到所有打上common标记的Mode中。相当于:分别针对commonModes对应的每一个runloopMode,都添加了commonModeItems中对应的Source。

主线程的commonModes包含两个:Default Mode和UITracking Mode。可通过CFRunLoopAddCommonMode(rl, modeName)来将指定name的mode标记为common。

每个runloop对象都有commonMode相关的两个字段:

CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
复制代码

commonModes

commonModes是一个 * NSMutableSet<NSString > 的集合。其中存储的是runloopMode的name而已。

commonModeItems

也是一个Set集合,包含Observer,Timer,Source。

若runloop中无modeItem则会退出,所以 *** 常驻线程必须有mode item *** 。

items会被同步到所有打上common标记的mode中。

不同mode的items彼此隔离,只能使用commonMode来实现同步。如NSTimer对应可添加到DefaultMode和UITrackingMode。

CFRunLoopSourceRef

Runloop Source区分Source0与Source1。

  1. Source0并不能主动触发事件,只有一个回调函数。Source0不能主动唤醒runloop。如performSelector。如休眠时,点击屏幕触发source0,唤醒runloop。而iOS自身是如何对UITouch事件进行包装的???
  2. Source1能主动唤醒runloop,用于在iOS内核线程和其他线程之间以mach msg的方式进行通信,有一个回调函数和一个mach port。
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;
};
复制代码

其中,version0和version1分别对应着source0和source1.

source对象中有一个特殊的字段 ***CFMutableBagRef _runLoops;***,暂时还不清楚是用作什么用途?

CFRunLoopSourceContext(Source0)

Source0只包含一个回调函数(函数指针),不能主动唤醒runloop,需要手动调用 CFRunLoopWakeUp(runloop) 函数来唤醒runloop。必须先将Source0标记为Signal状态(调用CFRunLoopSourceSignal函数),即将这个Source0标记为待处理(_bits字段),然后调用CFRunLoopWakeUp(runloop)函数来唤醒runloop让其处理该事件。

怎么样是手动调用。。。

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, CFRunLoopMode mode);
    void	(*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void	(*perform)(void *info);
} CFRunLoopSourceContext;
复制代码

其中有三个函数指针:

  1. schedule: 当source加入到runloop时触发的回调函数
  2. cancel: 当source从runloop中移除时触发的回调函数
  3. perform: 当source事件被触发时的回调, 使用CFRunLoopSourceSignal函数触发.

info即为source相关的一些信息。

CFRunLoopSourceContext1(Source1)

Source1包含了一个mach port和一个回调函数(函数指针),可以通过mach port和其他线程通过消息(mach msg)传递的方式进行通信。而mach msg能够做到在用户态和内核态之间进行切换,即以这种方式来主动唤醒runloop,一般用于非常底层的使用场景,在iOS系统内部使用非常广泛。

Source1的底层结构如下:

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);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t	(*getPort)(void *info);
    void *	(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *	(*getPort)(void *info);
    void	(*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
复制代码

perform函数指针即指向runloop被唤醒后将要处理的事情。

Source1为啥没有schedule和cancel函数指针???

getPort函数指针,用于当source被添加到runloop中的时候,从该函数中获取具体的mach_port_t对象.

CFRunLoopTimerRef

NSTimer是与runloop息息相关的,CFRunLoopTimerRef与NSTimer是可以toll-free bridged(免费桥转换)的。当timer加到runloop的时候,runloop会注册对应的触发时间点,时间到了,runloop若处于休眠则会被唤醒,执行timer对应的回调函数。

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits; // 标记timer的状态
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes; // 存储的是runloopMode的名称
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;		/* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;			/* TSR units */
    CFIndex _order;			/* immutable */
    CFRunLoopTimerCallBack _callout;	/* immutable */
    CFRunLoopTimerContext _context;	/* immutable, except invalidation */
};
复制代码

__CFRunLoopTimer中的结构如下:

  1. uint16_t _bits:标记timer的状态.
  2. CFRunLoopRef _runLoop:timer注册在哪个runloop中?
  3. CFMutableSetRef _rlModes:timer运行在哪个runloopMode下?这里存储的是mode名称,可以有多个mode。也验证了一个timer可以在多个runloopMode中使用。
  4. CFAbsoluteTime _nextFireDate:timer的下次触发时机,每次触发后都会再次设置该值。即 rlt->_nextFireDate = CFAbsoluteTimeGetCurrent() + __CFTimeIntervalUntilTSR(nextFireTSR);
  5. CFTimeInterval _tolerance:timer的允许时间偏差???对此疑问在于:runloop既然会对timer精度有干扰,那么这个_tolerance存在的意义是什么?
  6. CFRunLoopTimerCallBack _callout:timer的回调函数
  7. CFRunLoopTimerContext _context:上下文

CFRunLoopTimerCallBack _callout; 的函数原型为:

typedef void (*CFRunLoopTimerCallBack)(CFRunLoopTimerRef timer, void *info);
复制代码

CFRunLoopTimerContext _context 这个字段,可以用于传递参数到timer对象的回调函数中。

typedef struct {
    CFIndex	version;
    void *	info;
    const void *(*retain)(const void *info);
    void	(*release)(const void *info);
    CFStringRef	(*copyDescription)(const void *info);
} CFRunLoopTimerContext;
复制代码

这两个地方的 * void info 即是同一个。

runloop也提供了直接使用CFRunLoopTimer的接口:

CF_EXPORT CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, CFRunLoopTimerCallBack callout, CFRunLoopTimerContext *context);
#if __BLOCKS__
CF_EXPORT CFRunLoopTimerRef CFRunLoopTimerCreateWithHandler(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, void (^block) (CFRunLoopTimerRef timer)) API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
复制代码

CFRunLoopTimerCreate()函数返回一个CFRunLoopTimerRef对象,其使用场景如下:

- (void)testRunloopTimer {
    CFRunLoopTimerContext context = {
        0,
        (__bridge void *)self,
        &CFRetain,
        &CFRelease,
        NULL
    };
    CFRunLoopTimerRef timer = CFRunLoopTimerCreate(NULL, CFAbsoluteTimeGetCurrent(), 1, 0, 0, &RunloopTimerCallBack, &context);
    CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes);
}

void RunloopTimerCallBack(CFRunLoopTimerRef timer, void *info) {
    NSLog(@"RunloopTimerCallBack");
    
    /// 方法1,使用static变量存储self对象。
    ViewController *self1 = (__bridge ViewController *)ViewControllerSelf;
    NSLog(@"%@", self1);
    
    /// 方法2,使用CFRunLoopTimerContext来传递self对象。
    ViewController *self2 = (__bridge ViewController *)info;
    NSLog(@"%@", self2);
}
复制代码

若要使用在更精准的定时场景,可以放弃NSTimer,转而使用GCD timer。关于GCD Timer的使用,可以参考博客 比较一下iOS中的三种定时器

CFRunLoopObserverRef

runloop的生命周期中,其运行状态有六种:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Sources,这里是Source0
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出runloop
};
复制代码

使用Observer即可对runloop的这些状态进行监听,以完成指定任务。尤其是kCFRunLoopBeforeWaiting这个状态,不仅在iOS内部使用的非常多,开发者对其也比较钟爱,因为一旦到了kCFRunLoopBeforeWaiting状态,iOS系统就要从用户态切换至内核态,进入休眠状态了。也就意味着,此时用户任务已经全部完成了,CPU可以空闲了。

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 */
};
复制代码

Observer中的字段也很直观,_callout和_context就不多说了,跟timer类似。

  1. CFIndex _rlCount:runloop的运行次数???
  2. CFOptionFlags _activities:可以监听多个状态?
  3. CFIndex _order:这是个什么顺序???

observer也包含一个回调函数,在监听的runloop状态出现时触发该回调函数。runloop对observer的使用逻辑,基本与timer一致,都需要指定callback函数,然后通过context可传递参数。只要掌握到了这个套路,关于runloop的这一类使用基本不会陌生了。

如下代码,对runloop的BeforeWaiting状态进行了监听:

- (void)addRunloopObserver {
    CFRunLoopObserverContext context = { 0, (__bridge void *)self, &CFRetain, &CFRelease, NULL };
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &RunloopObserverCallBack, &context);
    
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
}

void RunloopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    NSLog(@"RunloopObserverCallBack");
}
复制代码

注意,Observer会依赖于特定的runloopMode,所以一旦runloop运行于其他mode,则需要的状态就会无法监听到。

mach_msg

Mach是iOS为内核Darwin的核心,提供了处理器调度、进程间通信(IPC)等基础服务。Mach中的进程间通信是以mach msg的方式,在两个mach port之间传递msg来进行的。发送和接收消息都是通过mach_msg函数来实现的。

runloop的Source1也称为Port-based Source,因其依靠mach msg发送到指定的mach port来唤醒runloop的。mach_msg()函数的原型如下:

/*
 *  Routine:    mach_msg
 *  Purpose:
 *      Send and/or receive a message.  If the message operation
 *      is interrupted, and the user did not request an indication
 *      of that fact, then restart the appropriate parts of the
 *      operation silently (trap version does not restart).
 */
mach_msg_return_t mach_msg(
    mach_msg_header_t *msg,
    mach_msg_option_t option,
    mach_msg_size_t send_size,
    mach_msg_size_t rcv_size,
    mach_port_name_t rcv_name,
    mach_msg_timeout_t timeout,
    mach_port_name_t notify
);
复制代码

mach_msg()内部实际上是调用了mach_msg_trap()系统调用。陷阱trap是操作系统层面的基本概念,用于操作系统状态的更改,如触发内核态与用户态的切换操作。trap通常由异常条件触发,如断点、除0操作、无效内存访问等。若程序没有Source的时候,runloop会进入休眠状态,是通过调用runloop源码中的__CFRunLoopServiceMachPort函数来实现,该函数内部即调用了mach_msg相关的函数操作使得系统内核状态发生改变:用户态切换至内核态。

如:App静止时,处于runloop休眠,点击调试暂停,则主线程调用栈是停留在mach_msg_trap()。

mach_msg()函数可以设置timeout参数,如果在timeout到来之前没有读到msg,当前线程的runloop会处于休眠状态。

mach_msg_header_t

mach_msg_header_t中存储了msg的基本信息,包括端口信息等。如本地端口msgh_local_port和远程端口msgh_remote_port,msg的传递方向在header中已经非常明确了。

typedef struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
} mach_msg_base_t;

typedef struct {
    mach_msg_bits_t msgh_bits;
    mach_msg_size_t msgh_size;
    mach_port_t msgh_remote_port;
    mach_port_t msgh_local_port;
    mach_port_name_t msgh_voucher_port;
    mach_msg_id_t msgh_id;
} mach_msg_header_t;
复制代码
typedef struct{
	mach_msg_size_t msgh_descriptor_count;
} mach_msg_body_t;
复制代码

mach_port_t

Mach Port实际上就是整数,用于标记端口。

typedef __darwin_mach_port_t mach_port_t;
typedef __darwin_natural_t __darwin_mach_port_name_t; /* Used by mach */
typedef __darwin_mach_port_name_t __darwin_mach_port_t; /* Used by mach */
typedef unsigned int            __darwin_natural_t;
复制代码

关于mach_port_t的一段说明:

/*
 *	mach_port_t - a named port right
 *
 *	In user-space, "rights" are represented by the name of the
 *	right in the Mach port namespace.  Even so, this type is
 *	presented as a unique one to more clearly denote the presence
 *	of a right coming along with the name.
 *
 *	Often, various rights for a port held in a single name space
 *	will coalesce and are, therefore, be identified by a single name
 *	[this is the case for send and receive rights].  But not
 *	always [send-once rights currently get a unique name for
 *	each right].
 *
 *	This definition of mach_port_t is only for user-space.
 *
 */
复制代码

mach msg的其他字段

  1. mach_msg_option_t option:标记了消息传递的方向。
  2. send_size和rcv_size:消息传递的一些数据大小
  3. mach_port_name_t notify

Thread-specific data

Thread-specific data(TSD)是线程私有的数据,包含TSD的一些函数用于向线程(thread)对象中存储和获取数据。如CFRunLoopGetMain()函数,调用_CFRunLoopGet0(),在其中即利用了TSD接口从thread中得到runloop对象。所以runloop对象不光存在于全局字典中,也存在于线程的TSD中。

关于TSD的详细源码,请参考 CFPlatform.c

// __CFTSDTable
typedef struct __CFTSDTable {
    uint32_t destructorCount;
    uintptr_t data[CF_TSD_MAX_SLOTS];
    tsdDestructor destructors[CF_TSD_MAX_SLOTS];
} __CFTSDTable;

// _CFGetTSD
CF_EXPORT void *_CFGetTSD(uint32_t slot) {
    __CFTSDTable *table = __CFTSDGetTable();
    if (!table) { return NULL; }
    uintptr_t *slots = (uintptr_t *)(table->data);
    return (void *)slots[slot];
}

// _CFSetTSD
CF_EXPORT void *_CFSetTSD(uint32_t slot, void *newVal, tsdDestructor destructor) {
    __CFTSDTable *table = __CFTSDGetTable();
    if (!table) { return NULL; }

    void *oldVal = (void *)table->data[slot];
    table->data[slot] = (uintptr_t)newVal;
    table->destructors[slot] = destructor;
    
    return oldVal;
}
复制代码

__CFTSDTable即为存储私有数据的数据结构,data数组是私有函数,destructors用来保存data元素对应的析构函数。_CFGetTSD()函数与_CFSetTSD()即提供了对__CFTSDTable的操作接口。如之前新创建runloop对象的代码中:

CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
复制代码

* _CFSetTSD(__CFTSDKeyRunLoop, (void )loop, NULL); 即给当前thread的私有数据的第__CFTSDKeyRunLoop个插槽的位置,设置一个runloop对象。thread中存储runloop对象就是这样实现的。

_CFSetTSD()函数的第三个参数即为该线程销毁时的一个析构函数。如 * _CFSetTSD(__CFTSDKeyRunLoopCntr, (void )(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void ( )(void ))__CFFinalizeRunLoop); 即给thread的第__CFTSDKeyRunLoopCntr个插槽设置了一个值,线程销毁时会触发 (void ( )(void ))__CFFinalizeRunLoop 函数的调用,即 __CFFinalizeRunLoop 函数是该runloop对象的析构函数。 PTHREAD_DESTRUCTOR_ITERATIONS 是线程退出时销毁其私有数据TSD的最大次数。所以,线程退出的时候,runloop对象就是这样销毁的。

实际上,runloop源码中有一系列这样的操作:

_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(data - 1), (void (*)(void *))__CFFinalizeRunLoop);
_CFSetTSD(__CFTSDKeyRunLoopCntr, 0, (void (*)(void *))__CFFinalizeRunLoop);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
// 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);
// Restore the previous voucher
_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
复制代码

在__CFTSDKeyRunLoop这个slot上存储runloop对象,在__CFTSDKeyRunLoopCntr这个slot上存储runloop对象的析构函数__CFFinalizeRunLoop。源码中,将runloop对象与其 引用计数 分开进行存储。

这里有一个有趣的地方:

_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
复制代码

这里的 (void *)6 和 (void *)0 是干啥呢?原来,__CFRunLoopModeIsEmpty()函数和__CFRunLoopRun()函数中,都有使用 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ) 的判断,以得到libdispatchQSafe变量的值。所以,这个__CFTSDKeyIsInGCDMainQ其实仅仅是用于在 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE (msg) 函数执行前后添加一个判断条件,以此完成类似锁操作而已。

Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue
复制代码
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
复制代码

另外,TSD是如何保证线程销毁时,这些析构函数都能正常调用呢?__CFTSDGetTable函数中有这样一句。

pthread_key_init_np(CF_TSD_KEY, __CFTSDFinalize);
复制代码

__CFTSDFinalize()函数即为CF_TSD_KEY对应的析构函数,也就是线程自身实际的销毁函数。它调用的时候,会遍历TSD中的所有slot,将data置为NULL,调用slot对应的析构函数,析构函数的参数为data。

static void __CFTSDFinalize(void *arg) {
    __CFTSDSetSpecific(arg);

    if (!arg || arg == CF_TSD_BAD_PTR) {
        return;
    }
    
    __CFTSDTable *table = (__CFTSDTable *)arg;
    table->destructorCount++;
        
    for (int32_t i = 0; i < CF_TSD_MAX_SLOTS; i++) {
        if (table->data[i] && table->destructors[i]) {
            uintptr_t old = table->data[i];
            table->data[i] = (uintptr_t)NULL;
            table->destructors[i]((void *)(old));
        }
    }
    
    // On PTHREAD_DESTRUCTOR_ITERATIONS-1 call, destroy our data
    if (table->destructorCount == PTHREAD_DESTRUCTOR_ITERATIONS - 1) {
        free(table);
        
        __CFTSDSetSpecific(CF_TSD_BAD_PTR);
        return;
    }
}
复制代码

所以,线程销毁时,runloop对象对应的析构函数__CFFinalizeRunLoop就是这样调用的。

小结

通过以上的一些底层数据结构,我们对runloop的底层实现有了一个大致的了解:

  1. runloop对象与thread一一对象,其映射关系存在于全局字典__CFRunLoops和线程的TSD中。thread默认没有runloop对象,在第一次获取的时候会自动创建;而thread退出的时候,runloop对象也会销毁,通过thread的TSD来实现。
  2. 一个runloop对象一次只能运行于一个mode中,一个mode可以对应于多个mode items(source、timer)
  3. CommonMode只是标记而已,可以为runloopMode打上common标记。
  4. Source0不能主动唤醒runloop,只能通过设置回调函数(如timer);而Source1是基于mach port和mach msg的,能主动唤醒runloop(因能够通过mach msg来触发系统内核状态改变)
  5. 可以添加Observer对runloop的运行状态进行监听,以完成指定的任务。
  6. 线程休眠和唤醒是通过mach_msg函数来实现的,基于mach port的基础之上。
  7. MACOSX下,runloop中使用GCD Timer和MK Timer来做定时器;而其他情况,使用MK Timer来做定时器。

参考资料

  1. Event loop
  2. Run Loops
  3. NSRunLoop
  4. CFRunLoop
  5. CFRunLoopPerformBlock
  6. CFRunLoopPerformBlock vs dispatch_async
  7. RunLoop.subproj
  8. Kernel Programming Guide: Mach Overview
  9. mach_msg
  10. CFPlatform.c
  11. AsyncDisplayKit,已更名为Texture
  12. 深入理解RunLoop
  13. 解密 Runloop
  14. iOS刨根问底-深入理解RunLoop
  15. 优化UITableViewCell高度计算的那些事
  16. 重拾RunLoop原理
  17. iOS RunLoop详解
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章