iOS底层学习——RunLoop实现原理
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循环。见下图:

我们知道RunLoop和线程有关系,并且提供了一套消息处理机制。在苹果官方的开发者文档Documentation Archive中搜索Thread内容,其中就包含了RunLoop的相关说明:

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

再结合官方的说明,我们可以知道,
RunLoop是线程用于处理运行事件、处理响应事件的循环。这些事件包括port事件源、屏幕触摸事件、performSelector、timer等。我们在上层
APP开发时貌似很少接触到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_为开头的方式命名,查看源码,其会根据不同的事件,提供不同的响应方法,见下图:
事件处理回调方法总结:
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__
-
-
节省
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均是通过线程进行获取。那么线程和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和Source1CFRunLoopTimer- 定时源,也就是NSTimerCFRunLoopObserver- 观察者,用来监听RunLoop
-
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是一个伪模式。 -
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。-
案例了解
mode的切换引入下面的案例,用于了解
mode的切换过程,见下图:
RunLoop添加Timer时放在了DefaultMode,程序正常情况下也运行在DefaultMode,但在滚动视图时,切换到了UITrackingMode,timer事件也不再触发。当停止滚动视图,又切回到了DefaultMode,timer恢复运行。如何解决这个问题呢?我们可以将
Timer添加到commonMode,为什么放在commonMode就不受视图滚动的影响了呢?下面会解答!
那么
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 */ };__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中的呢?底层又是如何去处理这些事件的呢?
1.添加事务
在源码中提供了一些事务(事件)添加方法,这些事务会添加到对应的mode中,见下图:

-
block事务添加当有
block事务时,RunLoop会调用CFRunLoopPerformBlock方法,将block事务存储到对应的mode中,见下图:
在此过程中首先进行
mode的判断处理,确定需要将事务放到哪个mode中,如果mode或者block为空,则释放;否则会创建一个block_item,该数据是一个链表结构,其存储了一个block和下一个节点的地址信息。 -
timer事务添加当有
timer事务时,RunLoop会调用CFRunLoopAddTimer方法,将block事务存储到对应的mode中,见下图:
这里会对mode进行判断,判断是否为commonModes,如果是会初始化_commonModeItems集合,并将timer事务添加到集合中。否则找到对应的mode,然后调用__CFRepositionTimerInMode方法,将timer添加到_timers集合中。
CFRunLoopAddObserver、CFRunLoopAddSource流程类似,这里不详细说明。
2.RunLoop循环
在程序运行的入口处设置断点,我们可以发现系统会首先调用CFRunLoopRunSpecific方法,启动RunLoop。见下图:

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

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

循环状态是对retVal进行控制,在循环过程中,会对状态进行判断,如,是否TimedOut、是否Stopped、是否Finished,来确定RunLoop是否需要销毁。
此部分代码是RunLoop的核心流程,关键点做了标注,总结下来,可以得出下面这个流程图:

3.事务处理
上面两节已经摸清楚事务(事件)的添加流程、RunLoop循环处理流程,下面重点分析RunLoop在循环过程中是如何处理事务的。
在上面的do while循环中,有处理事务的入口:__CFRunLoopDoBlocks、__CFRunLoopDoTimers、__CFRunLoopDoSources0、__CFRunLoopDoSource1。
-
处理
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 }
- 事务加入的
-
处理
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__函数。
总结可以得出以下流程图:

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和Source1CFRunLoopTimer- 定时源,也就是NSTimerCFRunLoopObserve- 观察者,用来监听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。KCFRunLoopEnteryRunLoop准备启动kCFRunLoopBeforeTimersRunLoop将要处理一些Timer相关的事件kCFRunLoopBeforeSourcesRunLoop将要处理一些Source事件kCFRunLoopBeforeWaitingRunLoop将要进行休眠状态,即将由用户状态切换内核态kCFRunLoopAfterWaitingRunLoop被唤醒,即从内核态切换到用户态kCFRunLoopExitRunLoop退出kCfRunLoopAllActivitires监听所有状态
-
各数据结构之间的联系
RunLoop和线程是一对一的关系RunLoop和RunLoopMode是一对多的关系RunLoopMode和RunLoopSource是一对多的关系RunLoopMode和RunLoopTimer是一对多的关系RunLoopMode和RunLoopObserver是一对多的关系
-
为什么
main函数能够保持一直存在且不退出?在
main函数内容会调用UIApplication函数,而在UIAPPlicationMain内部会启动主线程的RunLoop,可以做到有消息处理,能够迅速从内核态到用户态的切换,立刻唤醒处理,而没有消息处理时,通过用户态到内核态的切换进入等待状态,避免资源的占用。因此main函数能够一直存在并且不退出。
转载自:https://juejin.cn/post/7013559485461430286