>下面会用一些陌生的或者容易让人混淆的字符,我们先来统一概念再继续,这样能够让你更加愉快的阅读:

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());
}

苹果提供了CFRunLoopGetMainCFRunLoopGetMain这两个方法来创建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提供whilefor循环来驱动run loop。在你的循环中,你使用一个runloop object来运行事件处理的code,runloop接受事件并调用你已经准备好的方法。

/*
 这里doFireTimer方法就是你要处理的事件的code,<i>runloop</i>在从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(例如,图中的hanlePortmySelector等),并通过runUntilDate:方法(调用thread相关的NSRunLoop对象)来退出。Timer sources发送事件到runloop的handler,但不会引起runloop的退出。

runloop-1.jpg

除了处理输入源对应的handler,Runloop生成关于run loop行为的通知。注册run-loop 的observer接受这些通知。使用它们在thread上做一些其他的操作。你可以在你的线程中使用Core Foundation里面的类来创建run-loop observer

Run Loop Modes

run-loop mode是被监听的Input sourcesTimers的集合,同时还是被通知的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(kCFRunLoopDefaultModekCFRunLoopCommonModes等)。Cocoa和Core Foundation都定义了默认的mode和一些经常使用的mode。

你也能通过简单的字符串来自定义自己的mode。尽管custom mode定义起来好像很随意,但使用起来可不能随意啊。对于任何mode,你必须要确保添加一个或者更多的sourcestimers或者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。用 CFMachPortRefCFMessagePortRef,  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方法

Methods
performSelectorOnMainThread:withObject:waitUntilDone:performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:performSelector:onThread:withObject:waitUntilDone:modes:
performSelector:withObject:afterDelay:performSelector:withObject:afterDelay:inModes:
cancelPreviousPerformRequestsWithTarget:cancelPreviousPerformRequestsWithTarget:selector:object:
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 ReferenceCFRunLoopTimer 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执行的顺序如下面所列出的:

  1. 通知run-loop observer,进入runloop
  2. 通知run-loop observer,已经准备好的Timer将要触发
  3. 通知run-loop observer,以及准备好的*no-port-based input source*将要被触发
  4. 触发no-port-based input source
  5. 如果一个port-based input source准备好等待触发,立即处理该事件,再从第九步开始执行
  6. 通知run-loop observer,线程将要sleep
  7. 线程sleep直到下面的事件发生:
    • port-based input source事件到达
    • 一个Timer触发了
    • runloop超时了
    • runloop被手动唤醒
  8. 通知run-loop observer,线程将要被唤醒
  9. 处理挂起的事件

    • 如果一个用户定义的timer触发了,处理相关事件,重新开始循环,回到第二步继续执行
    • 如果input source触发,传递相应的事件
    • 如果runloop*明确的被唤醒,但是还没有超时,重新开始循环,回到第二步继续执行
  10. 通知run_loop observerrunloop已经退出

Tip:no-port-based input source就是我们常说的source0,port-based input source就是source1

因为Timer和input source的通知是在事件被触发之前,所以这之间有一个过渡时间。如果你要在这个时间做一些事情,你可以使用sleepawake-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事件在事件的回调函数中标记待处理的手势,并处理手势。
  • 界面更新:setNeedsLayoutsetNeedsDisplay都会将该view标记为待处理,等到下一个runloop的时候会布局UI。
  • 定时器:就是一个CFRunLoopTimerRef
  • PerformSelecter:将方法扔到某个线程中,在下一个run loop的时候执行。
  • 关于GCD:dispatch_async(dispatch_get_main_queue(), block)向主线程的run loop发送消息,唤醒run loop。并执行block中的内容。

PPT总结一下:

什么是runloop

基本组成

source的种类

runloop生命周期

为什么使用runloop

具体列子

检测主线程是否卡顿

AFNetworking