iOS开发 - 啰嗦讲解 Runloop

写在前面的

为什么要了解 RunLoop?如果你想成为一个高级iOS开发工程师,那这是你必须了解的东西,他能帮助你更好的理解底层实现的原理,可以利用它的特性做出一些高效又神奇的功能。RunLoop这个东西已经是在各路大神的Blog里面描述和详解过很多次的了,我把它翻出来再写一遍,一来是为了让自己温故而知新,二来会重点详细解读一下当初我理解时候遇到的难点,为初、中级想要进阶的iOS开发盆友排排坑。

本人写的东西不是很好(从小语文没学好),之前就懂的人看了肯定会觉得我很啰嗦(本人处女座,比较爱会啰嗦,不喜请跳过,我的写博文的贯彻的理念是:宁肯让大神们喷我啰嗦,也尽量让不熟悉的人少点晕厥),我之前初次理解这块的时候就想要别人越啰嗦越好,因为毕竟这块东西对于刚开始了解底层的小伙伴来说看起来会比较晕厥(不管你晕没晕,反正我当时是晕了)。如有大神路过,希望多多指点,共同学习。

总结:这是一篇可能会比较啰嗦的技术博文,我喜欢贴源代码,这样可以加深印象,鄙人难免有写得不好或不对的地方,希望指出,乐于接受意见。

RunLoop的概念及作用

从字面意义上来看可以简单的对它进行理解,Run就是跑,Loop就是圈,是的,这个就是对它最简单的解释——跑圈(这个是几乎每个Blog都是这么写的一个简单概念)。

开始我先上段代码:

int main() {
  printf("hello world!\n");
  return 1;

这是大家在初学C语言编程的时候最常见的main函数运行的一段代码,这里控制台会输出相应的字符串,之后有一个return 1,return后程序就停止运行了。

那么问题就来了,当我打开一个APP后,我要的是它可以随时响应我对他进行的各种操作,那前提肯定是整个APP会持续运行,只有持续运行我们才能监听和处理各种事件,那么在iOS中什么东西能够支撑APP持续运行呢,那就是我们的RunLoop了,就是这个东西让我们的APP能持续跑圈,保证线程不被销毁。

那怎么让我们的APP跑圈呢,那我再上一段代码:

int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

这段代码相信作为iOS开发的大家也会非常的眼熟,他就是每个工程中main.m文件中的main函数。大家可以看到main函数return了一个UIApplicationMain函数的调用,这个函数在程序正常运行时不会有返回值,只有在程序退出时UIApplicationMain才会返回。而这个UIApplicationMain函数开启了主线程的RunLoop(UIApplicationMain函数还做了其他很多事,关于其他详细的东西我在这里就不多说了,推荐这篇文章,有兴趣的可以看看),从而让我们的主线程不会被销毁,保证了程序的持续运行。

然而RunLoop除了让线程持续运行不被销毁以外,还会对线程性能做优化,这里涉及到一个模型叫做Event Loop,几乎每个写RunLoop的博文都会提到的这个东西,Event Loop在很多系统的框架里都有实现,在iOS中就是我们的RunLoop的实现,它的关键点在于:让线程有任务的时候干活儿,没任务的时候休眠,从而达到一个节省CPU资源,优化性能的目的。

总结:RunLoop的作用,这里先把其他大神总结出来的作用描述粘贴过来:

  1. 保持程序持续运行:例如程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的 RunLoop , RunLoop 保证主线程不会被销毁,也就保证了程序的持续运行;
  2. 处理 App 中的各种事件(比如:触摸事件,定时器事件,Selector事件等 );
  3. 节省CPU资源,优化程序性能:程序运行起来时,当什么操作都没有做的时候,RunLoop就通知系统,现在没有事情做,然后进行休息待命状态,这时系统就会将其资源释放出来去做其他的事情。当有事情做,也就是一有响应的时候RunLoop就会立马起来去做事情;

RunLoop与线程的关系

对于RunLoop和线程之间的关系,我先上一段苹果Core Foundation中的开源代码,源码地址在这里,先看下面我摘出来的片段(有些东西看着可能会比较烦,可以直接看我的注释):

//创建一个全局字典,用于保存线程对应的 RunLoop,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
//访问 loopsDic 时的锁
static CFLock_t loopsLock = CFLockInit; // should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//获取一个 pthread_t 对应的 CFRunLoopRef,此方法只能被 Foundation 框架调用
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
//如果传入的 pthread_t 为 kNilPthreadT (kNilPthreadT 的定义 static pthread_t kNilPthreadT = { nil, nil };) ,则将传入线程赋值为主线程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
// 第一次进入时,初始化全局字典,并先为主线程创建一个 RunLoop。
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, , 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);
}
//从全局字典中获取 pthread_t 对应的 RunLoop
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())) {
// 注册一个回调,回调方法为 __CFFinalizeRunLoop,当线程销毁时,顺便也销毁其对应的 RunLoop。
// 方法细节就不描述了,感兴趣的小伙伴可以我上面贴出的苹果开源代码查看
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if ( == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
} //在苹果 Foundation 框架中,我们熟悉的 [NSRunLoop mainRunLoop] 方法,据我臆测应该就会调用 Core Foundation 框架中的此函数
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
} //在苹果 Foundation 框架中,我们熟悉的 [NSRunLoop currentRunLoop] 方法,据我臆测应该就会调用 Core Foundation 框架中的此函数
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}

