Run Loops
下面会用一些陌生的或者容易让人混淆的字符,我们先来统一概念再继续,这样能够让你更加愉快的阅读:
runloop:iOS一个底层机制的专业术语。
run loop:一种运行的循环。
Handler:指handlePort:
、customSrc:
、mySelector:
、timerFired:
,指开发者希望当进入run loop的时候要执行的操作
run-loop observer:观察runloop行为的观察者
run-loop mode:每个runloop都要指定一种mode,一种mode可以对应多个input source
runloop object:runloop相关的对象,这里分Cocoa和CoreFoundation中的
Runloop是线程架构相关的一部分,是事件处理的循环。你可以用它来安排循环执行任务,协调相关的事件(kCFRunLoopEntry、kCFRunLoopExit等)。
用几行示意代码来展示:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
Runloop实际上是一个管理要处理的事件和消息的对象,并为开发者提供了一个入口来执行run loop的逻辑部分。一直处于类似于“接受消息-->等待-->处理”这样的一个循环汇总
Runloop的目的:一般我们自己写的线程在执行完相应的任务就会退出,而runloop让你的线程在有事干的时候干活,没事干的时候休息,节省系统资源。
Runloop的管理不全是自动的,对于自己的线程,必须要自己添加代码,并在适当的时候启动Run loop来响应接受的事件。
Cocoa和Core Foundation提供了runloop objects来帮助你配置和管理线程的run loop。你的Application不需要自己创建这些对象(像alloc、init这些方法)。每个thread,包括Application的主线程,都已经有runloop object,有方法可以直接获取这这些对象,类似于这样的[NSRunLoop currentRunLoop]
。
/// 全局的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());
}
苹果提供了CFRunLoopGetMain
、CFRunLoopGetMain
这两个方法来创建runloop,如果它不被调用,线程中就不会有runloop。
Cocoa中的
NSRunLoop
,和Core Foundation中的CFRunLoopRef
等Run loop相关的类。它们的方法一般也是一一对应的。
唯一不同的是:你自己创建的线程需要明确的代码来让runloop跑起来,比如[runLoop run]
这样的方法。但是对于主线程的runloop,系统会在App起来的时候自动设置并跑主线程里面的runloop,这是Application运行起来的一部分。
Anatomy of Run Loop(剖析runloop)
“一个运行的循环”,看它名字的意思就能猜得到。它会循环不断的进入线程,在runloop进入之后做一些自己的事情。你的code提供了状态的控制用来实现runloop真正的循环部分。—— 换句话说,你的code提供while
或for
循环来驱动run loop。在你的循环中,你使用一个runloop object来运行事件处理的code,runloop接受事件并调用你已经准备好的方法。
/*
这里doFireTimer方法就是你要处理的事件的code,*runloop*在从sleep状态被wakeup之后会执行改方法。
*/
[NSTimer scheduledTimerWithTimeInterval:0.016
target:self
selector:@selector(doFireTimer:)
userInfo:nil
repeats:YES];
上面我们提到“你使用一个runloop object来运行事件处理的code”,这里的事件是从哪里来呢?即事件源是什么?
事件源分两类:
- Input source所驱动的异步事件,一般它用来从一个线程向另一个线程发送事件或者从一个Application向另一个Application。
- Timer sources所驱动的同步事件,事件发生在一个已经预先设定好的时间,或者在重复的事件间隔发生。
当事件到达的时候,这两种source会按照系统指定的步骤来处理事件。
下图展示了一个runloop的概念结构和各种不同的sources。Input sources发送一个异步的事件来执行相对应的handler(例如,图中的hanlePort
、mySelector
等),并通过runUntilDate:
方法(调用thread相关的NSRunLoop对象)来退出。Timer sources发送事件到runloop的handler,但不会引起runloop的退出。
除了处理输入源对应的handler,Runloop生成关于run loop行为的通知。注册run-loop 的observer接受这些通知。使用它们在thread上做一些其他的操作。你可以在你的线程中使用Core Foundation里面的类来创建run-loop observer。
Run Loop Modes
run-loop mode是被监听的Input sources和Timers的集合,同时还是被通知的run-loop observer的集合,大家可能比较疑惑怎么是两种东西的集合(source和obaserver),下面是我的理解,从代码出发:
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
线程的每个Source都要指定一个Mode,这里是NSDefaultRunLoopMode
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopCommonModes);
观察者是观察该Runloop的某些Modes,kCFRunLoopCommonModes是mode的集合,每种mode都可以添加到Common中
每次你运行你的runloop的时候,要指定一个具体的mode在runloop里面跑。在进入runloop的时候,只有和这个mode相关联的source能被对应的run-loop observer监听到,被允许向它们传递事件。而其他mode相关的source会一直等待,直到run-loop mode和source指定的mode相对应的时候才开始传递事件。
在你的代码中,你通过具体的名称来定义mode(kCFRunLoopDefaultMode
、kCFRunLoopCommonModes
等)。Cocoa和Core Foundation都定义了默认的mode和一些经常使用的mode。
你也能通过简单的字符串来自定义自己的mode。尽管custom mode定义起来好像很随意,但使用起来可不能随意啊。对于任何mode,你必须要确保添加一个或者更多的sources,timers或者run-loop observer,如果你没有做到,run loop就会直接退出。
//如果没有第二行代码,runloop会自动退出
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
你可以使用mode过滤掉不想要的source。大多数情况下,你只需要用系统定义的“default”mode来运行你的runloop。
Note:Mode是根据事件source来匹配的而不是根据事件的type。例如,你不可以使用鼠标的按下事件或者键盘按下事件来匹配mode。
下面是一些系统定义好的Modes
Mode | Name |
---|---|
Default | NSDefaultRunLoopMode(Cocoa)kCFRunLoopDefaultMode(Core Foundation) |
Connection | NSConnectionReplyMode(Cocoa) |
Modal | NSModalPanelRunLoopMode(Cocoa) |
Event tracking | NSEventTrackingRunLoopMode(Cocoa) |
Common modes | NSRunLoopCommonModes(Cocoa)kCFRunLoopCommonModes(Core Foundation) |
Input Sources
Input source通过异步的方式向你的线程传递事件。事件的源取决于input source的类型。input source大概分为两类:
-
Port-based port
Port-based port监控你application的Mach port。 -
Custom input source
Custom input source监控自定义的事件source。
runloop不关心是哪种input source,这两种input sources系统都有实现。这两个input source唯一的不同就是它们如何发送信号。Port-based port通过内核自动发送信号。Custom input source只能从其他线程接受手动发送的信号。
当你创建一个input source,你把它赋值到runloop的mode。mode会影响被监控的input source。大多数情况,你在NSDefaultRunLoopMode
下运行runloop,但是你也可以指定自定义的mode。如果一个input source不在当前被监控的mode中,它产生的一些事件将会被暂时的放在一边,直到下一个run loop的mode和input source的mode相同才开始处理这个input source产生的事件。
接下去的部分描述了一些input source
Port-Based Sources
Cocoa和Core Foundation通过提供端口相关的对象和方法来创建Port-Based Source。例如,在Cocoa中,你没必要直接创建input source。你使用NSPort
的方法[NSPort port]
,添加port到run loop中。Port object会为你创建和配置所需的Port-Based Sources。
在 Core Foundation中,你必须要自己创建port和它的run loop source。用 CFMachPortRef, CFMessagePortRef, CFSocketRef这些不透明指针创建相应的对象。
如何配置和设置自定义的Port-Based Sources,请看 Configuring a Port-Based Input Source
Custom Input Sources(Core Foundation)
为了创建自定义的input source,你必须使用Core Foundation中的CFRunLoopSourceRef不透明类相关的方法。你需要在回调方法中配置这个Custom Input Sources。Core Foundation会在配置Custom Input Sources、处理输入事件和当source要从runloop中移除的时候调用这几个回调。
对于如何创建一个custom input source请看这里Defining a Custom Input Source
Cocoa Perform Selector Sources(Cocoa)
除了Port-Based Sources,Cocoa定义了一个custom input source,这允许你向任何线程中发送选择子。和Port-Based Sources一样,执行选择子selector的请求在同一个线程上是连续的,当多个方法同时向线程发送请求的时候可能会导致同步的问题。和Port-Based Sources不同的是:一个perform selector source执行完它的selector,它自己会从runloop中移除。
当向另一个线程发送selector请求时,目标线程必须有一个active的runloop。这意味着你必须等待,直到该线程中runloop的状态变成 active。因为主线程会自己开始一个runloop,只要系统一调用Application delegate的applicationDidFinishLaunching:
(这时候系统的runloop就创建出来了),你就可以向主线程发送selector请求。每次Runloop会处理所有队列的selector请求,而不是只处理一个队列的请求。下面列出了一些perform方法
Timer Sources
Timer sources会在将来的某个时间,发送同步事件到你的当前的线程。Timer是线程用来通知它自己的一种方式。
尽管它是基于时间的通知,一个timer并不能保证在准确的时间点执行事件。和其他的Input source一样,Timer和你run loop指定的mode有关系。如果Timer的mode不是你当前runloop正在跑的mode,它是不会被触发的,直到你的runloop运行的的mode是你的Timer所支持的。类似的,如果一个Timer被触发,但是当runloop在执行Handler,Timer将会一直等待,直到下次再次进入runloop的时候才会执行。如果这个runloop不再跑了,这个Timer将永远不会被触发。
你可以配置Timer来运行事件,一次或者不停重复。一个重复的Timer会基于设定好的触发时间,自动重新安排它自己到run loop中,但这个时间间隔并不一定准确。例如,如果一个Timer在一个特定时间被触发,每5秒重复一次。即使实际的触发时间被延迟了,被安排的触发时间总落在5秒的延迟时间间隔之内。如果触发时间被延迟太多,会导致它错过了一次或者更多被安排到run loop的机会。在一个不恰当的时机被触发之后,Timer则会被安排到下一次run loop。
关于配置timer sources的更多信息,请看Configuring Timer Sources。更多相关信息NSTimer Class Reference或CFRunLoopTimer Reference。
Run Loop Observers
一般的source是在一个异步或者同步的事件发生的时候被触发的,而run-loop observer是在runloop它执行的时候触发的。你可以通过run-loop observer来准备一些线程待处理的事件,或者在线程sleep之前,在线程中做一些事。run-loop observers可以观察到下面的一些事件:
- 进入runloop的时候
- 当runloop将要处理timer的时候
- 当runloop将要处理input source的时候
- 当runloop将要sleep的时候
- 当runloop已经醒来,还没有开始处理event的时候
- 当runloop退出的时候
你可以使用Core Foundation向Application添加run-loop observer。为了创建一个run_loop observer,你需要使用CFRunLoopObserverRef类。这个类会追踪你自定义callback函数、你感兴趣的CFRunLoopActivity
。
和Timer相似,run-loop observer可以只运行一次,或者不停重复。只运行一次的run-loop observer在一次run loop之后就会从runloop中移除。当你创建run-loop observer的时候,你要决定r是一次还是不停重复。
对于如何创建一个run-loop observer的例子,看 Configuring the Run Loop,更多相关信息CFRunLoopObserver Reference
The Run Loop Sequence of Events
每次你运行runloop,你线程的runloop会处理之前挂起的事件,并且向run-loop observer发送通知。runloop执行的顺序如下面所列出的:
- 通知run-loop observer,进入runloop
- 通知run-loop observer,已经准备好的Timer将要触发
- 通知run-loop observer,以及准备好的no-port-based input source将要被触发
- 触发no-port-based input source
- 如果一个port-based input source准备好等待触发,立即处理该事件,再从第九步开始执行
- 通知run-loop observer,线程将要sleep
-----------------------------------sleepping-------------------------------------
-
线程sleep直到下面的事件发生:
- port-based input source事件到达
- 一个Timer触发了
- runloop超时了
- runloop被手动唤醒
通知run-loop observer,线程将要被唤醒
-
处理挂起的事件
- 如果一个用户定义的timer触发了,处理相关事件,重新开始循环,回到第二步继续执行
- 如果input source触发,传递相应的事件
- 如果runloop明确的被唤醒,但是还没有超时,重新开始循环,回到第二步继续执行
通知run_loop observer,runloop已经退出
Tip:no-port-based input source就是我们常说的source0,port-based input source就是source1
因为Timer和input source的通知是在事件被触发之前,所以这之间有一个过渡时间。如果你要在这个时间做一些事情,你可以使用sleep和 awake-from-sleep 通知来帮助你获得这段时间的控制权。
一个runloop可以通过使用run loop object被唤醒。其他的事件也可能引起runloop被唤醒。例如,添加no-port-based input source来唤醒runloop以至于input source可以被立即处理,而不是等待其他的事件发生。
iOS系统中有很多用到了RunLoop的地方:
- AutoreleasePool:监听
kCFRunLoopEntry
事件,保证在所以回调之前创建自动释放池;监听BeforeWaiting
事件,释放旧的线程池,创建新的线程池;监听kCFRunLoopExit
事件,保证线程池最后被释放。- 事件响应:通过port-based input source监听用户的事件,并转发给App进程,如用UIControl的事件。
- 手势识别:监听
kCFRunLoopBeforeWaiting
事件在事件的回调函数中标记待处理的手势,并处理手势。- 界面更新:
setNeedsLayout
、setNeedsDisplay
都会将该view标记为待处理,等到下一个runloop的时候会布局UI。- 定时器:就是一个CFRunLoopTimerRef
- PerformSelecter:将方法扔到某个线程中,在下一个run loop的时候执行。
- 关于GCD:dispatch_async(dispatch_get_main_queue(), block)向主线程的run loop发送消息,唤醒run loop。并执行block中的内容。
PPT总结一下: