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
和Source1
CFRunLoopTimer
- 定时源,也就是NSTimer
CFRunLoopObserver
- 观察者,用来监听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
和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
函数能够一直存在并且不退出。
转载自:https://juejin.cn/post/7013559485461430286