从上面的源码可以得出以下几点(主要看我的注释),

1.线程和RunLoop之间他们是一对一的关系,其关系保存在一个全局的一个字典中。

2.线程不会自动创建RunLoop,必须主动获取后才会被创建(啰嗦两句,上面我贴出来的方法传入的参数就是一个线程,只有把需要开启RunLoop的线程传入这个方法后,RunLoop才会被创建,如果需要加深印象理解,还是建议把上面的源码多看几遍)。

3.在对应线程结束时 RunLoop 会被销毁。

总结:

  1. 每条线程都有且只有一个与之对应的 RunLoop 对象(RunLoop 跑圈的原理就是一个 do-while 的阻塞线程的循环,因此不可能在同一线程中同时有两个RunLoop);
  2. RunLoop 在第一次获取时创建,在线程结束时会被销毁;只能在一个线程的内部获取其 RunLoop(主线程除外)。
  3. 主线程的 RunLoop 系统默认启动,子线程的 RunLoop 需要主动开启;

RunLoop相关类及其详解

在 CoreFoundation 里面关于RunLoop有五个类(其他先别管,多看几遍,混个眼熟,反正这五个类是真的很重要):

  • CFRunLoopRef

  • CFRunLoopModeRef

  • CFRunLoopSourceRef

  • CFRunLoopTimerRef

  • CFRunLoopObserverRef

这五个类之间的关系,我就把网上都用的这张图先贴过来:

iOS开发 - 啰嗦讲解 Runloop

简单的描述一下这张图(又开始啰嗦了),这张图其实把RunLoop这重要的几个类的结构表述的比较清楚了,蓝色底的框框,就相当于是我们的RunLoop,五个类中的第一个,绿色底的框框就是Mode,五个类中的第二个。这里画的在RunLoop中有两个Mode,其实是可以有多个Mode,这个图的后面可以加个省略号。在每个Mode的框里又有三个黄色的框,这三个框对应的就是这五个类中后面三个类了。

CFRunLoopRef 和 CFRunLoopModeRef

刚刚简单的描述了一下他们的关系,但是第一次接触他们的小伙伴可能还是会有一些懵逼。别着急,我先 简单 的一一描述一下这五个类,先看前两个类,我们慢慢用源代码解释这一切:

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;
}; struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[];
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 */
};

以上是完整的 CFRunLoop 和 CFRunLoopMode 的结构体源码(太长了我的妈,用不着看完),下面我精简一下,把重要的留下,看如下代码(可以仔细看一下,加深印象):

