RunLoop-深入剖析
Posted 乌戈勒
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RunLoop-深入剖析相关的知识,希望对你有一定的参考价值。
前言
先来看下app开发中一个常见的现象:
一个应用开始运行以后放在那里,如果不对它进行任何操作,这个应用就像静止了一样,不会自发的有任何动作发生,但是如果我们点击界面上的一个按钮,这个时候就会有对应的按钮响应事件发生。
给我们的感觉就像应用一直处于随时待命的状态,在没人操作的时候它一直在休息,在让它干活的时候,它就能立刻响应。其实,这就是run loop的功劳。
这一篇文章主要总结一下runloop的理论,另外,想要知道我们开发过程中,能够用runloop做什么,可以参考我的另一篇文章。NSRunLoop-使用场景分析
一、RunLoop 的概念
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:
function loop()
initialize();
do
var message = get_next_message();
process_message(message);
while (message != quit);
这种模型通常被称作 Event Loop。
Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/ios 里的 RunLoop。
实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠,以避免资源占用,在有消息到来时立刻被唤醒。
所以,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。
线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
1、CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
2、NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
二、RunLoop 与线程的关系
1、线程任务有几种类型?
1.1、有些线程执行的任务是一条直线,起点到终点;
直线线程如简单的Hello World,运行打印完,它的生命周期便结束了,像昙花一现那样;
1.2、而另一些线程要干的活则是一个圆,不断循环,直到通过某种方式将它终止。
圆类型的如操作系统,一直运行直到你关机。
在IOS中,圆型的线程就是通过run loop不停的循环实现的。
2、线程和RunLoop的关系
run loop正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。
实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。
run loop是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop。
每个线程,包括程序的主线程main thread都有与之相应的run loop对象。
你可以通过 pthread_main_np() 或 [NSThread mainThread] 来获取主线程;
也可以通过 pthread_self() 或 [NSThread currentThread] 来获取当前线程。
CFRunLoop 是基于 pthread 来管理的。
苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。
CFRunLoopGetMain() 和 CFRunLoopGetCurrent()两个函数内部的逻辑大概如下:
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread)
OSSpinLockLock(&loopsLock);
if (!loopsDic)
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop)
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
OSSpinLockUnLock(&loopsLock);
return loop;
CFRunLoopRef CFRunLoopGetMain()
return _CFRunLoopGet(pthread_main_thread_np());
CFRunLoopRef CFRunLoopGetCurrent()
return _CFRunLoopGet(pthread_self());
- 分析:
从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。
线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。
RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。
你只能在一个线程的内部获取其 RunLoop(主线程除外)。
3、主线程的RunLoop默认是启动的
iOS的应用程序里面,程序启动后会有一个如下的main() 函数:
int main(int argc, char *argv[])
@autoreleasepool
return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class]));
- 分析:
重点看UIApplicationMain() 函数,这个方法会为主线程main thread 设置一个NSRunLoop 对象,这就解释了本文开始说的为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
- 主线程runloop和其它线程runloop的区别:
对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
4、如何获得当前线程的RunLoop
在任何一个Cocoa程序的线程中,都可以通过下面的API来获取到当前线程的run loop。
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
5、如何驱动一个RunLoop
Run loop的管理并不完全是自动的,我们仍必须设计线程代码以在适当的时候启动run loop并正确响应输入事件,当然前提是线程中需要用到run loop。
而且,我们还需要使用while/for语句来驱动run loop能够循环运行,下面的代码就成功驱动了一个run loop:
BOOL isRunning = NO;
do
isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]];
while (isRunning);
6、RunLoop的优点
一个run loop就是一个事件处理循环,用来不停的监听和处理输入事件并将其分配到对应的目标上进行处理。
如果仅仅是想实现这个功能,你可能会想一个简单的while循环不就可以实现了吗,用得着费老大劲来做个那么复杂的机制?显然,苹果的架构设计师不是吃干饭的,你想到的他们早就想过了。
首先,NSRunLoop是一种更加高明的消息处理模式,它就高明在对消息处理过程进行了更好的抽象和封装,这样才能是的你不用处理一些很琐碎很低层次的具体消息的处理,在NSRunLoop中每一个消息就被打包在input source输入源或者是timer source定时源(见后文)中了。
其次,也是很重要的一点,使用run loop可以使你的线程在有工作的时候工作,没有工作的时候休眠,这可以大大节省系统资源。
三、线程间的通信
1、概述
线程间通信和进程间通信从本质上讲是相似的。线程间通信就是在进程内的两个执行流之间进行数据的传递,就像两条并行的河流之间挖出了一道单向流动长沟,使得一条河流中的水可以流入另一条河流,物质得到了传递。
2、主要有两种方法
2.1、performSelect On The Thread
框架为我们提供了强制在某个线程中执行方法的途径,如果两个非主线程的线程需要相互间通信,可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了。
由于主线程比较特殊,所以框架直接提供了在主线程执行的方法。
@interface NSObject (NSThreadPerformAdditions)
//在主线程中执行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
//在指定线程中执行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
//在后台线程中执行操作
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
2.2、Mach Port
它的实质就是父线程创建一个NSMachPort对象,在创建子线程的时候以参数的方式将NSMachPort对象传递给子线程,这样子线程中就可以向这个传过来的NSMachPort对象发送消息;
如果想让父线程也可以向子线程发消息的话,那么子线程可以先向父线程发个特殊的消息,传过来的是自己创建的另一个 NSMachPort对象,这样父线程便持有了子线程创建的port对象了,可以向这个子线程的NSMachPort对象发送消息了。
当然各自的NSMachPort对象需要设置delegate,以及schdule到自己所在线程的RunLoop中,这样来了消息之后,处理NSMachPort消息的delegate方法会被调用,你就可以自己处理消息了。
四、RunLoop的相关知识点
1、CFRunLoopRef的构造
1.1、数据结构:
// mode数据结构
struct __CFRunLoopMode
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
;
// runloop数据结构
struct __CFRunLoop
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
;
分析:
_commonModes这个集合的作用:
一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。
每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
- 应用场景举例:
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。
这两个 Mode 都已经被标记为”Common”属性。
DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。
当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。
还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。
“commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。
1.2、RunLoop结构中相关类的关系
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source / Timer / Observer。
每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。
如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。
这样做主要是为了分隔开不同组的 Source / Timer / Observer,让其互不影响。
缩写的对应注释:
RunLoop -- 类CFRunLoopRef (包含若干个Mode类)
Mode -- 类CFRunLoopModeRef (包含若干个Source/Timer/Observer类)
Source -- 类CFRunLoopSourceRef
Timer -- 类CFRunLoopTimerRef
Observer -- 类CFRunLoopObserverRef
- runloop与mode的关系图:
2、CFRunLoopSourceRef类:事件来源
CFRunLoopSourceRef类是事件产生的地方。Source有两个版本:Source0 和 Source1。
主要相关的类有3个:
Port-Based Sources:与内核端口相关; Custom Input Sources:与自定义source相关; Cocoa Perform Selector Sources:与PerformSEL方法相关);
数据结构(source0/source1):
// source0 (manual): order(优先级),callout(回调函数)
CFRunLoopSource order =..., callout =...
// source1 (mach port):order(优先级),port:(端口), callout(回调函数)
CFRunLoopSource order = ..., port = ..., callout =...
1、Source0 是event事件,只包含了一个回调(函数指针),它并不能主动触发事件。
使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,
然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
2、Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。
这种 Source 能主动唤醒 RunLoop 的线程。
3、CFRunLoopTimerRef类:系统内“定时闹钟”
CFRunLoopTimerRef类是基于时间的触发器,其包含一个时间长度和一个回调(函数指针)。
当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
NSTimer和performSEL方法实际上是对CFRunloopTimerRef的封装;
runloop启动时设置的最大超时时间实际上是GCD的dispatch_source_t类型。
- 数据结构:
// Timer:interval:(闹钟间隔), tolerance:(延期时间容忍度),callout(回调函数)
CFRunLoopTimer firing =..., interval = ...,tolerance = ...,
next fire date = ...,callout = ...
- 创建和生效:
// 1、NSTimer:
// 创建一个定时器(需要手动加到runloop的mode中)
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// 默认已经添加到主线程的runLoop的DefaultMode中
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// 2、performSEL方法
// 内部会创建一个Timer到当前线程的runloop中
//(如果当前线程没runloop则方法无效;performSelector:onThread: 方法放到指定线程runloop中)
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
4、CFRunLoopObserverRef类:观察者
CFRunLoopObserverRef 是观察者,监听runloop状态,接收回调信息(常见于自动释放池创建销毁),每个 Observer 都包含了一个回调(函数指针),
当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
- 数据结构:
// Observer:order(优先级),ativity(监听状态),callout(回调函数)
CFRunLoopObserver order = ..., activities = ..., callout = ...
- 创建和添加:
// 第一个参数用于分配该observer对象的内存空间
// 第二个参数用以设置该observer监听什么状态
// 第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行
// 第四个参数用于设置该observer的优先级,一般为0
// 第五个参数用于设置该observer的回调函数
// 第六个参数observer的运行状态
CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities,
YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity)
// 执行代码
- 监听的状态:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity)
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
;
五、RunLoop 的内部逻辑
RunLoop从字面上看是运行循环的意思,这一点也不错,它确实就是一个循环的概念,或者准确的说是线程中的循环。
有些程序是一个圈,这个圈本质上就是这里的所谓的RunLoop,就是一个循环,只是这个循环里加入很多特性。
首先循环体的开始需要检测是否有需要处理的事件,如果有则去处理,如果没有则进入睡眠以节省CPU时间。
重点便是这个需要处理的事件。
在RunLoop中,需要处理的事件分两类,一种是输入源,一种是定时器,定时器好理解就是那些需要定时执行的操作,输 入源分三类:
1、performSelector源;
2、基于端口(Mach port)的源;
3、以及自定义的源。
编程的时候可以添加自己的源。
RunLoop以及不同事件源的结构:
根据苹果在文档里的说明,RunLoop 内部的逻辑大致如下:
1、内部逻辑关键在于两个判断点:是否睡眠?是否退出?
代码实现:
// 用DefaultMode启动
void CFRunLoopRun(void)
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle)
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle)
// 0.1 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
// 0.2 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
// 1.1 通知 Observers: RunLoop 即将进入 loop。(OB会创建释放池)
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
// 1.2 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled)
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do
// 2.1 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 2.2 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 2.3 RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 2.4 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime)
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
// 3.1 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。(OB会销毁释放池并建立新释放池)
if (!sourceHandledThisLoop)
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
// 3.2 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
// a. 一个基于 port 的Source1 的事件。
// b. 一个 Timer 到时间了
// c. RunLoop 自身的"超时时间到了"
// d. 被其他什么调用者"手动唤醒"
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort)
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
// 3.3 被唤醒,通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
// 4.0 收到消息,处理消息。
handle_msg:
// 4.1 如果消息是Timer类型,这个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer)
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
// 4.2 如果消息是dispatch到main_queue的block,执行block。
else if (msg_is_dispatch)
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
// 4.3 如果消息是Source1类型,这个 Source1 (基于port) 发出事件了,处理这个事件
else
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop)
mach_msg(reply, MACH_SEND_MSG, reply);
// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 5.1 如果处理事件完毕,启动Runloop时设置参数为一次性执行,设置while参数退出Runloop
if (sourceHandledThisLoop && stopAfterHandle)
retVal = kCFRunLoopRunHandledSource;
// 5.2 如果启动Runloop时设置的最大运转时间到期(超时),设置while参数退出Runloop
else if (timeout)
retVal = kCFRunLoopRunTimedOut;
// 5.3 如果启动Runloop被外部调用强制停止,设置while参数退出Runloop
else if (__CFRunLoopIsStopped(runloop))
retVal = kCFRunLoopRunStopped;
// 5.4 如果启动Runloop的modeItems为空(source/timer/observer一个都没有了),
// 设置while参数退出Runloop
else if (__CFRunLoopModeIsEmpty(runloop, currentMode))
retVal = kCFRunLoopRunFinished;
// 5.5 如果没超时,mode里没空,loop也没被停止,那继续loop,回到第2步循环。
while (retVal == 0);
// 6. 如果第6步判断后loop退出,通知 Observers: RunLoop 退出。--- (OB会销毁新释放池)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
- 分析:
可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
2、Runloop状态大致分为:
1、Entry:通知Observers(创建pool);
2、执行阶段:按顺序通知Observers并执行timer,source0;若有source1执行source1;
3、休眠阶段:利用mach_msg判断进入休眠,通知Observers(pool的销毁重建);
被消息唤醒通知Observers;
4、执行阶段:按消息类型处理事件;
5、判断退出条件:如果符合退出条件(一次性执行,超时,强制停止,modeItem为空)则退出,
否则回到第2阶段;
6、Exit:通知Observers(销毁pool)。
3、Runloop本质:mach port和mach_msg()
Mach是XNU的内核,进程、线程和虚拟内存等对象通过端口port发消息进行通信。
Runloop通过mach_msg() 函数发送消息,如果没有port消息,内核会将线程置于等待状态 mach_msg_trap() 。
如果有消息,判断消息类型,后处理事件,并通过modeItem的callback回调。
Runloop有两个关键判断点,一个是通过msg决定Runloop是否等待,一个是通过判断退出条件来决定Runloop是否循环。
以上是关于RunLoop-深入剖析的主要内容,如果未能解决你的问题,请参考以下文章