RunLoop
简介
RunLoop在OS X/ iOS中一项比较基础的知识点,虽然基础,但是十分重要。它与线程息息相关,是用于处理到来事件的循环处理机制,可以理解为消息队列,当有事件需要处理的时候,RunLoop会使得线程在持续运行状态,没有任务的时候会让线程进入休眠状态。每一条线程都有一条与之对应的RunLoop(但是不仅限于一条,可以在RunLoop内嵌套另一个RunLoop)。
本文尝试讲解RunLoop的一些原理方面的知识,同时会介绍几个RunLoop的具体应用案例,基于RunLoop在cocoa层的API(NSRunLoop)网上有许多参考资料,翻阅官方reference也能中得到,在这里就不花篇幅介绍了。
RunLoop结构
RunLoop的实现可以用伪代码表示为:
int retVal = 1
fun __CFRunLoopRun(CFRonLoopRef rl) {
var msg
do {
msg = get_next_msg
if (msg) {
rl.wakeUp()
handle(msg)
}else {
rl.sleep()
}
retVaule = msg==quit || msg==timeout 0 : 1
}while (retVal == 1)
}
这个模型被称为Event Loop
,也可以理解成消息队列。RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
在OS X和iOS中,苹果提供了两种类型的API:
-
cocoa层 : NSRunLoop
-
CoreFoundation : CFRunLoopRef
显而易见,NSRunLoop是对CFRunLoopRef在OC上面向对象的封装,但是NSRunLoop不是线程安全的,而CFRunLoopRef是线程安全的。
整个CoreFoundation是开源的,可以在这里下载源码。
这个CFRunLoop有五大类:
- CFRunLoopRef :
一个runLoop对象
- CFRunLoopMod
一个RunLoop包含若干个Mode,Mode中又包含若干个Source/Timer/Observer,如果切换Mode,只能退出当前的RunLoop,主要是为了分隔开不同组的Source/Timer/Observer。
- CFRunLoopObserverRef
RunLoop状态的观察者,每一个观察者都包含一个回调(指针函数),当RunLoop的状态发生变化时,观察者就能通过回调接收这个变化。
可以观察到的RunLoop的状态时间点有:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), //即将触发Timer
kCFRunLoopBeforeSources = (1UL << 2),//即将触发Source
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //即将被换新
kCFRunLoopExit = (1UL << 7), //即将退出
};
- CFRunLoopSourceRef
事件的产生的地方。
有两个版本的Source:
- *Source0 *:只包含一个回调函数指针,使用时需要将事件标记为待处理:
CFRunLoopSourceSignal(source)
,再调用CFRunLoopWakeUP(runloop)
来唤醒RunLoop,使其处理整个事件 - *Source1 *:包含一个用于内核和其他线程发送消息的方法:mach_port,这种source能主动唤醒RunLoop,具体原因看底下会讲到的mach_msg()
- CFRunLoopTimerRef
基于时间的触发器,与NSTimer可以混用,包含一个回调,当其加入RunLoop时,RunLoop会注册时间点,等到时间点到了RunLoop会被唤醒处理回调时间。
Source/Timer/Observer被统称为mode item,一个item可以加入不同的RunLoop,但是重复添加到同一个Runloop是不会起作用的;如果一个RunLoop不包含Source/Timer(只包含Observer也不行),这个RunLoop是不会进入循环的。
CFRunLoop结构RunLoop代码
CFRunLoopMode和CFRunLoop的关系大致为:
struct __CFRunLoopMode {
...
CFStringRef _name;//mode的名字:kCFRunLoopDefaultMode
CFMutableSetRef _source0;
CFMutableSetRef _source1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
}
struct __CFRunLoop {
...
CFMutableSets _commonModes; //存放具有"common"标示的mode
CFMutableSets _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes
...
}
__commonModesItems里面存放observer/timer/source,当RunLoop的状态发生变化时,RunLoop会将这个集合里面的所有mode item同步到具有"common"标示的mode中。
应用场景:
我们知道,一个timer在NSDefaultMode
下被触发,如果这个时候拖动scrollview的话,这个timer就失效了,因为拖动scrollview,RunLoop的mode切换为UITrackingRunLoopMode
。如果想要让一个定时器在两个模式下都有效有两种方法:1、将它加入到两个mode中;2、将timer加入到顶层的RunLoop的commonModeItems集合中,RunLoop会自动将这个集合中的所有item同步到具有"common"标示的mode
RunLoop与线程
线程与RunLoop是一一对应关系,其关系保存在一个全局字典中,但是不是说一条线程中只能有一个RunLoop,你可以通过在RunLoop中嵌套另一个RunLoop达到一个线程中多个RunLoop的目的。
苹果不允许直接创建RunLoop,只能通过提供的两个方法获取当前线程的RunLoop和主线程的RunLoop:CFRunLoopGetCurrent()
和CFRunLoopGetMain()
。当你获取RunLoop的时候,如果没有这个RunLoop,那么系统会创建一个RunLoop返回给你,线程刚创建时是没有RunLoop的,RunLoop的创建发生在第一次获取的时候。
获取线程RunLoop的代码大致如下:
CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop)
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop)
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
return loop;
}
RunLoop流程:
//1、通知observer即将进入RunLoop:
__CFRunLoopDoObserver(rl, currentMode, KCFRunLoopEntry);
__CFRunLoopRun(CFRunLoopRel rl, sourchHandleThisLoop) {
int retval = 1;
do {
//2、通知observer:即将处理Timer
__CFRunLoopDoObserver(rl, currentMode, kCFRunLoopBeforeTimers);
//3、通知observer:即将处理Source0
__CFRunLoopDoSource(rl, currentMode, kCFRunLoopBeforeSources);
//4、RunLoop触发source0
__CFRunLoopDoObserver(rl, currentMode, stopAfterHandler);
//5、如果有source1且是处理ready状态,跳转到 9 ,直接处理source1然后跳转去处理消息
if (__Source0DidDispatchPortLastTime) {
Boolean hasMag = __CFRunLoopServiceMachPort(dispatchPort, &msg);
if (hasMas) goto handleMsg
}
//6、如果没有待处理的消息,通知observer,即将进入休眠
if (!__soure0DidDispatchPortLastTime) {
__CFRunLoopDoObserver(rl, currentMode, kCFRunLoopBeforeWaiting);
}
//7、RunLoop进入休眠,除非有以下情况才会从休眠中被唤醒:
1)timer设定的时间点到了
2)基于port的source事件
3)分发到主队列RunLoop的任务
//8、通知observer线程刚刚被唤醒
__CFRunLoopDoObserver(rl, currentMode, kCFRunLoopAfterWaiting);
//9、RunLoop被唤醒,处理消息:
if (msg_is_timer) {
CFRunLoopDoTimer(rl, currentMode, mach_absoluteTime());
}else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_MAIN_DISAPTCH_QUEUE(msg)
}else {
CFRunLoopSourceRef source1 = __CFRunLoopFindSourceForMachPort(rl, currentMode, livePort);
sourceDoHandleThisPort = __CFRunLoopDoSource1(rl, currentMode, source1);
if (surceDoHnadleThisPort) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
} while (retVal == 1)
}
//10、通知observer,即将推出RunLoop
__CFRunLoopDoObserver(rl, currentMode, kCFRunLoopExit);
RunLoop底层实现
RunLoop的核心是基于mach_port,进入休眠期间调用的是mach_msg()
方法,
关于mach:
- mach是OS X/iOS 系统架构XNU内核,作为它作为一个微内核,仅提供处理器的调度,IPC等非常少量的服务。
- 在mach中的对象不能直接调用,只能通过消息传递的方式实现对象间的通信。
- 有关mach的通信如何使用参照这里。
- 消息在mach中是最基础的概念,在两个端口直接传递,行程了mach 的IPC核心。
- 从mach 的源码信息中可以看出mach其实就是一个二进制包数据,其头部包含了当前端口
local_port
和目标端口remote_port
。 - 苹果提供了在cocoa层的对mach端口操作API-NSMachport。
在APP静止的时候点击暂停可以从调用栈信息里面看到:
mach_msg_trap()
系统在用户态时会调用mach_msg_trap()会触发陷阱机制,切换到内核态,在内核态下实际上是调用了mach_msg()来实现完成实际工作。
大体过程是这样的:
RunLoop的五个Mode
RunLoop包含五个Mode,分别为:
- kCFRunLoopDefaultMode 默认的mode,主线程在此mode下实现
- UITrackingRunLoopMode 追踪scrollview的触摸滑动
- UIInitializationRunLoopMode 刚启动APP时的第一个mode,只在刚启动时有效,之后后切换为default mode
- GSEventReceiveRunLoopMode 系统事件的内部mode
- kCFRunLoopCommonMode 占位mode,无实际作用
RunLoop的调用函数
RunLoop在执行中,是通过一长串的函数进行回调的,例如:
即将出发timer的回调:
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers)
,
即将出发source回调:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
......
这些函数都能在调用栈信息中找到。
在RunLoop中,这些函数的执行顺序是这样的:
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9.1 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9.2 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9.3 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
note:autoreleasepool的创建和释放是在RunLoop的休眠和下一次启动之间进的。
RunLoop在cocoa中的应用
事件传递与手势识别
对于硬件事件(触摸、锁屏、摇晃)的处理,苹果注册了一个基于port的source1,它的回调函数是__IOHIDEventSystemClientQueueCallback()
,事件发生后,系统将事件包装成IOHIDEvent
对象,并由mach port分配到对应的APP进程中,随后触发source1的回调,并调用_UIApplicationHandleEventQueueCallback()
进行内部分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等,接下来发生的 事件传递响应链 了。
对于手势识别:当_UIApplicationHandleQueueCallback()
接收到手势的时候,会将TouchBegin等事件的回调打断,随后会将这个手势标记为待处理状态,同时注册一个observer,检测BeforeWaiting状态,当RunLoop即将进入休眠时,其内部会获取到刚才所有标记为待处理的手势,执行_UIGestureRecognizerUpdateQueue()
。
Autorealease
iOS中autorelease变量什么时候释放,应该分为两种情况:
- 手动释放@autoreleasepool { }中的自动释放变量在当前大括号作用域结束时释放;
- 系统释放:在当前RunLoop本次Loop结束后释放;
autorelease原理:
主线程注册了两个observer1,observer2,这两个observer的回调函数都是
__wrapRunLoopWithAutoReleasePoolHandler()
。
observer1监测Entry状态,当进入RunLoop时,调用
_obj_autoreleasepool_push()
方法创建一个新的autoreleasepool,这个observer的优先级最高,确保autoreleasepool的创建在所有的回调之前;
observer2监测BeforeWaiting状态,当RunLoop即将进入休眠时,回调中先调用
_objc_autoreleasepool_pop()
方法将autoreleasepool里面的自动释放类型的变量释放,然后再调用_objc_autoreleasepool_push()
方法创建一个新的autoreleasepool。同时observer2还会检测Exit状态,当退出RunLoop时调用_objc_autoreleasepool_pop()
。这个observer的优先级最低,确保autoreleasepool的释放在所有的回调之后。
Autorelease的深层原理请参考:黑幕背后的Autorelease
页面刷新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。在这个函数之后就在屏幕上看到UI的变化
Timer
可以说没有RunLoop就不可能实现定时器的功能。定时器的大致原理:设定一个时间点,将定时器加入RunLoop中,等到达设定的时间点的时候回唤醒线程处理回调。
PerfromSeletor:afterDelay:
如果当前线程中没有RunLoop这个方法是不会有效的,本质上是在当前线程的RunLoop中添加一个定时器,当时间点到了会唤醒RunLoop执行回调。
dispatch_main_queue
当调用 dispatch_async(dispatch_get_main_queue(), block)
时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
有一种说法,说RunLoop的timer和GCD中的timer是一个东西,其实不是的,但是GCD的timer和RunLoop是怎么协调工作的,具体还不太清楚。
RunLoop的实际使用案例
AFNetWorking
在AFN中的:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
在上面的代码中,AFN创建了一个RunLoop,并且加入了一个mach port,但是这个port什么事情也没做,主要作用是为了让RunLoop处于常驻状态,否则这个RunLoop马上就会退出了,所以,这也是创建一个常驻服务线程的方法。
滚动视图中延迟加载图片
在tableview或者collection view中,如果要在停止拖拽的时候再加载图片,最直接的想法是,通过tableview和colletionview的isDragging等属性来进行判断,但是使用RunLoop来实现这一功能就不需要那么麻烦:
[imgView performSelector:@selector(loadImage:)
withObject:image
inModeS:@[NSDefaultRunLoopMode]];