// RunLoopMode 数据结构
struct __CFRunLoopMode {
CFStringRef _name; // Mode 名字, 唯一的标识,例如 kCFRunLoopDefaultMode
CFMutableSetRef _sources0; // Set<CFRunLoopSourceRef> source0 集合
CFMutableSetRef _sources1; // Set<CFRunLoopSourceRef> source1 集合
CFMutableArrayRef _observers; // Array<CFRunLoopObserverRef> observer 数组
CFMutableArrayRef _timers; // Array<CFRunLoopTimerRef> timer 数组
...
};
// RunLoop 数据结构
struct __CFRunLoop {
__CFPort _wakeUpPort; // 用来唤醒runLoop的端口,接收消息,执行CFRunLoopWakeUp方法
CFMutableSetRef _commonModes; // Set<CFStringRef> 标记为 Common 的 Mode 的集合
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer> commonMode 的 items 集合
CFRunLoopModeRef _currentMode; // Current Runloop Mode. RunLoop 当前运行的 Mode
CFMutableSetRef _modes; // Set<CFRunLoopModeRef> Mode 的集合
...
};

上面是精简出来比较关键的 RunLoop 和 RunLoopMode 的结构体,从上面源码可以看出:

一个 RunLoop 对象有一个用来被唤醒的端口 _wakeUpPort,一个当前运行的 mode 叫 _currentMode,以及若干个 _modes_commonModes_commonModeItems(commonModes这2个东西后面详细讲)。runLoop 有很多 mode,即 _modes,但是只有一个 _currentMode,RunLoop 一次只能运行在一个 mode 下,如果需要切换 Mode,只能退出 Loop,不可能在多个 Mode 下同时运行(这是iOS运行流畅的原因之一)。

从 runLoopMode 的组成可以看出来:mode管理了所有的事件(Source/Timer/Observer 被称为 Mode Item),而 RunLoop 管理着若干个 mode。

这两个结构体中,已经涉及到了我们的所有五个类了,关于他们的关系我后面会详细说,这里简单的看看,对他们有个印象,混个脸熟,先来看 CFRunLoopSourceRef

CFRunLoopSourceRef

在我 RunLoopMode 数据结构代码中可以看到这两个东西 CFMutableSetRef _source0 和 CFMutableSetRef _source1,首先这两个东西是 Set(集合),集合中存放的是一堆数据结构(这里就可以对应上面蓝色底那张图来看,这是那种图图里面的Source集合的部分),那这个 source 到底是个什么东西呢,在 RunLoopMode 结构体的注释中我也写了,他们其实也是一个数据结构 CFRunLoopSourceRef。那 CFRunLoopSourceRef 结构又是怎样的呢,我们再来看下面它的结构代码:

struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
  CFRunLoopSourceContext version0; /* source0的数据结构 */
CFRunLoopSourceContext1 version1; /* source1的数据结构 */
} _context;
}; //source0
typedef struct {
CFIndex version; // 版本号,用来区分是source1还是source0
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; //source1
typedef struct {
CFIndex version; // 版本号,用来区分是source1还是source0
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;

上面代码贴出来了三个数据结构,其他多余的别看,光看我注释的部分就行,其中第一个数据结构 __CFRunLoopSource 包含一个 _context 成员,他的类型是 CFRunLoopSourceContext 或者是 CFRunLoopSourceContext1,也就是后面两个数据结构。

大家可以从我重点看我注释的行 CFRunLoopSourceContext(其实就是source0的数据结构)和 CFRunLoopSourceContext1(source1) 的区别就在于 CFRunLoopSourceContext1(source1) 多了一个 mach_port_t 接收消息的端口。mach_port_t 这又是个什么玩意儿,这里暂时不用管,可以简单的啰嗦两句,mach是iOS系统内核的心脏,他管理着处理器的资源,关于它的一些结构和原理,我以后会写一篇文章来描述它的结构和工作原理,现在我还是把话收回来说主题,不走远了。

这里简单总结一下:

  • CFRunLoopSourceRef 是事件产生的地方;
  • 这个 CFRunLoopSourceRef 有两个版本就是 source0 和 source1;
  • source0只包含一个回调(函数指针),不能主动出发事件,需要 CFRunLoopSourceSignal(source) 将 Source 标记为待处理,CFRunLoopWakeUp(runloop) 唤醒 RunLoop,让其处理事件
  • source1包含 mach_port 和一个回调(函数指针),用于通过内核和其它线程相互发送消息,能主动唤醒 RunLoop。

CFRunLoopObserver

这个东西从名字上来看是观察者,我们先来看看他的结构体:

struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; //observer对应的runLoop
CFIndex _rlCount; //observer当前监测的runLoop数量
CFOptionFlags _activities; //observer观测runLoop的状态,枚举类型
CFIndex _order; //CFRunLoopMode中是数组形式存储的observer,_order就是他在数组中的位置
CFRunLoopObserverCallBack _callout; //observer观察者的函数回调
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};

CFRunLoopObserver 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态(CFOptionFlags _activities)发生变化时,观察者就能通过回调函数接收到状态的变化。那么 RunLoop 的状态(CFOptionFlags)又有哪些呢:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << ), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << ), // runLoop即将处理 Timers
kCFRunLoopBeforeSources = (1UL << ), // runLoop即将处理 Sources
kCFRunLoopBeforeWaiting = (1UL << ), // runLoop即将进入休眠
kCFRunLoopAfterWaiting = (1UL << ), // runLoop刚从休眠中唤醒
kCFRunLoopExit = (1UL << ), // 即将退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

