准备工作
1. 什么是RunLoop
RunLoop是一个运行循环,也是一个对象,并且提供了入口函数,进行do while循环,保证运行程序不退出。
一个程序运行结束的标志性语句是return,在iOS应用的入口main函数中,return并执行了一个UIApplicationMain函数,如下:

既然已经
return了,为什么应用依然可以接收消息,处理消息呢?程序不应该到此结束吗?我们在代理AppDelegate的application:didFinishLaunchingWithOptions:方法中添加断点,并bt打印堆栈信息,探索到以下内容,如下:
由堆栈信息看出,程序的执行流程,首先
dyld进行应用程序加载,执行main函数,启动RunLoop,加载GCD………由此可见应用程序在启动过程中进行了一系列的初始化工作。同时可以确定,RunLoop来自CoreFoundation框架,CoreFoundation的部分源码是开源的,其中包括RunLoop。在其源码中,RunLoop Run的实现也确实是一个do while循环。如下:
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
我们知道RunLoop和线程有关系,并且提供了一套消息处理机制。在苹果官方的开发者文档Documentation Archive中搜索Thread内容,其中就包含了RunLoop的相关说明,如下:

这也说明了
Runloop和线程是息息相关的,这也是我们以下需要分析的。
2. RunLoop的作用
RunLoop的作用总结如下几点:
- 保持程序的
持续运行 - 处理
APP中的各种事件(触摸、定时器、performSelector) - 节省
cpu资源、提高程序的性能。
2.1 保持程序的持续运行
在main函数创建UIApplicationMain时启动了RunLoop,如果没有启动RunLoop,程序就会直接退出。
2.2 处理APP中的各种事件
在苹果的官方文档中,有这样一张图:

结合官方的说明,我们可以知道,
RunLoop是线程用于处理运行事件、处理响应事件的循环。这些事件包括port事件源、屏幕触摸事件、performSelector、timer等。
我们在写上层代码的时候很少接触到Runloop,因为Runloop封装的非常好。一些事件处理都运用到了RunLoop,下面通过案例来分析:
-
Timer
处理Timer事件,对应__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__,如下:
案例分析 -
performSelector
处理performSelector事件,也对应的__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__,如下:
案例分析 -
GCD
队列中处理事件,对应__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__,如下:
案例分析
通过上面的探索,我们可以发现这些事件的处理方法均以__CFRUNLOOP_IS_为开头的方式命名,查看源码,其会根据不同的事件,提供不同的响应方法,如下:
响应方法
source相关响应方法
事件处理回调方法总结:
-
block应用:__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ - 调用
timer:__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ - 响应
source0:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ - 响应
source1:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ -
GCD主队列:__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ -
observer源:__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
2.3 节省cpu资源、提高程序的性能
RunLoop能节省cpu资源,提高程序的性能,这点体现在哪呢?见如下案例:

从以上的案例可以发现,应用程序启动后,
保持运行状态,但是此时的cpu占用率一直是0%。我们知道,RunLoop实际上就是一个do while循环,我们如果开启一个循环会怎样呢?对比一下!
通过上图可以发现,
cpu占用一直很高,所以可以得出结论,RunLoop所提供的循环是和普通的循环是有区别的,有事需要处理才会运行,没有事则会休息!从而达到了节省cpu资源,提高程序性能的作用。那么这种功能是如何实现的,下面会分析。
3. RunLoop与线程的关系
RunLoop和线程是息息相关的,并且是一一对应的关系。那么他们的关系是如何建立的呢?
// 获取main RunLoop
NSLog(@"%@", CFRunLoopGetMain());
// 获取当前 RunLoop
NSLog(@"%@", CFRunLoopGetCurrent());
通常会通过上面的两种方式打印输出main RunLoop以及当前RunLoop。在CFRunLoop源码中查看其实现,如下:
//获取主线程的Runloop
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;
}
//获取当前线程的Runloop
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
根据上面的代码不难发现,获取RunLoop均是通过线程进行获取。那么线程和RunLoop的关系是如何建立的呢?这就需要解读_CFRunLoopGet0函数的实现源码,如下:


