iOS RunLoop
概念
- RunLoop 是一个对象,在它的生命周期中有一个线程与之对应,它是这个线程的thread specific data(线程特有数据);它是线程不安全的,所有的操作都必须在这个线程中进行;
- 开发者很少直接使用 RunLoop,大部分情况下系统已经通过 RunLoop 来响应例如网络、输入输出事件、点击事件、UI事件、定时器事件等;
- RunLoop 内部实际上是一个 dowhile 循环,不同于我们自己写的循环,RunLoop 循环的目的是保证系统能在闲时不占用过多的资源,而在需要响应事件时及时做出响应;
RunLoop 流程
在了解 RunLoop 流程之前,需要先了解 RunLoop 的一些属性或元素
RunLoop 相关的属性/元素
在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoop
CFRunLoopMode
CFRunLoopSource
CFRunLoopTimer
CFRunLoopObserver
下面是部分源码:
/* CFRunLoop.h */
typedef struct __CFRunLoop * CFRunLoopRef;
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
/* CFRunLoop.c */
struct __CFRunLoopMode {
...
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
}
struct __CFRunLoop {
...
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
}
通过这些声明,可以很清楚的明确它们的关系,但 CFRunLoopMode
并未开放出来
CFRunLoop
包含若干CFRunLoopMode
CFRunLoopMode
包含若干CFRunLoopSource
、CFRunLoopObserver
、CFRunLoopTimer
我们先从 CFRunLoopMode
包含的 source/observer/timer 说起
CFRunLoopSource
CFRunLoopSource
用定义了一个 _context
共同体,个人认为这个属性是用来区分 source0
和 source1
的:
struct __CFRunLoopSource {
...
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
}
通过 ibireme(后简称”I大”)的文章「RunLoop 对外的接口」一节我们得知 source0
和 source1
的区别:
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
source1
的信息可以在 CFRunLoopAddSource
这个函数实现中发现一些:
/* CFRunLoop.c */
...
typedef mach_port_t __CFPort;
...
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
...
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL != rlm && NULL == rlm->_sources0) {
rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
}
...
if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
...
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
...
}
}
CFRunLoopObserver
CFRunLoopObserver
是观察者,它包含了一个状态位和回调函数:
/* CFRunLoop.c */
struct __CFRunLoopObserver {
...
CFOptionFlags _activities; /* immutable */
...
CFRunLoopObserverCallBack _callout; /* immutable */
...
}
/* CFRunLoop.h */
/* 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
};
CFRunLoopTimer
以下引用自 I大 的文章:
CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
其结构大致如下:
/* CFRunLoop.c */
struct __CFRunLoopTimer {
...
CFTimeInterval _interval; /* immutable */
...
CFRunLoopTimerCallBack _callout; /* immutable */
...
}
CFRunLoopMode
以上 source/observer/timer 是 CFRunLoopMode
的重要组成部分:
从源码很容易看出,Runloop总是运行在某种特定的CFRunLoopModeRef下(每次运行__CFRunLoopRun()函数时必须指定Mode)。而通过CFRunloopRef对应结构体的定义可以很容易知道每种Runloop都可以包含若干个Mode,每个Mode又包含Source/Timer/Observer。每次调用Runloop的主函数__CFRunLoopRun()时必须指定一种Mode,这个Mode称为** _currentMode**,当切换Mode时必须退出当前Mode,然后重新进入Runloop以保证不同Mode的Source/Timer/Observer互不影响。
以上文章节选自另一位先行者 KenshinCui(下文简称“K大”)的文章,以下是节选的部分源码,以验证 K大 的结论:
/* CFRunLoop.c */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
...
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}
...
}
通过 CFRunLoopFindMode
函数获取 mode:
/* CFRunLoop.c */
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
...
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
...
int32_t result = kCFRunLoopRunFinished;
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
...
}
static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create) {
...
rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL);
if (NULL == rlm) {
return NULL;
}
...
rlm->_name = CFStringCreateCopy(kCFAllocatorSystemDefault, modeName);
rlm->_stopped = false;
...
return rlm;
}
开发者可以通过以下两个方法来管理 mode:
CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode);
CF_EXPORT SInt32 CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
它们在内部通过以下方法进行 mode 的修改:
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName);
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName);
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName);
void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName);
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName);
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName);
执行三组函数时,RunLoop
都会将 _commonModeItems
里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里,我们可以在以上代码的实现中发现以下代码片段:
...
if (modeName == kCFRunLoopCommonModes) {
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rlt);
...
}
...
RunLoop 的内部逻辑
苹果在 RunLoop
文档The Run Loop Sequence of Events这一项中提到了两个概念:non-port-based input sources
和 port-based input source
,根据上面我们分析的 source0 和 source1 的区别,很容易得知 source0
是前者,而 source1
属于后者;流程图可以参照 I大 文章中 「RunLoop 的内部逻辑」展示的图片,如下图所示:
这一章 I大 和K大 的文章说的都很具体,我们可以结合起来一起看。详细的代码可以在 CFRunLoop.c
文件中的 CFRunLoopRunSpecific
函数找到,这里就不再展开说明了;
RunLoop 与线程的关系
还有一点需要说明的是,苹果不允许用户直接创建 RunLoop
,而是提供了两个函数来实现:
CF_EXPORT CFRunLoopRef CFRunLoopGetCurrent(void);
CF_EXPORT CFRunLoopRef CFRunLoopGetMain(void);
它们函数都调用了 _CFRunLoopGet0
,在这个函数中我们可以看出 RunLoop
与线程的关系,下面是部分源码:
/* CFRunLoop.c */
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
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);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
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 (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
一些说明:
- 首次调用这个函数时,因为
__CFRunLoops
为NULL
,会创建一个字典dict
和mainLoop
,并将pthreadPointer(pthread_main_thread_np())
作为key
,mainLoop
作为值保存在dict
里;此时通过OSAtomicCompareAndSwapPtrBarrier
函数,在__CFRunLoops
等于NULL
时,将dict
赋值给__CFRunLoops
,并保证其原子性,否则不赋值; - 根据传递进来的线程
t
获取runLoop
对象,如果对象不存在就创建一个新的;
这里有一个
cf_trace
函数,在文件头部被声明为#define cf_trace(...) do{}while(0)
,这在C/C++
中比较常见,一般用来消除goto
语句;在宏定义中声明,还能够让宏定义作为 if 语句条件的情况下保证不出错;
网上搜了搜并没有发现
tc_trace
的有关说明,因此这个函数在这里的具体作用尚不清楚,如有知晓的老哥老姐欢迎补充;
- 下一步,判断传递进来的线程
t
是否是当前线程,如果是,就调用CFSetTSD
设置__CFRunLoops
和loop
的对应关系;TSD
即thread specific data
,意思是线程特有数据; - 最后一步,通过
__CFFinalizeRunLoop
这个函数将其释放;至于为什么通过CFTSDKeyRunLoopCntr
能够设置清理loop
的回调,另一位先行者 ZenonHuang 在他的 blog 里做了详细说明;
顺带说一句,通过
CFPlatform.c
中__CFTSDFinalize
的实现,以及CFInternal.h
中的宏定义:{
…
__CFTSDKeyRunLoop = 10,
…
// autorelease pool stuff must be higher than run loop constants __CFTSDKeyAutoreleaseData2 = 61,
__CFTSDKeyAutoreleaseData1 = 62,
…
}
可以明确推断出
RunLoop
和aotuRelease
的释放顺序关系:前者在后者释放之前释放;
RunLoop 实践
何时使用 RunLoop
苹果在文档中指出,开发者使用 RunLoop
的唯一场景是在需要创建辅助线程时;而在主线程,RunLoop
会由系统自动配置并生效;
如果启用了辅助线程,使用 RunLoop
也并非必须:如果只这个线程中执行一些预定的耗时操作,而不涉及到跨线程的交互时,是不需要用到 RunLoop
的:
If you use a thread to perform some long-running and predetermined task, you can probably avoid starting the run loop.
那么何种场景需要采用 RunLoop
呢?苹果给出了几个场景:
- 需要通过使用端口或自定义输入源与其它线程进行交互;
- 在线程里使用定时器;
- 在 Cocoa 应用程序中使用任意
performSelector...
方法; - 保持线程定期的执行某项任务;
总结一下就是涉及跨线程交互、涉及时间相关(定期做某事)都需要用到 RunLoop
;
例子
假如某个操作比较耗时,且需要多次调用,我们可以新开一个线程来处理,为了保持这个线程一直存在,我们需要在线程中加入一个 RunLoop 来保证这个线程一直存在,大致的代码如下:
/* ViewController.swift */
/// 懒加载自定义线程对象
lazy var myThread: Thread = {
let th = Thread.init(target: self, selector: #selector(runTask), object: nil)
return th
}()
override func viewDidLoad() {
super.viewDidLoad()
// 模拟第一次执行
DispatchQueue.main.asyncAfter(deadline: .now()+1.0) {
self.perform(#selector(self.otherTask), on: self.myThread, with: nil, waitUntilDone: false)
}
// 模拟第二次执行
DispatchQueue.main.asyncAfter(deadline: .now()+5.0) {
self.perform(#selector(self.aTask), on: self.myThread, with: nil, waitUntilDone: false)
}
}
@objc func runTask() -> Void {
print("run task")
// 增加一个 runloop
let loop = RunLoop.current
loop.add(NSMachPort.init(), forMode: .default)
loop.run()
}
@objc func otherTask() -> Void {
print("otherTask")
if self.myThread.isEqual(Thread.current) {
print("123")
// 如果需要退出线程,执行这一句
// Thread.exit()
}
}
@objc func aTask() ->Void {
print("aTask")
if self.myThread == Thread.current {
print("222")
}
}
写在最后
通过阅读前人的文章,再加上自己的验证,能够让自己加深对知识的印象,只是需要花费比较多的时间;目前来看自己在理论方面掌握的相对强一点,而在实际运用方面略有不足;在学习的过程中我了解到有很多第三方框架在实现的过程中都用到了 RunLoop 的知识,后面我会抽时间去阅读一些运用到多线程的框架的源码,比如 AFNetworking
、SDWebImage
甚至是它们的 Swift
版。
参考链接
- 深入理解RunLoop 作者:ibireme
- iOS刨根问底-深入理解RunLoop 作者:KenshinCui
- RunLoop 源码阅读 作者:ZenonHuang
- Run Loops
- iOS RunLoop基础和应用举例