这些就是 RunLoop 的状态们, 我们通过添加 CFRunLoopObserver 就能对其状态进行监听。在实际应用中,通过监听的结果我们就能在更新某个状态的时候做一些事情。

CFRunLoopTimer

还是一样,先来看结构:

struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; //所在的RunLoop
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; //函数回调
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

看中文注释就行了,这个 CFRunLoopTimer 就跟我们平时用的 NSTimer 是一个东西,它是一个基于时间的触发器,包含了时间长度和回调函数,当 timer 加入 RunLoop 后,RunLoop 会注册对应的时间点,在时间点上,RunLoop 会被唤醒执行 timer 中的函数回调。

到这里关于 RunLoop 的五个类已经简单的描述完了(我又要开始啰嗦了),在讲五个类之间的关系前,我还要先说另一个概念 RunLoopMode 的 Mode(这里我避免大家把 RunLoopMode 的 Mode 和后面要讲的 commonMode 搞混了,我在这里把他看做 RunLoopMode 的“名字”,下面再详细说明一下这个东西)

CFStringRef _name

(这一部分特别容易搅晕,我尽量多啰嗦一点,有不清楚的地方可以多看几遍)回过头来看 __CFRunLoopMode 结构体,这里面的第一个成员是 CFStringRef _name(这是 RunLoopMode 的唯一标识(“名字”),他的类型是 Core Foundation 框架中的 String),RunLoopMode 都是通过“名字”来创建的,你传入 Mode 的“名字” RunLoop 会给你创建对应的 RunLoopMode,那 RunLoopMode 的“名字”又有哪些,这个“名字”主要有以下五种(为什么说主要有这五种,因为在APP的主线启动时对应的 RunLoop 会默认创建这五种,后面我会进行验证,当然肯定不止五种,这里可以看到更多的苹果内部的 RunLoopMode 的“名字”):

kCFRunLoopDefaultMode //默认模式,通常主线程在这个模式下运行

UITrackingRunLoopMode //界面跟踪Mode,用于追踪Scrollview触摸滑动时的状态。

kCFRunLoopCommonModes //占位符,带有Common标记的字符串,比较特殊的一个mode;

UIInitializationRunLoopMode //刚启动App时进入的第一个Mode,启动后不在使用。

GSEventReceiveRunLoop //内部Mode,接收系事件。

在这里我建了个工程(我本来是想找苹果 Darwin 源码来看 RunLoop 在启动时候的情况的,但是可能因为太笨了,死活没找到,那就笨人用笨办法来验证吧),用来验证 RunLoop 创建后它的 RunLoopMode 的情况。首先我再工程的 ViewController 的 ViewDidLoad 中写了如下代码:

iOS开发 - 啰嗦讲解 Runloop

这段代码就是获取了当前主线程的 RunLoop ,并且输出他的详情,在运行后,我在控制台得到了一个特别长(特别特别长)的结果(感兴趣的朋友可以操作一下,再用json格式化一下),我这里就放几个关键的截图,用来验证APP启动后,主线程 RunLoop 中的 RunLoopMode 的初始化情况:

iOS开发 - 啰嗦讲解 Runloop

这个是 CFRunLoopRef 结构体中 modes 成员的 description 的开头部分,我们可以看到他的数量是5,我们再分别看他里面的成员:

iOS开发 - 啰嗦讲解 Runloop

iOS开发 - 啰嗦讲解 Runloop

iOS开发 - 啰嗦讲解 Runloop

iOS开发 - 啰嗦讲解 Runloop

iOS开发 - 啰嗦讲解 Runloop

至此我们可以验证出在主线程中(注意子线程并不会被默认创建这5个)这5个 RunLoopMode 被苹果默认创建。在这5个 RunLoopMode 中有一个 Mode 比较特殊,那就是 kCFRunLoopCommonModes。如果操作打印了 RunLoop 详情的朋友可能会发现他与其他四个 Mode 都不同,我先把它的形态贴出来:

iOS开发 - 啰嗦讲解 Runloop

先补充一个概念,Source/Timer/Observer(就是我们介绍的五个类中的后三个) 被统为 Mode Item,如果一个 Mode 中没有一个 Item,而 RunLoop 又在这个 Mode 下运行,那么这个 RunLoop 将会退出,不会进入循环。

从上面的结构可以看到它的 Source/Timer/Observer 全部为空,显然他不是一个正常 Mode,所以在上面对他的注释中写到的,他是一个占位符,没有实际作用(具体作用我后面会详细说到,先混个脸熟)。

Common Modes

苹果官方文档里找到的一个概念名词,这个东西容易被搅晕,很多Blog里都没有说得很清楚,更有的说的直接就是错的。Common Modes 先看名字,前面是一个 “Common” 后面是一个 “Modes”,简单的理解为被贴了“Common”标签的 Mode 们的集合(因为他是 Modes,一个复数),而苹果官方跟这个集合起了个名字就叫 “Common Modes”。但是实际上 Mode 的结构体里面并没有地方让你贴这个“Common”标签,那我怎么知道 Mode 是其中一个 Common Modes,我们倒回去看结构体 CFRunLoopRef (可以翻到上面再看看他的结构)中的 _commonModes 成员,它是一个 CFMutableSetRef 类型的,这个集合中存的是 Mode 的“名字”,而此集合就可以理解为是 Common Modes 的名单而值得注意的是 Common Modes 是保存在 RunLoop 中的,而不是在 Mode 中。

在看 CFRunLoopRef 结构体的时候肯定还会看到一个叫 _commonModeItems 的成员,它也是一个集合,它里面存放的是 Mode Item(Source/Timer/Observer)。_commonModes 和 _commonModeItems 两个集合是相互有关联的,他们有这样的关系:

  1. _commonModes 集合中添加了一个 Mode,那么这个新添加的 Mode 会把 _commonModeItems 中所有的 Mode Item(Source/Timer/Observer)添加到自己当中。
  2. _commonModeItems 集合中添加了一个 Mode Item(Source/Timer/Observer),那么这个 Item 将会被添加到 _commonModes 集合中所有的 Mode 中。

这个东西说起来比较抽象,还是把源码贴上来,给大家加深印象,先看第一个关系:

//向 runLoop 的 commonModes 添加一个 mode
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
__CFRunLoopLock(rl);
// 判断 modeName 是否在_commonModes 中,如果已经存在,else中不做任何处理
if (!CFSetContainsValue(rl->_commonModes, modeName)) {
// 拷贝 runloop 的 _commonModeItems 集合
CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
//往 _commonModes 中添加 mode 的 名字
CFSetAddValue(rl->_commonModes, modeName);
// 如果items 存在
if (NULL != set) {
CFTypeRef context[] = {rl, modeName};
/* add all common-modes items to new mode */
//把从 _commonModeItems 拷贝的集合中的 mode item 们添加到这个 mode 中
CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
CFRelease(set);
}
} else {
}
__CFRunLoopUnlock(rl);
} // 把一个 mode item 添加到指定的 mode 中
static void __CFRunLoopAddItemsToCommonMode(const void *value, void *ctx) {
CFTypeRef item = (CFTypeRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[]);
CFStringRef modeName = (CFStringRef)(((CFTypeRef *)ctx)[]);
// 判断 item 具体是哪种类型,然后进行添加
if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
}
}