解读
_CFRunLoopGet0源码:
- 维护了一个
CFMutableDistionaryRef字典__CFRunLoops,字典默认为NULL; - 如果
CFRunLoops是空,则创建一个CFMutableDistionaryRef字典,并默认初始化主线程的RunLoop; - 将创建的
RunLoop放入到CFMutableDistionaryRef字典中,也就是放入__CFRunLoops中,以线程为key,RunLoop为value; - 在通过线程获取
RunLoop时,以key-value方式从字典中获取对应的RunLoop; - 如果
RunLoop为空,则创建一个newLoop,以线程为key,RunLoop为value,存储到__CFRunLoops中; - 返回线程对应的
RunLoop。
结论:主线程的RunLoop会默认被创建,而子线程的RunLoop是懒加载的,需要时才会创建,RunLoop和线程是一对一的关系,存储在一个字典中。
- 子线程
RunLoop案例分析
GFThread * gfThread = [[GFThread alloc] initWithBlock:^{
NSLog(@"running....");
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"helloc timer...%@", [NSThread currentThread]);
}];
}];
gfThread.name = @"Hello.thread";
[gfThread start];
GFThread是一个继承自NSThread的自定义线程,并重写了dealloc方法。案例中,在子线程中使用了一个NSTimer,运行这段代码会是什么结果呢?请往下看:

线程生命周期结束,但是
NSTimer的任务并没有执行。这是因为NSTimer需要依赖于RunLoop,主线程的RunLoop默认开启,而子线程的RunLoop是懒加载,需要手动开启。
对上面的案例进行修改,启动子线程的RunLoop。如下:

那么如何结束
NSTimer呢?首先我们需要理理清楚一个关系:线程和RunLoop一一对应,而NSTimer又依赖于RunLoop。根据这个思路可以做如下修改:
通过
外部变量可以控制线程,如果线程退出,对应的RunLoop也会停止运行,NSTimer又依赖于RunLoop,也自然不能运行。
4. RunLoop数据结构
RunLoop中涉及到5个重要的类,分别如下:
-
CFRunLoop-RunLoop对象 -
CFRunLoopMode-五种运行模式 -
CFRunLoopSource- 输入源/事件源,包括Source0和Source1 -
CFRunLoopTimer- 定时源,也就是NSTimer -
CFRunLoopObserver- 观察者,用来监听RunLoop
1. CFRunLoop
在底层RunLoop对象为CFRunLoop,NSRunLoop是OC层的封装。我们可以通过以下两种方式获取当前线程的RunLoop:
// c/c++
CFRunLoopRef lp = CFRunLoopGetCurrent();
// OC
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
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;
};
CRRunLoop中包括锁_lock、用于处理source1的唤醒端口_wakeUpport、关联的线程_pthread、当前模式_currentMode等。同时维护了一个set集合_modes。通过其结构体的数据,我们可以得出结论:RunLoop和mode是一对多的关系。同时包括_commonModes属性,commonMode是一个伪模式。
2. CFRunLoopMode
可以通过以下代码获取当前线程RunLoop的currentMode和mode列表:
CFRunLoopRef lp = CFRunLoopGetCurrent();
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(lp);
NSLog(@"mode == %@",mode);
CFArrayRef modeArray= CFRunLoopCopyAllModes(lp);
NSLog(@"modeArray == %@",modeArray);
运行上面的代码,查看运行结果,如下:

此时
currentMode是kCFRunLoopDefaultMode,而当前线程的RunLoop包括了三种mode,分别是:UITrackingRunLoopMode、GSEventReceiveRunLoopMode、kCFRunLoopDefaultMode。
3. 案例了解mode的切换
引入下面的案例,用于了解mode的切换过程,如下:

RunLoop添加Timer时放在了DefaultMode,程序正常情况下也运行在DefaultMode,但在滚动视图时,切换到了UITrackingMode,timer事件也不再触发。当停止滚动视图,又切回到了DefaultMode,timer恢复运行。
为什么model切换timer就停止运行?需要什么操作才能解决这样子的问题?请继续往下走。
首先查看model的定义如下:
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 */
};
__CFRunLoopMode源码定义中包括了4个set集合_sources0、_sources1、_observers、_timers,这四个集合也就是我们常说的事件(事务)。所以我们可以得出结论:CFRunLoopMode和sourses、timer、observer也是一对多的关系。
在Developer Document中搜索NSRunLoopMode可以找到,系统共维护了5种mode见下图:

