Runloop 是和线程紧密相关的一个基础组件,是很多线程有关功能的幕后功臣。尽管在平常使用中几乎不太会直接用到,理解 Runloop 有利于我们更加深入地理解 iOS 的多线程模型。
本文从如下几个方面理解RunLoop的相关知识点。
RunLoop概念
RunLoop实现
RunLoop运行
RunLoop应用
RunLoop概念
RunLoop介绍
RunLoop 是什么?RunLoop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 RunLoop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,RunLoop 会进入休眠状态,有事件发生时, RunLoop 会去找对应的 Handler 处理事件。RunLoop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
从代码上看,RunLoop其实就是一个对象,它的结构如下,源码看这里:
struct __CFRunLoop { CFRuntimeBase _base; pthread_mutex_t _lock; /* locked for accessing mode list */ __CFPort _wakeUpPort; // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop Boolean _unused; volatile _per_run_data *_perRunData; // reset for runs of the run loop pthread_t _pthread; //RunLoop对应的线程 uint32_t _winthread; CFMutableSetRef _commonModes; //存储的是字符串,记录所有标记为common的mode CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer) CFRunLoopModeRef _currentMode; //当前运行的mode CFMutableSetRef _modes; //存储的是CFRunLoopModeRef struct _block_item *_blocks_head;//doblocks的时候用到 struct _block_item *_blocks_tail; CFTypeRef _counterpart; };复制代码
可见,一个RunLoop对象,主要包含了一个线程,若干个Mode,若干个commonMode,还有一个当前运行的Mode。
RunLoop与线程
当我们需要一个常驻线程,可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。我们就在线程里面执行下面这个代码,一直等待消息,线程就不会退出了。
do { //获取消息 //处理消息 } while (消息 != 退出) 复复制代码
上面的这种循环模型被称作 Event Loop,事件循环模型在众多系统里都有实现,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
下图描述了Runloop运行流程(基本描述了上面Runloop的核心流程,当然可以查看官方The Run Loop Sequence of Events
描述):
整个流程并不复杂(需要注意的就是_黄色_区域的消息处理中并不包含source0,因为它在循环开始之初就会处理),整个流程其实就是一种Event Loop的实现,其他平台均有类似的实现,只是这里叫做RunLoop。
RunLoop与线程的关系如下图
图中展现了 Runloop 在线程中的作用:从 input source 和 timer source 接受事件,然后在线程中处理事件。
Runloop 和线程是绑定在一起的。每个线程(包括主线程)都有一个对应的 Runloop 对象。我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象。
主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动。
RunLoop Mode
Mode可以视为事件的管家,一个Mode管理着各种事件,它的结构如下:
struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock; /* must have the run loop locked before locking this */ CFStringRef _name; //mode名称 Boolean _stopped; //mode是否被终止 char _padding[3]; //几种事件 CFMutableSetRef _sources0; //sources0 CFMutableSetRef _sources1; //sources1 CFMutableArrayRef _observers; //通知 CFMutableArrayRef _timers; //定时器 CFMutableDictionaryRef _portToV1SourceMap; //字典 key是mach_port_t,value是CFRunLoopSourceRef __CFPortSet _portSet; //保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中 CFIndex _observerMask;#if USE_DISPATCH_SOURCE_FOR_TIMERSdispatch_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_TOOmach_port_t _timerPort; Boolean _mkTimerArmed;#endif#if DEPLOYMENT_TARGET_WINDOWSDWORD _msgQMask; void (*_msgPump)(void);#endifuint64_t _timerSoftDeadline; /* TSR */ uint64_t _timerHardDeadline; /* TSR */ }; 复复制代码
一个CFRunLoopMode对象有一个name,若干source0、source1、timer、observer和若干port,可见事件都是由Mode在管理,而RunLoop管理Mode。
从源码很容易看出,Runloop总是运行在某种特定的CFRunLoopModeRef下(每次运行**__CFRunLoopRun()函数时必须指定Mode)。而通过CFRunloopRef对应结构体的定义可以很容易知道每种Runloop都可以包含若干个Mode,每个Mode又包含Source/Timer/Observer。每次调用Runloop的主函数__CFRunLoopRun()时必须指定一种Mode,这个Mode称为 _currentMode**,当切换Mode时必须退出当前Mode,然后重新进入Runloop以保证不同Mode的Source/Timer/Observer互不影响。
如图所示,Runloop Mode 实际上是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同组的 Source,Timer 和 Observer 隔绝开来。Runloop 在某个时刻只能跑在一个 Mode 下,处理这一个 Mode 当中的 Source,Timer 和 Observer。
苹果文档中提到的 Mode 有五个,分别是:
NSDefaultRunLoopMode
NSConnectionReplyMode
NSModalPanelRunLoopMode
NSEventTrackingRunLoopMode
NSRunLoopCommonModes
iOS 中公开暴露出来的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes。 NSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode(注意:并不是说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。当然你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合)。 五种Mode的介绍如下图:
RunLoop Source
Run Loop Source分为Source、Observer、Timer三种,他们统称为ModeItem。
CFRunLoopSource
根据官方的描述,CFRunLoopSource是对input sources的抽象。CFRunLoopSource分source0和source1两个版本,它的结构如下:
struct __CFRunLoopSource { CFRuntimeBase _base; uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理 pthread_mutex_t _lock; CFIndex _order; /* immutable */ CFMutableBagRef _runLoops; union { CFRunLoopSourceContext version0; /* immutable, except invalidation */ CFRunLoopSourceContext1 version1; /* immutable, except invalidation */ } _context; };复制代码
source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态,以下是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); } CFRunLoopSourceContext;复制代码
source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
source1由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是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);#elsevoid * (*getPort)(void *info); void (*perform)(void *info);#endif} CFRunLoopSourceContext1;复制代码
Source1除了包含回调指针外包含一个mach port,Source1可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)。官方也指出可以自定义Source,因此对于CFRunLoopSourceRef来说它更像一种协议,框架已经默认定义了两种实现,如果有必要开发人员也可以自定义,详细情况可以查看官方文档。
CFRunLoopObserver
CFRunLoopObserver是观察者,可以观察RunLoop的各种状态,并抛出回调。
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 */ };复制代码
CFRunLoopObserver可以观察的状态有如下6种:
/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), //即将进入run loop kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer kCFRunLoopBeforeSources = (1UL << 2),//即将处理sourcekCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒但是还没开始处理事件 kCFRunLoopExit = (1UL << 7),//run loop已经退出 kCFRunLoopAllActivities = 0x0FFFFFFFU };复制代码
Runloop 通过监控 Source 来决定有没有任务要做,除此之外,我们还可以用 Runloop Observer 来监控 Runloop 本身的状态。 Runloop Observer 可以监控上面的 Runloop 事件,具体流程如下图。
CFRunLoopTimer
CFRunLoopTimer是定时器,可以在设定的时间点抛出回调,它的结构如下:
struct __CFRunLoopTimer { CFRuntimeBase _base; uint16_t _bits; //标记fire状态 pthread_mutex_t _lock; CFRunLoopRef _runLoop; //添加该timer的runloop CFMutableSetRef _rlModes; //存放所有 包含该timer的 mode的 modeName,意味着一个timer可能会在多个mode中存在 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和NSTimer是toll-free bridged的,可以相互转换。
CFRunLoopTimer is “toll-free bridged” with its Cocoa Foundation counterpart, NSTimer. This means that the Core Foundation type is interchangeable in function or method calls with the bridged Foundation object.
所以CFRunLoopTimer具有以下特性:
CFRunLoopTimer 是定时器,可以在设定的时间点抛出回调
CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互转换
RunLoop实现
下面从以下3个方面介绍RunLoop的实现。
获取RunLoop
添加Mod
添加Run Loop Source
获取RunLoop
从苹果开放的API来看,不允许我们直接创建RunLoop对象,只能通过以下几个函数来获取RunLoop:
CFRunLoopRef CFRunLoopGetCurrent(void)
CFRunLoopRef CFRunLoopGetMain(void)
+(NSRunLoop *)currentRunLoop
+(NSRunLoop *)mainRunLoop
前两个是Core Foundation中的API,后两个是Foundation中的API。
那么RunLoop是什么时候被创建的呢?
我们从下面几个函数内部看看。
CFRunLoopGetCurrent
//取当前所在线程的RunLoop CFRunLoopRef CFRunLoopGetCurrent(void) { CHECK_FOR_FORK(); CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);if (rl) return rl; //传入当前线程return _CFRunLoopGet0(pthread_self()); }复制代码
在CFRunLoopGetCurrent函数内部调用了_CFRunLoopGet0(),传入的参数是当前线程pthread_self()。这里可以看出,CFRunLoopGetCurrent函数必须要在线程内部调用,才能获取当前线程的RunLoop。也就是说子线程的RunLoop必须要在子线程内部获取。
CFRunLoopGetMain
do { //获取消息 //处理消息 } while (消息 != 退出) 复复制代码
0
在CFRunLoopGetMain函数内部也调用了_CFRunLoopGet0()
,传入的参数是主线程pthread_main_thread_np()
。可以看出,CFRunLoopGetMain()
不管在主线程还是子线程中调用,都可以获取到主线程的RunLoop。
CFRunLoopGet0
前面两个函数都是使用了CFRunLoopGet0实现传入线程的函数,下面看下CFRunLoopGet0的结构是咋样的。
do { //获取消息 //处理消息 } while (消息 != 退出) 复复制代码
1
这段代码可以得出以下结论:
RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中
主线程的RunLoop会在初始化全局字典时创
子线程的RunLoop会在第一次获取的时候创建,如果不获取的话就一直不会被创建
RunLoop会在线程销毁时销毁
添加Mode
在Core Foundation中,针对Mode的操作,苹果只开放了以下3个API(Cocoa中也有功能一样的函数,不再列出):
CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl)
CFRunLoopAddCommonMode Adds a mode to the set of run loop common modes. 向当前RunLoop的common modes中添加一个mode。 CFRunLoopCopyCurrentMode Returns the name of the mode in which a given run loop is currently running. 返回当前运行的mode的name CFRunLoopCopyAllModes Returns an array that contains all the defined modes for a CFRunLoop object. 返回当前RunLoop的所有mode
我们没有办法直接创建一个CFRunLoopMode对象,但是我们可以调用CFRunLoopAddCommonMode传入一个字符串向RunLoop中添加Mode,传入的字符串即为Mode的名字,Mode对象应该是此时在RunLoop内部创建的。下面来看一下源码。
CFRunLoopAddCommonMode
do { //获取消息 //处理消息 } while (消息 != 退出) 复复制代码
2
可以看得出:
modeName不能重复,modeName是mode的唯一标识符
RunLoop的_commonModes数组存放所有被标记为common的mode的名称
添加commonMode会把commonModeItems数组中的所有source同步到新添加的mode
CFRunLoopMode对象在CFRunLoopAddItemsToCommonMode函数中调用CFRunLoopFindMode时被创建
CFRunLoopCopyCurrentMode/CFRunLoopCopyAllModes
CFRunLoopCopyCurrentMode和CFRunLoopCopyAllModes的内部逻辑比较简单,直接取RunLoop的_currentMode和_modes返回,就不贴源码了。
添加Run Loop Source(ModeItem)
我们可以通过以下接口添加/移除各种事件:
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)
CFRunLoopAddSource
CFRunLoopAddSource的代码结构如下:
do { //获取消息 //处理消息 } while (消息 != 退出) 复复制代码
3
通过添加source的这段代码可以得出如下结论:
如果modeName传入kCFRunLoopCommonModes,则该source会被保存到RunLoop的_commonModeItems中
如果modeName传入kCFRunLoopCommonModes,则该source会被添加到所有commonMode中
如果modeName传入的不是kCFRunLoopCommonModes,则会先查找该Mode,如果没有,会创建一个
同一个source在一个mode中只能被添加一次
CFRunLoopRemoveSource
remove操作和add操作的逻辑基本一致,很容易理解。
do { //获取消息 //处理消息 } while (消息 != 退出) 复复制代码
4