代码略长,看我注释就够了,再把上面的总结粘贴到这里:当 _commonModes 集合中添加了一个 Mode,那么这个新添加的 Mode 会把 _commonModeItems 中所有的 Mode Item(Source/Timer/Observer)添加到自己当中。

下面再来看第二个关系的源码:

//往指定的 RunLoop 中的 Mode 中添加 source
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rls)) return;
Boolean doVer0Callout = false;
__CFRunLoopLock(rl);
//如果传入需要添加的 mode 名字为 kCFRunLoopCommonModes
if (modeName == kCFRunLoopCommonModes) {
//拷贝 _commonModes 拿到 Common Modes 的名单
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
//如果 _commonModeItems 为空就创建
if (NULL == rl->_commonModeItems) {
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, , &kCFTypeSetCallBacks);
}
//先把 source 添加到 _commonModeItems 中
CFSetAddValue(rl->_commonModeItems, rls);
if (NULL != set) {
CFTypeRef context[] = {rl, rls};
/* add new item to all common-modes */
//再按 Common Modes 的名单挨个添加该 source
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
//不是往 _commonModeItems 添加 item 这里就省略了
...
}
}

只看注释就好,代码混个脸熟,这样印象深刻,代码的总结我再粘贴一遍:当 _commonModeItems 集合中添加了一个 Mode Item(Source/Timer/Observer),那么这个 Item 将会被添加到 _commonModes 集合中所有的 Mode 中。

在APP主线程中对应的 RunLoop 中,有两个 Mode 被放到了 _commonModes 中,它们是 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode(仅限主线程,我们自己创建的子线程只有 kCFRunLoopDefaultMode)。放下面一张图来点直观的印象(看最右边一列,看哪些是Part of common modes):

iOS开发 - 啰嗦讲解 Runloop

kCFRunLoopDefaultMode 是 App 平时所处的状态,UITrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。

说了这么多,那这个 Common Modes 到底是干嘛的,哪些场景会用到。我这里还是把那个经典例子拿出来说一下,当你创建一个 Timer 并加到 kCFRunLoopDefaultMode 时(Timer必须要加入 RunLoop 中才会跑起来),Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 的 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

但是往往你会想要 Timer 不管TableView是否在滚动都能回调方法,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到的 RunLoop 的 _commonModeItems 中,(再次贴出刚才总结)当 _commonModeItems 集合中添加了一个 Mode Item(Source/Timer/Observer),那么这个 Item 将会被添加到 _commonModes 集合中所有的 Mode 中(主线程 RunLoop 的 Common Modes 中有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode ),那么这个经典问题就被愉快的解决了。

关于 Common Modes 最后再啰嗦补充一下,要想将某个 Mode 变成 Common Modes CoreFoundation 框架提供的方法是:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);

而如果要将某个 Mode Item(Source/Timer/Observer)加入 RunLoop 的 _commonModeItems 中,可以调用  框架提供的方法:

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef modeName);

其中 modeName 参数传 kCFRunLoopCommonModes 即可,或者是调用 Foundation 框架中的方法

- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
- (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;

其中 mode 参数传 NSRunLoopCommonModes 即可。

RunLoop的内部逻辑

他的内部逻辑各种大牛也详细的阐述过,因为源代码太长了,有几百行,我这里就不一比一的贴出来了,只贴出大致的重点逻辑代码,这里涉及到 RunLoop 内部逻辑的函数主要有三个:

第一个函数,我们指定一个 Mode 来运行 RunLoop 会调用以下方法:

/**
* 根据指定运行模式 mode 运行 RunLoop
*/
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

第二个函数,会调用上面代码中的 CFRunLoopRunSpecific 方法,它的返回值和上面的函数是一样的,是一个 SInt32 类型的值,根据返回值,来决定 RunLoop 的运行状况:

//在指定的 mode 下,运行指定的 runLoop
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
// 根据rl,modeName 获取指定的 currentMode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); // 1. 如果当前 mode 不存在,或者当前 Mode 中没有 Mode Item(source/timer/observer),即返回 kCFRunLoopRunFinished
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
// 声明一个标识did,默认false
Boolean did = false;
// did 为 false,返回 kCFRunLoopRunFinished
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
} // 初始化一个返回结果,值为kCFRunLoopRunFinished
int32_t result = kCFRunLoopRunFinished; // 2.通知 observers 即将开始循环
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 运行 RunLoop 主体
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // 3.通知 observers 即将退出循环runLoop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}