-
kCFRunLoopDefaultMode默认的运行模式,通常主线程是在这个Mode下运行 -
UITrackingRunLoopMode界面跟踪Mode,用于ScrollView等视图,追踪触摸滑动,保证界面的滑动不受其他Mode的影响 -
UIInitializationRunLoopMode在刚启动App时进入的第一个Mode,启动完成后就不再使用 -
GSEventReceiveRunLoopMode接受系统时间的内部Mode,通常用不到 -
kCFRunLoopCommonModes是一个伪模式,可以在标记为CommonModes的模式下运行,RunLoop会自动将_commonModeItems里的source、observe、timer同步到具有标记的Mode里。
综上,可以得出以下关系图:

-
RunLoop与线程一对一 -
RunLoop与Mode一对多 -
Mode和source、timer、observer也是一对多
5. RunLoop事件处理机制
RunLoop处理APP中的各种事件(触摸、定时器、performSelector),也就是说block、timer、source0、source1、GCD、observer都需要依赖于RunLoop,那么这些事件是如何加入到RunLoop中的呢?底层又是如何去处理这些事件的呢?让我们继续往下探讨吧!!
5.1 添加事务
在源码中提供了一些事务(事件)添加方法,这些事务会添加到对应的mode中,如下:

5.1.1 block事务添加
当有block事务时,RunLoop会调用CFRunLoopPerformBlock方法,将block事务存储到对应的mode中,如下:

在此过程中首先进行
mode的判断处理,确定需要将事务放到哪个mode中,如果mode或者block为空,则释放;否则会创建一个block_item,该数据是一个链表结构,其存储了一个block和下一个节点的地址信息。
5.1.2 timer事务添加
当有timer事务时,RunLoop会调用CFRunLoopAddTimer方法,将timer事务存储到对应的mode中,如下:

进入
__CFRepositionTimerInMode方法,如下:
这里会对
mode进行判断,判断是否为commonModes,如果是会初始化_commonModeItems集合,并将timer事务添加到集合中。否则找到对应的mode,然后调用__CFRepositionTimerInMode方法,将timer添加到_timers集合中。
注意:CFRunLoopAddObserver、CFRunLoopAddSource流程类似,这里不详细说明。
5.2 RunLoop循环
在程序运行的入口处设置断点,我们可以发现系统会首先调用CFRunLoopRunSpecific方法,启动RunLoop。如下:

下面跟踪RunLoop处理流程。在源码中查找CFRunLoopRunSpecific的方法实现,如下:

在这里注册了两个
Observer,第一个Observer监视的事件是Entry(即将进入Loop),第二个Observer监视Exit(即将退出Loop)。
进入__CFRunLoopRun方法,方法实现如下:









循环状态是对
retVal进行控制,在循环过程中,会对状态进行判断,如,是否TimedOut、是否Stopped、是否Finished,来确定RunLoop是否需要销毁。此部分代码是
RunLoop的核心流程,关键点做了标注,总结下来,可以得出下面这个流程图:
5.3 事务处理
上面两节已经摸清楚事务(事件)的添加流程、RunLoop循环处理流程,下面重点分析RunLoop在循环过程中是如何处理事务的。
在上面的do while循环中,有处理事务的入口:__CFRunLoopDoBlocks、__CFRunLoopDoTimers、__CFRunLoopDoSources0、__CFRunLoopDoSource1。
5.3.1 处理block事务
__CFRunLoopDoBlocks源码实现,如下:


在分析
block事务添加过程时提到,block事务是以链表的形式存储的,这里进行处理事务时通过_next指针循环遍历所有的block事务。
block执行逻辑:
- 事务加入的
mode和当前RunLoop的mode相等 - 当前
mode是commonModes - 通过调用回调函数
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__执行任务
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void)) {
if (block) {
block();
}
asm __volatile__(""); // thwart tail-call optimization
}
5.3.2 处理timer事务
__CFRunLoopDoTimers源码实现如下:

此过程中,会从当前
mode的_timers中获取需要执行的timer事务,放入到数组timers中,然后在调用__CFRunLoopDoTimer方法执行timer。__CFRunLoopDoTimer实现原理如下:
在此流程中会对
Timer的状态进行判断,并调用函数__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__完成事务的执行。
__CFRunLoopDoSources0的处理流程不再详细介绍,最终会调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数。
5.4 总结流程图

