准备工作
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
里注册了两个Observe
r,其回调都是_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)
。 -
AutoreleasePoo
l里的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
函数能够一直存在并且不退出。