此段代码省去了这个方法的细节部分,提炼出来了重点的三个部分即注释中的1,2,3:

  1. 如果当前 mode 不存在,或者当前 Mode 中没有 Mode Item(source/timer/observer),即函数返回 kCFRunLoopRunFinished
  2. 通知 observers 即将开始循环
  3. 通知 observers 即将退出循环 RunLoop

第三个函数,这个函数是 RunLoop 的主体函数,处理了 RunLoop 整个生命周期的所有逻辑,代码原版有几百行,这里省去了大部分,从它的核心 do-while 循环开始的,可以主要看注释(如果觉得太长的可以跳过看后面的“大众”总结图):

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

  ...   // 声明一个标识,默认true,用于执行消息处理
Boolean didDispatchPortLastTime = true;
// 声明一个返回值,用于最后的结果返回
int32_t retVal = ; // do..while循环主体,处理runLoop的逻辑
do { // 1. RunLoop 即将处理 Timers, 通知 observers
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 2. RunLoop 即将处理 Sources,通知 observers
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // 3. RunLoop 开始处理 Source0事件
// sourceHandledThisLoop 是否处理完Source0事件
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); if (sourceHandledThisLoop) {
// 处理完Source0之后的回调
__CFRunLoopDoBlocks(rl, rlm);
} // 处理完source0事件,且没有超时 poll 为false,
// 没有处理完source0 事件,或者超时,为true
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // didDispatchPortLastTime 初始化为true,即第一次循环的时候不会走if方法,
// 4. 消息处理,source1 事件,此处跳过休眠,直接到下面代码的第8步
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
// 从消息缓冲区获取消息
msg = (mach_msg_header_t *)msg_buffer;
// dispatchPort收到消息,立刻去处理
// dispatchPort 主线程接收消息的端口
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, , &voucherState, NULL)) {
// 收到消息,跳过下面休眠,goto第8步处理消息
goto handle_msg;
}
}
// didDispatchPortLastTime 设置为false,以便进行消息处理
didDispatchPortLastTime = false; // 5. 通知 observers runLoop即将休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); // runLoop 休眠
__CFRunLoopSetSleeping(rl); // 6.线程进入休眠, 直到被下面某一个事件唤醒:
// a. 基于 port 的 Source1 的事件
// b. Timer 到时间了
// c. RunLoop 启动时设置的最大超时时间到了
// d. 被手动唤醒
do {
// 从消息缓冲区获取消息
msg = (mach_msg_header_t *)msg_buffer;
// 内部调用 mach_msg() 等待接受 waitSet 的消息
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (); // 设置rl不再等待唤醒
__CFRunLoopSetIgnoreWakeUps(rl);
// runloop 醒来
__CFRunLoopUnsetSleeping(rl); // 7. 已被唤醒,通知observers
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 8. 处理消息
handle_msg:;
// 设置rl不再等待唤醒
__CFRunLoopSetIgnoreWakeUps(rl); // 判断 livePort
// 8.1 如果不存在
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// 8.2 如果是唤醒rl的端口,回到第1步
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
ResetEvent(rl->_wakeUpPort);
}
// 定时器事件__CFRunLoopDoTimers
// 8.3 如果是定时器的端口
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
// 处理定时器事件
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
}
// 8.4. 如果端口是主线程的端口,直接处理
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {
// 8.5. 除上述4点之外的端口
CFRUNLOOP_WAKEUP_FOR_SOURCE(); // 从端口收到的消息事件,为source1事件
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort); if (rls) { mach_msg_header_t *reply = NULL;
// 处理source1 事件
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
// 消息处理,
// message.h中,以后有时间会再研究一下
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, , MACH_PORT_NULL, , MACH_PORT_NULL);
} } }
// 9. 判断是否要退出 RunLoop
if (sourceHandledThisLoop && stopAfterHandle) {
// 9.1 如果事件处理完就退出,并且source处理完成
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 9.2 超时退出
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 9.3 调用 CFRunLoopStop() 退出
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
// 9.4 runLoopMode 状态停止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// 9.5 Mode Item(source/timer/observer) 一个都没有了
retVal = kCFRunLoopRunFinished;
}
// 上述几种情况,会跳出do..while循环,
// 除此之外,继续循环
} while ( == retVal);
return retVal;
}