6. RunLoop与AutoreleasePool
AutoreleasePool创建和释放
-
App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。 - 第一个
Observer监视的事件是Entry(即将进入Loop),其回调内会调用_objc_autoreleasePoolPush()创建自动释放池。其order是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
-第二个Observer监视了两个事件:BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush(),释放旧的池并创建新池;Exit(即将退出Loop) 时调用_objc_autoreleasePoolPop()来释放自动释放池。这个Observer的order是2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。 - 在主线程执行的代码,通常是写在诸如
事件回调、Timer回调内的。这些回调会被RunLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建Pool了。
总结:
-
AutoreleasePool创建是在一个RunLoop事件开始之前(push), -
AutoreleasePool释放是在一个RunLoop事件即将结束之前(pop)。 -
AutoreleasePool里的Autorelease对象的加入是在RunLoop事件中,AutoreleasePool里的Autorelease对象的释放是在AutoreleasePool释放时。
7. RunLoop总结
RunLoop是通过系统内部维护的循环进行事件、消息管理的一个对象。RunLoop实际上就是一个do...while循环,有任务时开始,无任务时休眠。本质是通过mach_msg()函数接收、发送消息。
-
RunLoop与线程的关系:-
RunLoop的作用就是来管理线程,当线程的RunLoop开启后,线程就会在执行完任务后,处于休眠状态,随时等待接受新的任务,不会退出。 - 只有主线程的
RunLoop是默认开启的,其他线程的RunLoop需要手动开启。所以当程序开启后,主线程一直运行,不会退出。
-
-
RunLoop中涉及到5个重要的类:-
CFRunLoop-RunLoop对象 -
CFRunLoopMode- 五种运行模式 -
CFRunLoopSource- 输入源/事件源,包括Source0和Source1 -
CFRunLoopTimer- 定时源,也就是NSTimer -
CFRunLoopObserve- 观察者,用来监听RunLoop
-
-
CFRunLoopMode-五种运行模式-
kCFRunLoopDefaultMode默认的运行模式,通常主线程是在这个Mode下运行 -
UITrackingRunLoopMode界面跟踪Mode,用于ScrollView等视图,追踪触摸滑动,保证界面的滑动不受其他Mode的影响 -
UIInitializationRunLoopMode在刚启动App时进入的第一个Mode,启动完成后就不再使用 -
GSEventReceiveRunLoopMode接受系统时间的内部Mode,通常用不到 -
kCFRunLoopCommonModes是一个伪模式,可以在标记为CommonModes的模式下运行,RunLoop会自动将_commonModeItems里的source、observe、timer同步到具有标记的Mode里。
-
-
CFRunLoopSource- 事件源-
Source1:基于mach_port和回调函数指针,也就是端口通讯,处理来自系统内核或其他进程的事件,比如点击手机屏幕 -
Source0:非基于Port的处理事件,也就是应用层事件(内部事件、APP负责管理的事件,UIEvent),包含一个回调函数指针,需要手动标记为待处理或者手动唤醒RunLoop,如performSelector、block等 - 例如:一个
APP在前台静止,用户点击APP界面,屏幕表面的时事件会先包装成Event告诉source1(基于mach_port),source1唤醒RunLoop将事件Event分发给source0,由source0来处理。
-
CFRunLooTimer- 定时源
就是NSTimer,在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(Timer是不准确的,因为RunLoop只负责分发源消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。-
CFRunLoopObserver- 观察者
用来监听时间点事件CFRunLoopActivity。-
KCFRunLoopEntery RunLoop准备启动 -
kCFRunLoopBeforeTimers RunLoop将要处理一些Timer相关的事件 -
kCFRunLoopBeforeSources RunLoop将要处理一些Source事件 -
kCFRunLoopBeforeWaiting RunLoop将要进行休眠状态,即将由用户状态切换内核态 -
kCFRunLoopAfterWaiting RunLoop被唤醒,即从内核态切换到用户态 -
kCFRunLoopExit RunLoop退出 -
kCfRunLoopAllActivitires监听所有状态
-
-
各数据结构之间的联系
-
RunLoop和线程是一对一的关系 -
RunLoop和RunLoopMode是一对多的关系 -
RunLoopMode和RunLoopSource是一对多的关系 -
RunLoopMode和RunLoopTimer是一对多的关系 -
RunLoopMode和RunLoopObserver是一对多的关系
-
为什么
main函数能够保持一直存在且不退出?
在main函数内容会调用UIApplication函数,而在UIAPPlicationMain内部会启动主线程的RunLoop,可以做到有消息处理,能够迅速从内核态到用户态的切换,立刻唤醒处理,而没有消息处理时,通过用户态到内核态的切换进入等待状态,避免资源的占用。因此main函数能够一直存在并且不退出。




