RunLoop的概念
RunLoop实际上就是通过内部维护的一个事件循环来对事件或者消息进行管理的一个对象。
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
这种模型通常被称作Event Loop。Event Loop在很多系统和框架里都有实现。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
所以,RunLoop实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面Event Loop的逻辑。线程执行了这个函数后,就会一直处于这个函数内部“接受消息->等待->处理”的循环中,知道这个循环结束,比如quit的消息,函数返回。
Foundation框架中:NSRunloop
Core Foundation:CFRunloop
NSRunloop就是对CFRunloop的一个封装提供了一些面向对象的api。真正的核心还是CFRunloop。但是CFRunloop通过pthread_mutex互斥锁实现了线程安全,而NSThread并不是线程安全的
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
CFRunLoopRef loop = CFRunLoopGetCurrent();
CFRunLoopRef loop = CFRunLoopGetMain();
runloop并不能手动创建当你获取当前线程端runloop时,系统自动创建。
这里需要注意的是:主线程的RunLoop在应用启动的时候就会自动创建。做这件事儿的就是Main函数里边调用的UIApplicationMain() 函数,这个函数会给main Thread设置一个runloop。
RunLoop与线程的关系
线程和RunLoop之间是一一对应的,其关系是保存在一个全局的Dictionary里。线程刚创建时并没有RunLoop,如果你不主动获取,那它一直都不会有。RunLoop的创建是发生在第一次获取时,RunLoop的销毁是发生在线程结束时。你只能在一个线程的内部获取其RunLoop(主线程除外)。
正常情况下,我们让一个线程执行一个任务,执行完毕后线程就退出了。但实际中,我们可能需要线程随时处理事件,并不因为某一个任务处理完毕而退出。我们想让这个线程,有任务时处理任务,没任务时休息,避免占用资源,在消息到来时被唤醒。run loop是为了线程而生,没有线程,它就没有存在的必要
上述过程实际上就是一个事件循环,有任务就处理任务,处理完一个任务,如果还存在任务就继续处理,直到任务全部处理完,让这个线程进入休眠状态,等待消息来将它唤醒,但是休眠的这个期间并不代表跳出循环。
根据前边所述,每一个runloop都只对应一个线程,但是并不代表每一个线程都存在与其对应的一个runloop,因为只有当你需要一个runloop的时候,它才存在,如果你仅仅用一个线程单独的执行一个任务,并没有在当前线程获取runloop,那么这个runloop就不存在。
RunLoop对外的接口
在coreFoundation里关于runloop有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中CFRunLoopModeRef类并没有对外暴露,只是通过CFRunLoopRef的接口进行了封装。他们的关系如下:
一个Runloop包含若干个Mode,每个mode又包含若干个Source/Timer/Observer。每次调用RunLoop的主函数时,只能指定其中一个Mode这个Mode被称作CurrentMode。如果需要切换Mode,只能退runLoop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。
CFRunLoopSourceRef是事件产生的地方,source有两个版本:Source0和Source1。
Source0只包含一个回调(函数指针),它并不能主动触发事件。使用时,需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让其处理这个事件。
Source1包含了一个mach_port和一个回调,被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop的线程,其原理在下面会讲到。
CFRunLoopTimerRef是基于事件的触发器,他和NSTimer是toll-free bridged的,可以混用(即前者氏coreFoundation后者是cocoa foundation中得)。其包含一个事件长度和一个回调。当其加入到Runloop时,runloop会注册对应的事件点,当事件点到时,runloop会被唤醒以执行那个回调。
CFRunLoopObserverRef是观察者,每个Observer都包含一个回调,当runloop的状态发生变化时,观察者就能通过回调接受到这个变化,可以观测的时间点有以下几个:
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
};
上面的Source/Timer/Observer被统称为mode item,一个item可以被同时加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,则runloop会直接退出,不进入循环。
RunLoop中的mode
CFRunLoopMode和CFRunLoop的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name,例如@"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
这里有个概念叫CommonModes:一个mode可以将自己标记位Common属性(通常将其codename添加到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里去。
CFRunLoop对外暴露的管理Mode接口只有下面2个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode暴露的管理mode item的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
你只能通过mode name来操作内部的mode,当你传入一个新的mode name但runloop内部没有对应mode时,runloop会自动帮你创建对应的CFRunLoopModeRef。对于一个RunLoop来说,其内部的mode只能增加不能删除。
同时苹果还提供了一个操作Common标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作Common Items,或标记一个Mode为"Common"。使用时注意区分这个字符串和其他mode name。
每个runloop包含若干个mode,每个mode里又包含若干个source/timer/observer,我们通过CFRunLoopMode的结果提看看他们之间的关系。
从结构体中可以看出,mode下边有两个集合分别存储source0和source1,然后有两个数组来存储这个mode的timers和observers。
接下来分别说下source/timer/observer
CFRunLoopSource
source0只包含了一个回调,其实就是一个函数指针,当这个函数指针被调用的时候,就发出了一个source0类型的事件元,所以它并不能主动触发事件,使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
source1包含了一个mach_port和一个回调,mach_port是一个系统内部的端口,通过这个端口发送消息而产生的事件元就是source1类型,这种source能够主动唤醒runloop所在的线程。
CFRunLoopTimer
系统内的定时闹钟,NSTimer和PerformSEL方法实际上就是对CFRunloopTimer的封装。
NSTimer和CFRunloopTimer其实就是OC语言和C语言的转换,可以通过桥连接相互转换来混用。
CFRunLoopObserver
监听runloop状态,接收回调信息(常见于自动释放池创建销毁)
RunLoop的内部逻辑
实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
字面意思是“消息循环、运行循环”,runloop内部实际上就是一个do-while循环,它在循环监听着各种事件源、消息,对他们进行管理并分发给线程来执行。
通知观察者将要进入运行循环。
线程和 RunLoop 之间是一一对应的
通知观察者将要处理计时器。
通知观察者任何非基于端口的输入源即将触发。
触发任何准备触发的基于非端口的输入源。
如果基于端口的输入源准备就绪并等待触发,请立即处理该事件。转到第9步。
通知观察者线程即将睡眠。
将线程置于睡眠状态,直到发生以下事件之一:
事件到达基于端口的输入源。
计时器运行。
为运行循环设置的超时值到期。
运行循环被明确唤醒。
通知观察者线程被唤醒。
处理待处理事件。
如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第2步。
如果输入源被触发,则传递事件。
如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。
通知观察者运行循环已退出。
RunLoop的底层实现
RunLoop的核心是基于mach port的,其进入休眠时调用的函数是mach_msg();
那么什么是mach port?
轻量级的进程间通讯的一种方式
某个进程发消息可以发送到这个port上,
关于GCD
在gcd中和runloop唯一有关的地方就是在调用dispatch_get_main_queue()这个方法的时候,会向主线程的runloop发送消息。
相关连接:
https://blog.csdn.net/hherima/article/details/51746125【精】Runloop 深入浅出,综合解答
https://blog.ibireme.com/2015/05/18/runloop/深入理解RunLoop
http://www.cocoachina.com/ios/20170407/18998.htmlRunLoop 官方编程手册翻译
http://yangchao0033.github.io/blog/2016/01/18/runloop-5/RunLoop深度探究(五)
https://www.cnblogs.com/jiangzzz/p/5619512.htmliOS面试题之runloop
网友评论