从上面代码总结来看,我们的 RunLoop 其实就是一个 do-while 循环,在这个循环的过程中不断的处理各种消息,直到超时或者被手动停止,该函数才会返回。下面这张在网上随处可见的总结图其实对 RunLoop 的整个过程描述的非常清楚了,下面上图先:

iOS开发 - 啰嗦讲解 Runloop

(备注:左边黄色的地方,“source0 (port) ”改为”source1 (port)”)

看图说话,总结而论,在__CFRunLoopRun()中,先初始化RunLoop超时机制,然后进入do-while循环,循环内,处理source0,看情况决定是否真正进入休眠,唤醒后(或者根本没休眠),查询是否有 Timer 需要执行、是否有主线程 Port 消息需要转发、是否有 source1 需要执行,在关键的步骤上都会通知 observer 有状态更新,而后根据参数和执行情况更新返回值,最后根据返回值确定是否继续循环。

什么时候使用RunLoop

说了那么多关于 RunLoop 的东西, 那么平时什么时候会用到它呢?用它又可以干些什么神奇的事儿呢?先来看看我们平时最常用到的:

NSTimer

经常用到的定时器,这个跟上面说到的 CFRunLoopTimerRef 他们是 Toll-Free Bridge (免费桥,指 Core Foundation 对象与 Objective-C 对象之间的一种转换,这种转换不需要使用额外的 CPU 资源,可以简单的理解为他们骨子里就是同一种东西),timer 必须要注册到一个活的 RunLoop 中才能有效的回调。

PerformSelecter

这个方法平时我们也经常用到,然而这个方法有带不同参数的写法,这里就举个例子,比如我们调用 NSObject 的 performSelecter:afterDelay: 方法,它的原理是在内部创建了一个 Timer 并添加到当前线程的 RunLoop 中的 kCFRunLoopDefaultMode 里(performSelecter 的方法不都是为 RunLoop 创建 Timer,不要误解了),既然是 Timer ,那么这里就有两个问题,第一个如果这个 performSelecter 运行在一个没有开启 RunLoop 的子线程中,那么这个方法就会失效;第二个问题,如果 performSelecter 运行在主线程中,而 afterDelay 设置了一个时间,而时间点到时,如果用户正在滑动 TableView ,那么它并不会准时执行方法。

GCD相关

平时写多线程最常用到的就是GCD了,上面说到的 Timer 其实他的内部也是通过 dispatch_source_t 来实现的,而调用  dispatch_async(dispatch_get_main_queue(), block) 方法时,其内部实现是会唤醒 RunLoop ,从消息中得到 block,并回调(仅限于主线程,子线程依然会用 GCD 自己的 libDispatch 来处理)。

界面更新

所有其他大牛的博客里都说到了这个东西,我也简单说一下,苹果更新UI的逻辑用到了 RunLoop 中的 Observer,CA注册了对 RunLoop 的监听,Observer 监听了 BeforeWaiting(即将进入休眠) 事件,并在 BeforeWaiting 的时候进行了 UI 的更新。

上面说到了平时开发中 RunLoop 的实际体现,不过上面涉及到的很多时候我们都只是用到,而并可能没有感受到它真正的存在,也没有说到真正要手动开启一个 RunLoop 的实例,那么哪些时候需要我们手动开启一个 RunLoop 呢?

  1. 使用端口或自定义输入源来和其他线程通信
  2. 在子线程中使用定时器
  3. 使线程周期性工作

上面这些应用,这里暂时不讲了,也写了这么长了 ,此篇文章主要讲一下 RunLoop 的原理和概念,后面我会专门再写一篇文章来讲 RunLoop 的应用。本人第一篇博文,肯定有写得不好的地方,也有说得不清楚的地方,后面会尽量完善...

————————————————

本文参考:深入理解RunLoopRunLoop个人小结

上一篇:解决ftp客户端连接验证报错Server sent passive reply with unroutable address. Using server address instead


下一篇:《基于Node.js实现简易聊天室系列之详细设计》