iOS RunLoop完全指南

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013378438/article/details/80239686

什么是RunLoop

概念

什么是RunLoop,顾名思义,RunLoop就是在‘跑圈’,其本质是一个do
while循环。RunLoop提供了这么一种机制,当有任务处理时,线程的RunLoop会保持忙碌,而在没有任何任务处理时,会让线程休眠,从而让出CPU。当再次有任务需要处理时,RunLoop会被唤醒,来处理事件,直到任务处理完毕,再次进入休眠。

上面这段话,相信绝多数人都知道,大致知道RunLoop是个什么玩意。

如果继续问到:RunLoop是怎么实现休眠机制的?RunLoop都可以处理哪些任务,又是怎么处理的呢?RunLoop在iOS系统中都有什么应用呢?

可能很多人都很茫然。这是公司在筛选面试者时常用的手段,考察面试者对问题理解的深度,这从侧面反映了一个人的学习能力以及未来的可塑性,这也是为什么许多大公司偏爱问原理性问题的原因。
当然,了解了底层实现,对我们平日写代码的优化,主动避免一些坑,也是很有帮助的。
现在,就让我们一起深入了解RunLoop吧~

从main函数说起

先看一下下面这个命令行程序:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}

如果我们运行一下程序的话,会发现在命令行输出“Hello, World!”后,程序的进程自动退出:
这里写图片描述

原因很简单,因为我们的主线程完成了代码逻辑,return 0,程序结束。

再来看一下我们平常所写的iOS APP的main程序入口(main.m):


#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

代码和命令行版本的main函数很像,在main里面,直接return 了UIApplicationMain函数的返回值。
但是奇怪的是,我们的iOS APP却不会像命令行程序一样退出,这是为什么?

其实,UIKit会在UIApplicationMain函数中,启动main runloop,使得main runloop一直在跑圈(没事儿时在睡觉),这样,UIApplicationMain函数就不会立刻返回,我们的APP也就不会退出啦~

如果打系统断点CFRunLoopRunInMode的话,会看到在UIApplicationMain函数中,系统启动了main runloop:
这里写图片描述

RunLoop的结构组成

RunLoop位于苹果的Core Foundation库中,而Core Foundation库则位于iOS架构分层的Core Service层中(值得注意的是,Core Foundation是一个跨平台的通用库,不仅支持Mac,iOS,同时也支持Windows):

这里写图片描述

这里写图片描述

在CF中,和RunLoop相关的结构有下面几个类:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

RunLoop的组成结构如下图:
这里写图片描述
你可能需要不时回过头来看这张图,来加深理解RunLoop的结构以及他们之间的相互作用关系。

当我们在自己的APP中下断点时,有90%的几率会看到调用堆栈中有如下六个函数之一,它们都是RunLoop不同情境下的回调函数:

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

这也说明了我们的RunLoop都能干些什么事情,而且系统的大部分功能,都是和RunLoop相关的。GCD并不是基于RunLoop的(除了dispatch 到main queue),所以,你在GCD的调用堆栈中,是看不到它们的。

RunLoop对应的数据结构为:

// Foundation:
NSRunLoop : NSObject

// Core Foundation:
struct __CFRunLoop
__CFRunLoop * CFRunLoopRef

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

CFRunLoopRef 与 NSRunLoop之间的转换时toll-free的。关于RunLoop的具体实现代码,我们会在下面提到。

RunLoop提供了如下功能(括号中CF**表明了在CF库中对应的数据结构名称):

  1. RunLoop(CFRunLoop)使你的线程保持忙碌(有事干时)或休眠状态(没事干时)间切换(由于休眠状态的存在,使你的线程不至于意外退出)。
  2. RunLoop提供了处理事件源(source0,source1)机制(CFRunLoopSource)。
  3. RunLoop提供了对Timer的支持(CFRunLoopTimer)。
  4. RunLoop自身会在多种状态间切换(run,sleep,exit等),在状态切换时,RunLoop会通知所注册的Observer(CFRunLoopObserver),使得系统可以在特定的时机执行对应的操作。相关的如AutoreleasePool 的Pop/Push,手势识别等。

RunLoop在run时,会进入如下图所示的do while循环:
这里写图片描述

当RunLoop没有任务处理时,会进入休眠状态,此时如果在XCode点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。
对应CF源码,RunLoop会卡在

ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);

在mach_msg函数里面,线程会陷入mach_msg_trap。

这里写图片描述

关于RunLoop的休眠,涉及到一个关键点,就是mach port通信。

以下内容摘自深入理解RunLoop

RunLoop 的核心就是一个 mach_msg() (图中第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。
这里写图片描述

苹果官方将整个系统大致划分为上述4个层次:
应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。
应用框架层即开发人员接触到的 Cocoa 等框架。
核心框架层包括各种核心框架、OpenGL 等内容。
Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。

Darwin的架构如下:
这里写图片描述

其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。
XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。
BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。
在 Mach 中,所有的东西都是通过对象实现的,进程、线程和虚拟内存都被称为”对象”。

Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。 其实Mach通信,可以参考相对熟悉的socket通信。

好,让我们回到RunLoop中来。

Thread & RunLoop

虽然RunLoop与Thread的关系十分密切,但是并不是每个Thread都拥有一个RunLoop的。
在iOS中,除了系统会为主线程自动创建一个RunLoop,在子线程中,我们需要手动获取线程对应的RunLoop:

[NSRunLoop currentRunLoop];
[NSRunLoop mainRunLoop]
// 其对应的CF源码为:
CF_EXPORT CFRunLoopRef CFRunLoopGetCurrent(void);
CF_EXPORT CFRunLoopRef CFRunLoopGetMain(void);

方法的名字很具有迷惑性,Get方法可能让我们以为已经存在线程对应的RunLoop了,其实这是一个懒加载方法,代码实现是:

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

Get RunLoop函数首先会在在static 的__main runLoop 或当前子线程的TSD(程私有数据Thread-specific Data)中去找是否有当前线程对应的RunLoop,如果没有找到,则
这两个函数最终都会调用 _CFRunLoopGet0(pthread_t t)方法,其参数是当前线程自身,简化版实现是:

static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
...
 CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 在静态全局变量__CFRunLoops寻找线程t所对应的RunLoop(key为线程自身)
    __CFUnlock(&loopsLock);
    if (!loop) { // 没找到
    CFRunLoopRef newLoop = __CFRunLoopCreate(t); // 创建一个新的Loop
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 为什么get两次??
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); // 将新创建的Loop存入__CFRunLoops字典
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) { // 若当前调用线程和t是一个线程,则同时设置当前线程的TSD
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop; // 返回Loop
}

由代码可知:

  1. RunLoop和Thread是一一对应的(key: pthread value:runLoop)
  2. Thread默认是没有对应的RunLoop的,仅当主动调用Get方法时,才会创建
  3. 所有Thread线程对应的RunLoop被存储在全局的__CFRunLoops字典中。同时,主线程在static CFRunLoopRef __main,子线程在TSD中,也存储了线程对应的RunLoop,用于快速查找。

这里有一点要弄清,Thread和RunLoop不是包含关系,而是平等的对应关系。Thread的若干功能,是通过RunLoop实现的。

另一点是,RunLoop自己是不会Run的,需要我们手动调用Run方法(Main RunLoop会由系统启动),我们的RunLoop才会跑圈。静止(注意,这里的静止不是休眠的意思)的RunLoop是不会做任何事情的

RunLoop Mode

上面我们可以知道,RunLoop只有在Run的情况下,才会处理具体事务。
那么提到Run,这里就离不开RunLoop Mode。

每次RunLoop开始Run的时候,都必须指定一个Mode,称为RunLoop Mode。Mode指定了在这次的Run中,RunLoop可以处理的任务。对于不属于当前Mode的任务,则需要切换RunLoop至对应Mode下,再重新调用run方法,才能够被处理。

这点在CF的代码中可以看出,CF中RunLoop的run函数如下:

static int32_t __CFRunLoopRun(CFRunLoopRef rl,             // runloop will run
                              CFRunLoopModeRef rlm,        // the runloop 's mode to run
                             Boolean stopAfterHandle,      // if Yes, exit runloop after handle on source
                             CFRunLoopModeRef previousMode) // previous runloop mode

RunLoop,RunLoop mode和RunLoop items的关系如下图所示:
这里写图片描述

一个RunLoop可以有多个mode,每一个mode下都有4个集合结构,分别对应着当前mode可以处理的source0,source1, 和timer事件,以及当前mode下所注册的RunLoop state observers。

在Mode 的右侧类似棒棒糖的图形上,分别标出了当RunLoop休眠时(mach_msg_trap状态),外部线程/进程可以将RunLoop唤醒的Mach Port。有两大类,_portSet, 是一个port集合。另一个是_timerPort,专用于NSTimer事件的RunLoop唤醒。(其实在_portSet中是包含了_timePort的)关于RunLoop的休眠和唤醒,我们将在稍后提到。

RunLoop mode的代码如下所示:

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0; 
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;  // dispatch timer 所在的queue
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

我们可以自定义mode,系统也为我们预定义了一些mode:

这里写图片描述

这里我们只需要关心Default 和 Event tracking Mode,以及一个特殊的’mode’ : Common modes。

Default mode 是RunLoop默认的mode。当RunLoop被创建时,就会对应创建出一个default mode。其余的mode,则是懒加载的。

Event tracking mode 是Cocoa在处理密集传入的事件时所使用的mode(如scrollview的滑动)。

Common modes 其实不算是一个mode,而是一个mode的集合。在Cocoa程序中,默认会包含default,modal,event tracking mode。而在Core Foundation程序中,默认仅有Default mode。我们也可以将自定义的mode加入到common modes中。如果我们希望将事件能够在多个mode下得到处理,则直接将事件注册到Common modes中即可。

这里,我们稍微在源码的角度了解下mode是如何加入到common modes中的。
我们回看CFRunLoop定义中和mode相关的结构:

struct __CFRunLoop {
   ...
    CFMutableSetRef _commonModes;  // set:被加入到common mode中的mode
    CFMutableSetRef _commonModeItems; // set: 被加入到common mode 下的items(source/timer/observer )
    CFRunLoopModeRef _currentMode; // 当前的mode
    CFMutableSetRef _modes;  // RunLoop所有的modes
    ...
};

一个普通的mode可以将自身‘标记’为common,具体做法是,系统会将该mode的name添加到_commonModes中。

当item被加入到common modes下时,首先,会在runloop的_commonModeItems中添加一个条目,然后,会遍历所有的_commonModes,将该item加入到已经被标记为common的mode中。

当一个mode作为整体被加入common modes下时,则会进行另外一番操作。系统会调用void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName)方法。在方法内部,系统首先会将mode置位为common(将mode name加入到_commonModes)。然后,会将_commonModeItems中的所有元素,添加到这个mode中。注意,这里mode中的item并不会被加入到_commonModeItems中。

这意味着,当我们将mode作为整体加入到Common modes中时,该mode可以响应common mode item事件,而其本身自带的mode items,在别的被标记为common的mode中,却不会被响应。

当我们将任务交给RunLoop的时候,需要指定任务在哪个mode下面处理,如果不指定,则默认在default mode下处理

一个任务可以提交到多个mode中,如果向一个mode多次提交同一个任务,则mode中仅会保存一个任务,因为在代码中会有类似的判断:

if(!CFSetContainsValue(rlm->_sources0, rls)) {
    // 将任务加入到mode中
}

如,timer是基于RunLoop实现的,我们在创建timer时,可以指定timer的mode:

NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
       NSLog(@"do timer");
    }];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; // 指定timer在common modes(default mode + event tracking mode) 下运行

如果我们没有指定timer的RunLoop mode,则默认会添加到default mode下运行。

这也就解释了,为什么当我们在滑动scrollview的时候,timer事件不会被回调。因为如果我们将timer添加到默认的主线程 的default mode时,当用户滑动scrollview的时候,main RunLoop 会切换到event tracking mode下来接收处理密集的滑动事件,这时候,添加在default mode下的timer是不会被触发的。

解决方法就是,我们将timer添加到common modes下,让其在default mode和Event tracking mode下面都可以被调用。

RunLoop Source

苹果文档将RunLoop能够处理的事件分为Input sources和timer事件。下面这张图取自苹果官网,不要注意那些容易让人混淆的细节,只看Thread , Input sources 和 Timer sources三个大方块的关系即可,不要关注里面的内容。

这里写图片描述

timer事件我们下面会讲到,现在来看Input source。

根据CF的源码,Input source在RunLoop中被分类成source0和source1两大类。source0和source1均有结构体__CFRunLoopSource表示:

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;         /* 优先级,越小,优先级越高。可以是负数。immutable */
    CFMutableBagRef _runLoops;
    union {  // 联合,用于保存source的信息,同时可以区分source是0还是1类型
        CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;   /* immutable, except invalidation */
    } _context;
};

typedef struct {
    CFIndex version;      // 类型:source0
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*perform)(void *info);  // call out 
} CFRunLoopSourceContext;

typedef struct {
    CFIndex version;  // 类型:source1
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t (*getPort)(void *info);
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *  (*getPort)(void *info);
    void    (*perform)(void *info); // call out
#endif
} CFRunLoopSourceContext1;

source0和source1由联合_context来做代码区分。

source0 VS source1

相同
1. 均是__CFRunLoopSource类型,这就像一个协议,我们甚至可以自己拓展__CFRunLoopSource,定义自己的source。
2. 均是需要被Signaled后,才能够被处理。
3. 处理时,均是调用__CFRunLoopSource._context.version(0?1).perform,其实这就是调用一个函数指针。

不同

  1. source0需要手动signaled,source1系统会自动signaled
  2. source0需要手动唤醒RunLoop,才能够被处理: CFRunLoopWakeUp(CFRunLoopRef rl)。而source1 会自动唤醒(通过mach port)RunLoop来处理。

Source1 由RunLoop和内核管理,Mach Port驱动。
Source0 则偏向应用层一些,如Cocoa里面的UIEvent处理,会以source0的形式发送给main RunLoop。

Timer

我们经常使用的timer有几种?

  • NSTimer & PerformSelector:afterDelay:(由RunLoop处理,内部结构为CFRunLoopTimerRef)
  • GCD Timer(由GCD自己实现,不通过RunLoop)
  • CADisplayLink(通过向RunLoop投递source1 实现回调)

    NSObject perform系列函数中的dealy类型, 其实也是一种Timer事件,可能不那么明显:

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay
- 

这种Perform delay的函数底层的实现是和NSTimer一样的,根据苹果官方文档所述:

This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.
If you want the message to be dequeued when the run loop is in a mode other than the default mode, use the performSelector:withObject:afterDelay:inModes: method instead. 

NSTimer & PerformSelector:afterDelay:

NSTimer在CF源码中的结构是这样的:

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;       /* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;          /* 触发时间,TSR units */
    CFIndex _order;         /* immutable */
    CFRunLoopTimerCallBack _callout;    /* immutable */ // timer 回调
    CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

关于Timer的计时,是通过内核的mach time或GCD time来实现的。

在RunLoop中,NSTimer在激活时,会将休眠中的RunLoop通过_timerPort唤醒,(如果是通过GCD实现的NSTimer,则会通过另一个CGD queue专用mach port),之后,RunLoop会调用

__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

来回调到timer的fire函数。

下面是NSTimer触发时的函数调用堆栈:
这里写图片描述

PerformSelector:afterDelay:有类似的调用堆栈,因为底层实现和NSTimer是一样的。

Observer

Observer在CF中的结构如下:

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;      /*所监听的事件,通过位异或,可以监听多种事件 immutable */
    CFIndex _order;         /* 优先级 immutable */
    CFRunLoopObserverCallBack _callout; /* observer 回调 immutable */
    CFRunLoopObserverContext _context;  /* immutable, except invalidation */
};

Observer的作用是可以让外部监听RunLoop的运行状态,从而根据不同的时机,做一些操作。
系统会在APP启动时,向main RunLoop里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用
_objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠)
时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个
Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool
环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

Observer可以监听的事件在CF中以位异或表示:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),    // 进入RunLoop循环(这里其实还没进入)
    kCFRunLoopBeforeTimers = (1UL << 1),  // RunLoop 要处理timer了
    kCFRunLoopBeforeSources = (1UL << 2), // RunLoop 要处理source了
    kCFRunLoopBeforeWaiting = (1UL << 5), // RunLoop要休眠了
    kCFRunLoopAfterWaiting = (1UL << 6),   // RunLoop醒了
    kCFRunLoopExit = (1UL << 7),           // RunLoop退出(和kCFRunLoopEntry对应)
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

这里,
kCFRunLoopEntry,
kCFRunLoopExit
在每次RunLoop循环中仅调用一次,用于表示即将进入循环和退出循环。

kCFRunLoopBeforeTimers,
kCFRunLoopBeforeSources,
kCFRunLoopBeforeWaiting,
kCFRunLoopAfterWaiting
这些通知会在循环内部发出,可能会调用多次。

关于如何利用观察RunLoop的状态,一个检测界面卡顿的例子:
RunLoop总结:RunLoop的应用场景(四)App卡顿监测
微信iOS卡顿监控系统

RunLoop源码剖析

CF的RunLoop源码比较长,而且还为了跨平台引入了一些Windows的逻辑。我这里删除了一些非主要的逻辑,并配上注释,大家可以结合对比真实的代码进行理解。

/**
 *  运行run loop
 *
 *  @param rl              运行的RunLoop对象
 *  @param modeName        运行的mode名称
 *  @param seconds         run loop超时时间
 *  @param returnAfterSourceHandled true:run loop处理完事件就退出  false:一直运行直到超时或者被手动终止
 *  
 *
 *  @return 返回4种状态
 */

 SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled){
     // 根据modeName获得当前mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);

    // 保存上一次mode 并将runloop替换为当前mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 通知observer,进入RunLoop

    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);  // 调用 __CFRunLoopRun 真正开始run

    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); // 通知observer, runloop退出

    rl->_currentMode = previousMode; // 将runloop恢复为之前的mode
    return result; // 返回runloop run的返回值
}

CFRunLoopRunSpecific的函数里面,核心的是调用__CFRunLoopRun 让RunLoop真正的run起来:

/**
 *  运行run loop
 *
 *  @param rl              运行的RunLoop对象
 *  @param rlm             运行的mode
 *  @param seconds         run loop超时时间
 *  @param stopAfterHandle true:run loop处理完事件就退出  false:一直运行直到超时或者被手动终止
 *  @param previousMode    上一次运行的mode
 *
 *  @return 返回4种状态
 */
 static int32_t __CFRunLoopRun(CFRunLoopRef rl, 
                               CFRunLoopModeRef rlm,
                               CFTimeInterval seconds, 
                               CFRunLoopModeRef previousMode) {
    // 借助GCD timer,设置runloop的超时时间
    dispatch_source_t timeout_timer = ...
    // 超时回调函数 __CFRunLoopTimeout
    dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout); 
    // 超时取消函数  __CFRunLoopTimeoutCancel
    dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel); 

    // 进入do while循环,开始run
    int32_t retVal = 0; // runloop run返回值,默认为0,会在do while中根据情况被修改,当不为0时,runloop退出
    do{
        mach_port_t livePort = MACH_PORT_NULL; // 用于记录唤醒休眠的RunLoop的mach port,休眠前是NULL
        __CFPortSet waitSet = rlm->_portSet; // 取当前mode所需要监听的mach port集合,用于唤醒runloop(__CFPortSet 实际上是unsigned int 类型)

        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);  // 通知 即将处理 Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // 通知 即将处理 sources

        // 处理提交到runloop的blocks
        __CFRunLoopDoBlocks(rl, rlm);

        // 处理 source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); 
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks(rl, rlm); // 处理提交的runloop的block
        }

        // 如果有source1被signaled,则不休眠,直接跳到handle_msg来处理source1
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg; 
         }

        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); // 通知observer before waiting

        // ****** 开始休眠 ******
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); // 调用__CFRunLoopServiceMachPort, 监听waitSet所指定的mach port端口集合, 如果没有port message,进入 mach_msg_trap, RunLoop休眠,直到收到port message或者超时

      // ****** 休眠结束 ******
      __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 通知observer, runloop醒了

handle_msg:; // 处理事件
     // 根据唤醒RunLoop的livePort值,来进行对应逻辑处理
     if (MACH_PORT_NULL == livePort) { // MACH_PORT_NULL: 可能是休眠超时,啥都不做
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
      } else if (livePort == rl->_wakeUpPort) { // rl->_wakeUpPort: 被其他线程或进程唤醒,啥都不做
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            // do nothing on Mac OS
      } else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) // rlm->_timerPort: 处理nstimer 消息
         if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
      }else if (livePort == dispatchPort) // dispatchPort:处理分发到main queue上的事件
      {
          __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
      }else {   // 其余的,肯定是各种source1 事件
          __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
          if (NULL != reply) { // 如果有需要回复soruce1的消息,则回复
                (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
           }
      }
     // ****** 到这里就结束了对livePort的处理 ******

     __CFRunLoopDoBlocks(rl, rlm);  // 处理提交到runloop的blocks

     // 检查runloop是否需要退出
    if (sourceHandledThisLoop && stopAfterHandle) { // case1. 指定了仅处理一个source 退出kCFRunLoopRunHandledSource
        retVal = kCFRunLoopRunHandledSource;
    } else if (timeout_context->termTSR < mach_absolute_time()) { // case2. RunLoop 超时
        retVal = kCFRunLoopRunTimedOut;
    } else if (__CFRunLoopIsStopped(rl)) { // case3. RunLoop 被终止
        __CFRunLoopUnsetStopped(rl);
        retVal = kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {  // case4. RunLoop Mode 被终止
        rlm->_stopped = false;
        retVal = kCFRunLoopRunStopped;
    } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { // case5. RunLoop Mode里面没有任何要被处理的事件了(没有source0,source1, timer,以及提交到当前runloop mode的block)
        retVal = kCFRunLoopRunFinished;
    }
    }while(0 == retVal);

    // runloop循环结束,返回退出原因
    return retVal;
}

总结一下:

  • RunLoop仅会对当前mode下的source,timer和observer进行处理
  • RunLoop在各个状态下会对observer发送相应通知。通知顺序是:
    进入RunLoop循环前: (1)kCFRunLoopEntry
    进入循环((2)–(4)反复循环):
    (2)kCFRunLoopBeforeTimers -> (3)kCFRunLoopBeforeSources -> (4)kCFRunLoopBeforeWaiting -> (5)kCFRunLoopAfterWaiting
    退出循环:(6)kCFRunLoopExit
  • RunLoop通过调用__CFRunLoopServiceMachPort,通过Mach Port监听Ports来实现休眠(陷入mach_msg_trap状态)。可以唤醒RunLoop的事件包括:
    (1) Mach Port监听超时
    (2)被其他线程\进程唤醒
    (3)有Timer事件需要执行
    (4)有提交到main queue上的block(当前RunLoop是main RunLoop时才有这种情况)
    (5)被source1唤醒
  • RunLoop退出的可能原因有:
    (1)RunLoop超时
    (2)RunLoop Mode超时
    (3)RunLoop被终止
    (4)RunLoop Mode被终止
    (5)RunLoop Mode里面source0, source1, timer队列都空了,没啥要处理了

当我们的APP没有任何事件处理时,通过XCode的APP暂停键,可以看到main RunLoop处于休眠状态的堆栈:
这里写图片描述

苹果用RunLoop实现的功能

事件响应

iOS设备的事件响应,是有RunLoop参与的。
提起iOS设备的事件响应,相信大家都会有一个大概的了解:

(1)用户触发事件->(2)系统将事件转交到对应APP的事件队列->(3)APP从消息队列头取出事件->(4)交由Main Window进行消息分发->(5)找到合适的Responder进行处理,如果没找到,则会沿着Responder chain返回到APP层,丢弃不响应该事件

这里涉及到两个问题,(3)到(5)步是由进程内处理的,而(1)到(2)步则涉及到设备硬件,iOS操作系统,以及目标APP之间的通信,通信的大致步骤是什么样的呢?
当我们的APP在接收到任何事件请求之前,main RunLoop都是处于mach_msg_trap休眠状态中的,那么,又是谁唤醒它的呢?

好,莫急,让我们慢慢分析。首先,我们用po命令,打印出APP的main RunLoop(注意,callback所对应的具体函数名称只能在模拟器中显示,可能和真机的访问控制有关)。一堆东西,慢慢看还是能够看懂的,没有耐心的同学也可以忽略,直接看下面的分析:

<CFRunLoop 0x6000001f7200 [0x1029cebb0]>{wakeup port = 0x1f03, stopped = false, ignoreWakeUps = false, 
current mode = kCFRunLoopDefaultMode,
common modes = <CFBasicHash 0x600000048c40 [0x1029cebb0]>{type = mutable set, count = 2,
entries =>
    0 : <CFString 0x103d52820 [0x1029cebb0]>{contents = "UITrackingRunLoopMode"}
    2 : <CFString 0x1029a47f0 [0x1029cebb0]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = <CFBasicHash 0x6080000491b0 [0x1029cebb0]>{type = mutable set, count = 14,
entries =>
    0 : <CFRunLoopSource 0x608000172300 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x10771b6c6)}}
    1 : <CFRunLoopSource 0x604000172b40 [0x1029cebb0]>{signalled = No, valid = Yes, order = -2, context = <CFRunLoopSource context>{version = 0, info = 0x6080000495a0, callout = __handleHIDEventFetcherDrain (0x1034e5dd8)}}
    2 : <CFRunLoopObserver 0x604000128a20 [0x1029cebb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x102b7d276), context = <CFArray 0x60400044da40 [0x1029cebb0]>{type = mutable-small, count = 1, values = (
    0 : <0x7fb024022048>
)}}
    3 : <CFRunLoopObserver 0x604000128980 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x102bad057), context = <CFRunLoopObserver context 0x7fb023600240>}
    4 : <CFRunLoopObserver 0x6040001288e0 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x102bacfdc), context = <CFRunLoopObserver context 0x7fb023600240>}
    5 : <CFRunLoopObserver 0x604000128ac0 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x102b7d276), context = <CFArray 0x60400044da40 [0x1029cebb0]>{type = mutable-small, count = 1, values = (
    0 : <0x7fb024022048>
)}}
    6 : <CFRunLoopObserver 0x608000127f80 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x1087f2648), context = <CFRunLoopObserver context 0x0>}
    8 : <CFRunLoopObserver 0x608000127da0 [0x1029cebb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x10316b1a9), context = <CFRunLoopObserver context 0x6080000df800>}
    11 : <CFRunLoopSource 0x608000172f00 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x6080000b1dc0, callout = FBSSerialQueueRunLoopSourceHandler (0x106e87821)}}
    15 : <CFRunLoopSource 0x604000173980 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 39943, subsystem = 0x107296f78, context = 0x6080000b1b80}}
    17 : <CFRunLoopSource 0x6040001729c0 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x60400014e0d0, callout = __handleEventQueue (0x1034e5dcc)}}
    18 : <CFRunLoopSource 0x604000172900 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 17935, subsystem = 0x103d09ce8, context = 0x0}}
    19 : <CFRunLoopSource 0x600000172000 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 22535, subsystem = 0x103d24088, context = 0x60000003c180}}
    21 : <CFRunLoopSource 0x60c000172900 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x4b03, callout = PurpleEventCallback (0x10771dbef)}}
}
,


// 各种RunLoop Modes
modes = <CFBasicHash 0x600000048d60 [0x1029cebb0]>{type = mutable set, count = 4,
entries =>

    // UITrackingRunLoopMode  
    2 : <CFRunLoopMode 0x608000190cf0 [0x1029cebb0]>{name = UITrackingRunLoopMode, port set = 0x5203, queue = 0x60800014de10, source = 0x608000190dc0 (not fired), timer port = 0x2d03, 
    sources0 = <CFBasicHash 0x608000049180 [0x1029cebb0]>{type = mutable set, count = 4,
entries =>
    0 : <CFRunLoopSource 0x608000172300 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x10771b6c6)}}
    3 : <CFRunLoopSource 0x6040001729c0 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x60400014e0d0, callout = __handleEventQueue (0x1034e5dcc)}}
    5 : <CFRunLoopSource 0x608000172f00 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x6080000b1dc0, callout = FBSSerialQueueRunLoopSourceHandler (0x106e87821)}}
    6 : <CFRunLoopSource 0x604000172b40 [0x1029cebb0]>{signalled = No, valid = Yes, order = -2, context = <CFRunLoopSource context>{version = 0, info = 0x6080000495a0, callout = __handleHIDEventFetcherDrain (0x1034e5dd8)}}
}
,
    sources1 = <CFBasicHash 0x608000049210 [0x1029cebb0]>{type = mutable set, count = 4,
entries =>
    1 : <CFRunLoopSource 0x604000172900 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 17935, subsystem = 0x103d09ce8, context = 0x0}}
    2 : <CFRunLoopSource 0x60c000172900 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x4b03, callout = PurpleEventCallback (0x10771dbef)}}
    3 : <CFRunLoopSource 0x600000172000 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 22535, subsystem = 0x103d24088, context = 0x60000003c180}}
    4 : <CFRunLoopSource 0x604000173980 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 39943, subsystem = 0x107296f78, context = 0x6080000b1b80}}
}
,
    observers = (
    "<CFRunLoopObserver 0x604000128a20 [0x1029cebb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x102b7d276), context = <CFArray 0x60400044da40 [0x1029cebb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fb024022048>\n)}}",
    "<CFRunLoopObserver 0x608000127da0 [0x1029cebb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x10316b1a9), context = <CFRunLoopObserver context 0x6080000df800>}",
    "<CFRunLoopObserver 0x6040001288e0 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x102bacfdc), context = <CFRunLoopObserver context 0x7fb023600240>}",
    "<CFRunLoopObserver 0x608000127f80 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x1087f2648), context = <CFRunLoopObserver context 0x0>}",
    "<CFRunLoopObserver 0x604000128980 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x102bad057), context = <CFRunLoopObserver context 0x7fb023600240>}",
    "<CFRunLoopObserver 0x604000128ac0 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x102b7d276), context = <CFArray 0x60400044da40 [0x1029cebb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fb024022048>\n)}}"
),
    timers = (null),
    currently 547996945 (171843335980762) / soft deadline in: 1.84465722e+10 sec (@ -1) / hard deadline in: 1.84465722e+10 sec (@ -1)
},


    // GSEventReceiveRunLoopMode
    3 : <CFRunLoopMode 0x608000190e90 [0x1029cebb0]>{name = GSEventReceiveRunLoopMode, port set = 0x4d03, queue = 0x60800014dec0, source = 0x608000190f60 (not fired), timer port = 0x4c03, 
    sources0 = <CFBasicHash 0x6080000492a0 [0x1029cebb0]>{type = mutable set, count = 1,
entries =>
    0 : <CFRunLoopSource 0x608000172300 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x10771b6c6)}}
}
,
    sources1 = <CFBasicHash 0x6080000492d0 [0x1029cebb0]>{type = mutable set, count = 1,
entries =>
    0 : <CFRunLoopSource 0x60c0001729c0 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x4b03, callout = PurpleEventCallback (0x10771dbef)}}
}
,
    observers = (null),
    timers = (null),
    currently 547996945 (171843337133040) / soft deadline in: 1.84465722e+10 sec (@ -1) / hard deadline in: 1.84465722e+10 sec (@ -1)
},

    // kCFRunLoopDefaultMode
    4 : <CFRunLoopMode 0x600000190a80 [0x1029cebb0]>{name = kCFRunLoopDefaultMode, port set = 0x1e03, queue = 0x60000014de10, source = 0x600000190b50 (not fired), timer port = 0x2a03, 
    sources0 = <CFBasicHash 0x608000049240 [0x1029cebb0]>{type = mutable set, count = 4,
entries =>
    0 : <CFRunLoopSource 0x608000172300 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x0, callout = PurpleEventSignalCallback (0x10771b6c6)}}
    3 : <CFRunLoopSource 0x6040001729c0 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x60400014e0d0, callout = __handleEventQueue (0x1034e5dcc)}}
    5 : <CFRunLoopSource 0x608000172f00 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x6080000b1dc0, callout = FBSSerialQueueRunLoopSourceHandler (0x106e87821)}}
    6 : <CFRunLoopSource 0x604000172b40 [0x1029cebb0]>{signalled = No, valid = Yes, order = -2, context = <CFRunLoopSource context>{version = 0, info = 0x6080000495a0, callout = __handleHIDEventFetcherDrain (0x1034e5dd8)}}
}
,
    sources1 = <CFBasicHash 0x608000049270 [0x1029cebb0]>{type = mutable set, count = 4,
entries =>
    1 : <CFRunLoopSource 0x604000172900 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 17935, subsystem = 0x103d09ce8, context = 0x0}}
    2 : <CFRunLoopSource 0x60c000172900 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 1, info = 0x4b03, callout = PurpleEventCallback (0x10771dbef)}}
    3 : <CFRunLoopSource 0x600000172000 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 22535, subsystem = 0x103d24088, context = 0x60000003c180}}
    4 : <CFRunLoopSource 0x604000173980 [0x1029cebb0]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource MIG Server> {port = 39943, subsystem = 0x107296f78, context = 0x6080000b1b80}}
}
,
    observers = (
    "<CFRunLoopObserver 0x604000128a20 [0x1029cebb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x102b7d276), context = <CFArray 0x60400044da40 [0x1029cebb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fb024022048>\n)}}",
    "<CFRunLoopObserver 0x608000127da0 [0x1029cebb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x10316b1a9), context = <CFRunLoopObserver context 0x6080000df800>}",
    "<CFRunLoopObserver 0x6040001288e0 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x102bacfdc), context = <CFRunLoopObserver context 0x7fb023600240>}",
    "<CFRunLoopObserver 0x608000127f80 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x1087f2648), context = <CFRunLoopObserver context 0x0>}",
    "<CFRunLoopObserver 0x604000128980 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x102bad057), context = <CFRunLoopObserver context 0x7fb023600240>}",
    "<CFRunLoopObserver 0x604000128ac0 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x102b7d276), context = <CFArray 0x60400044da40 [0x1029cebb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fb024022048>\n)}}"
),
    timers = <CFArray 0x6040000b1a60 [0x1029cebb0]>{type = mutable-small, count = 0, values = ()},
    currently 547996945 (171843337166767) / soft deadline in: 1.84465722e+10 sec (@ -1) / hard deadline in: 1.84465722e+10 sec (@ -1)
},


    // kCFRunLoopCommonModes
    5 : <CFRunLoopMode 0x600000190c20 [0x1029cebb0]>{name = kCFRunLoopCommonModes, port set = 0x300f, queue = 0x60000014df70, source = 0x600000190cf0 (not fired), timer port = 0x3a07, 
    sources0 = (null),
    sources1 = (null),
    observers = (null),
    timers = (null),
    currently 547996945 (171843338250981) / soft deadline in: 1.84465722e+10 sec (@ -1) / hard deadline in: 1.84465722e+10 sec (@ -1)
},

}
}

我们会发现,系统会向main RunLoop添加一堆的source,observer。
这里,我们只需要关注:
在kCFRunLoopDefaultMode,UITrackingRunLoopMode以及kCFRunLoopCommonModes下面均注册了的source0事件源:

<CFRunLoopSource 0x6040001729c0 [0x1029cebb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x60400014e0d0, callout = __handleEventQueue (0x1034e5dcc)}}

他的回调函数是__handleEventQueue,没错,APP就是通过这个回调函数来处理事件队列的。

但是,我们注意到了,__handleEventQueue 所对应的source类型是0,也就是说它本身不会唤醒休眠的main RunLoop, main线程自身在休眠状态中也不可能自己去唤醒自己,那么,系统肯定还有一个子线程,用来接收事件并唤醒main thread,并将事件传递到main thread上

聪明如你,没错,确实还一个子线程,我们将APP暂停,就会看到,除了主线程外,系统还为我们自动创建了几个子线程,其中有一个名字叫做

com.apple.uikit.eventfetch-thread

这里写图片描述

看线程的名字就知道,它是UIKit所创建的用于接收event的线程(以下我们简称为event fetch thread)。
我们打印出com.apple.uikit.eventfetch-thread的RunLoop。相比main RunLoop要简单很多,但一眼望去还是眼花,没有耐心的同学也可以直接跳到结论:

<CFRunLoop 0x6040001f6700 [0x107f54bb0]>{wakeup port = 0x1c03, stopped = false, ignoreWakeUps = true, 
current mode = kCFRunLoopDefaultMode,
common modes = <CFBasicHash 0x60400005f050 [0x107f54bb0]>{type = mutable set, count = 1,
entries =>
    2 : <CFString 0x107f2a7f0 [0x107f54bb0]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = <CFBasicHash 0x608000240c90 [0x107f54bb0]>{type = mutable set, count = 4,
entries =>
    2 : <CFRunLoopSource 0x608000167440 [0x107f54bb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x600000131940, callout = _UIEventFetcherTriggerHandOff (0x108d71599)}}
    3 : <CFRunLoopSource 0x604000167980 [0x107f54bb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x608000156f20 [0x107f54bb0]>{valid = Yes, port = 4c07, source = 0x604000167980, callout = __IOHIDEventSystemClientAvailabilityCallback (0x10b179281), context = <CFMachPort context 0x7f8b28d02ae0>}}
    4 : <CFRunLoopSource 0x604000167a40 [0x107f54bb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x608000156dc0 [0x107f54bb0]>{valid = Yes, port = 300f, source = 0x604000167a40, callout = __IOHIDEventSystemClientQueueCallback (0x10b17904d), context = <CFMachPort context 0x7f8b28d02ae0>}}
    5 : <CFRunLoopSource 0x6040001678c0 [0x107f54bb0]>{signalled = No, valid = Yes, order = 1, context = <CFMachPort 0x608000157080 [0x107f54bb0]>{valid = Yes, port = 4e0f, source = 0x6040001678c0, callout = __IOMIGMachPortPortCallback (0x10b185692), context = <CFMachPort context 0x6080000d3780>}}
}
,
modes = <CFBasicHash 0x60400005f080 [0x107f54bb0]>{type = mutable set, count = 1,
entries =>
    2 : <CFRunLoopMode 0x604000190740 [0x107f54bb0]>{name = kCFRunLoopDefaultMode, port set = 0x1d03, queue = 0x6040001569a0, source = 0x604000190810 (not fired), timer port = 0x2b03, 
    sources0 = <CFBasicHash 0x60800005f8f0 [0x107f54bb0]>{type = mutable set, count = 1,
entries =>
    0 : <CFRunLoopSource 0x608000167440 [0x107f54bb0]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x600000131940, callout = _UIEventFetcherTriggerHandOff (0x108d71599)}}
}
,
    sources1 = <CFBasicHash 0x608000240b10 [0x107f54bb0]>{type = mutable set, count = 3,
entries =>
    0 : <CFRunLoopSource 0x604000167a40 [0x107f54bb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x608000156dc0 [0x107f54bb0]>{valid = Yes, port = 300f, source = 0x604000167a40, callout = __IOHIDEventSystemClientQueueCallback (0x10b17904d), context = <CFMachPort context 0x7f8b28d02ae0>}}
    1 : <CFRunLoopSource 0x6040001678c0 [0x107f54bb0]>{signalled = No, valid = Yes, order = 1, context = <CFMachPort 0x608000157080 [0x107f54bb0]>{valid = Yes, port = 4e0f, source = 0x6040001678c0, callout = __IOMIGMachPortPortCallback (0x10b185692), context = <CFMachPort context 0x6080000d3780>}}
    2 : <CFRunLoopSource 0x604000167980 [0x107f54bb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x608000156f20 [0x107f54bb0]>{valid = Yes, port = 4c07, source = 0x604000167980, callout = __IOHIDEventSystemClientAvailabilityCallback (0x10b179281), context = <CFMachPort context 0x7f8b28d02ae0>}}
}
,
    observers = (null),
    timers = (null),
    currently 547998693 (173591812879054) / soft deadline in: 1.84465705e+10 sec (@ -1) / hard deadline in: 1.84465705e+10 sec (@ -1)
},

}
}

这里我们只需要知道,event fetch thread只有一个default mode(不算common modes的话),而在Default mode和common modes下,均注册了一个source1 :

<CFRunLoopSource 0x604000167a40 [0x107f54bb0]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x608000156dc0 [0x107f54bb0]>{valid = Yes, port = 300f, source = 0x604000167a40, callout = __IOHIDEventSystemClientQueueCallback (0x10b17904d), context = <CFMachPort context 0x7f8b28d02ae0>}}

它的回调是

__IOHIDEventSystemClientQueueCallback

哎~ 有门。既然这个是source1类型,则是可以被系统通过mach port唤醒event fetch thread的RunLoop, 来执行__IOHIDEventSystemClientQueueCallback回调的。

我们打上符号断点__IOHIDEventSystemClientQueueCallback, __handleEventQueue,然后点击button,测试执行流程。可以发现,会依次调用__IOHIDEventSystemClientQueueCallback,__handleEventQueue来处理事件。
具体流程可以参考这里iphonedevwiki

经测试,得出结论:

用户触发事件, IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收,SpringBoard会利用mach port,产生source1,来唤醒目标APP的com.apple.uikit.eventfetch-thread的RunLoop。Eventfetch thread会将main runloop 中__handleEventQueue所对应的source0设置为signalled == Yes状态,同时唤醒main RunLoop。mainRunLoop则调用__handleEventQueue进行事件队列处理。

手势识别

iOS的手势识别也依赖于RunLoop。

当系统识别出一个手势时,会打断touch系列的回调,同时更新标记对应手势的UIGestureRecognizer状态(Needing update?)

而UIKit会向main RunLoop注册一个observer(其实是分别在tracking mode,default mode和common modes下分别注册):

<CFRunLoopObserver 0x608000127da0 [0x1029cebb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x10316b1a9), context = <CFRunLoopObserver context 0x6080000df800>

这里查看一下它的activities属性0x20,结合CF中的位标记定义:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

可知该observer监听main RunLoop的kCFRunLoopBeforeWaiting事件。每当main RunLoop即将休眠时,该observer被触发,同时调用回调函数_UIGestureRecognizerUpdateObserver。_UIGestureRecognizerUpdateObserver会检测当前需要被更新状态的recognizer(创建,触发,销毁)。

如果有手势被触发,在_UIGestureRecognizerUpdateObserver回调中会借助UIKit一个内部类UIGestureEnvironment 来进行一系列处理。其中会向APP的event queue中投递一个gesture event,这个gesture event的处理流程应该和上面的事件处理类似的,内部会调用__handleEventQueueInternal处理该gesture event,并通过UIKit内部类UIGestureEnvironment 来处理这个gesture event,并最终回调到我们自己所写的gesture回调中。

这里写图片描述

界面刷新

当我们需要界面刷新,如UIView/CALayer 调用了setNeedsLayout/setNeedsDisplay, 或更新了UIView的frame,或UI层次。
其实,系统并不会立刻就开始刷新界面,而是先提交UI刷新请求,再等到下一次main RunLoop循环时,集中处理(集中处理的好处在于可以合并一些重复或矛盾的UI刷新)。而这个实现方式,则是通过监听main RunLoop的before waitting和Exit通知实现的。

我们在main RunLoop的observer里面,可以看到下面属性的observer(同样是注册在default,Tracking,common modes下),

<CFRunLoopObserver 0x608000127f80 [0x1029cebb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x1087f2648), context = <CFRunLoopObserver context 0x0>}

可以看到其注册事件是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit),它的优先级(order=2000000)比事件响应的优先级(order=0)要低。

该observer的回调是

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv

其内部会调用

CA::Transaction::observer_callback // 位于QuartzCore 中 

在该函数中,会将所有的界面刷新请求提交,刷新界面,以及调用相关回调:
这里写图片描述

NSTimer

NSTimer在CF层对应的结构如下,它们之间的转换时toll-free的:

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;         // timer对应的runloop
    CFMutableSetRef _rlModes;      // timer对应的runloop modes
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;       /* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;          /* timer本次需要被触发的时间, TSR units */
    CFIndex _order;         /* immutable */
    CFRunLoopTimerCallBack _callout;    /* timer 回调 immutable */
    CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

Timer的触发流程大致是这样的:
(1)用户添加timer到runloop的某个或几个mode下->
(2)根据timer是否设置了tolerance,如果没有设置,则调用底层xnu内核的mk_timer注册一个mach-port事件,如果设置了tolerance,则注册一个GCD timer->
(3)当由XNU内核或GCD管理的timer的 fire time到了,通过对应的mach port唤醒RunLoop(mk_timer对应rlm的_timerPort, GCD timer对应GCD queue port)->
(4)RunLoop执行__CFRunLoopDoTimers,其中会调用__CFRunLoopDoTimer, DoTimer方法里面会根据当前mach time和Timer的fireTSR属性,判断fireTSR 是否< 当前的mach time,如果小于,则触发timer,同时更新下一次fire时间fireTSR。

这样一次timer事件从注册到执行大致流程如图所示:

这里写图片描述

如果想更详细的了解timer的底层实现,可以参考这里:从RunLoop源码探索NSTimer的实现原理

timer事件触发时的函数堆栈:
这里写图片描述

PerformSelector:afterDealy

PerformSelector:afterDealy系列函数的底层实现实际上是在目标线程的RunLoop的default mode下添加了一个timer,因此,在没有RunLoop启动的线程下,该函数是无法使用的。

PerformSelectorOnThread(mainThread):

其他的PerformSelector到指定thread的函数,也是基于RunLoop实现的。当我们调用PerformSelectorOnThread(mainThread)时,系统会向目标thread的Default mode下添加一个source0,在source0的回调中,会执行我们的selector。

因此,在目标线程没有启动RunLoop的情况下,PerformSelectorOnThread系列函数也是不能生效的。

这里写图片描述

dispatch to main queue

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

当我们在dispatch main queue block中下断点时,可以看到如下调用堆栈:
这里写图片描述

注意,这里仅对提交到main queue上的block系统是这么处理的。

网络请求

NSURLConnection

在iOS 7之前,我们是使用NSURLConnection来进行网络传输的。NSURLConnection底层的网络回调是基于发起网络请求的线程的RunLoop的
这就是说,如果网络请求是在子线程中发起的,如果子线程的RunLoop没有run,或子线程在网络请求结果返回前退出,NSURLConnection是不会生效的。

关于NSURLConnection和RunLoop的具体关系,可以参考这里:
深入理解RunLoop

NSURLSession

毕竟,NSURLConnection已经是老古董了,已经不推荐使用(查看NSURLConnection的调用堆栈,会发现网络的cache还会用sqlite来存,而且还必须依赖于RunLoop,确实挺low的)。

在iOS 7之后的版本中,我们应该使用NSURLSession进行网络请求。而NSURLSession的实现是基于GCD的,已经和RunLoop完全解耦。

NSURLSession基于GCD的实现意味着,我们可以在任意线程发起网络请求,而不必考虑RunLoop是否在Run,并且不用关心在网络请求返回前,发起请求线程是否退出。

RunLoop应用实例

我们何时需要RunLoop? 苹果给我们列出了以下情境:

  • 需要使用port或input source和别的线程通信(基本用不到)
  • 在当前线程需要使用NSTimer
  • 在Cocoa程序中需要使用performSelector… 系列函数
  • 需要子线程保活来执行后台任务

线程保活

在文章开头我们就提到,RunLoop的一个作用就在于让线程跑圈,不至于退出。如果我们自己创建一个子线程而没有让它的RunLoop一直run的话,线程会立刻退出。

我们可以用如下代码,让子线程一直等待一个mach port,从而维持RunLoop 不退出循环:

    @autoreleasepool{
      NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
      self.messagePort = [NSPort port];
      self.messagePort.delegate = self;
      [runLoop addPort:self.messagePort forMode:NSDefaultRunLoopMode];
      [runLoop run]; // 当runloop 在跑圈时,下面的代码永远不会执行。
      NSLog(@"runloop exit!!");  // 仅当子线程即将退出时,才会执行到这里
   }

之所以要在代码外包一层autoreleasepool,是因为系统不会为子线程自动创建autorelease pool,为了防止内存泄漏,我们在所有的子线程开始都要创建一个autorelease pool。

这里我们为当前runloop放了一个mach port,让runloop一直空等。所以,只要我们将该port remove出runloop,runloop一直不会停掉。(除非超时或被别的线程或进程停掉)。
可以调用如下代码,让runloop退出:

[subThreadRunLoop removePort:self.messagePort forMode:NSDefaultRunLoopMode];

这里涉及到一个问题,就是我们如何退出一个runloop。有两种方式:

  • 指定一个超时时间
  • 调用void CFRunLoopStop(CFRunLoopRef rl) 来干掉runloop

这两个方法都是可行的,runloop会在退出前执行一些清理工作,同时,会向observer发送通知。

虽然我们也可以通过移除runloop所有的input source和timer来让其退出,但这种方法是不可靠的。因为有时系统可能会注册一些input source或timer,这些是不受我们控制的。

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架。
其核心思想是将能够放到后台线程处理的界面任务,尽量都在后台执行。只有最终展示时,才返回到main Thread。
这其中用到了RunLoop的相关概念。但是鉴于笔者对该框架并不了解,有兴趣的同学可以自己查找资料。

RunLoop线程安全

在CF层,CFRunLoopRef线程安全的。

而在Cocoa 层, NSRunLoop则不是线程安全的,我们必须保证在NSRunLoop对应的线程来进行任何的runloop操作,否则会发生不可预料的结果。

RunLoop相关常见面试题

关于RunLoop是在面试中经常会问到的问题,下面列举出一些,相信经过我们的梳理,大家应该能够比较好的回答上来。如果有任何疑问,可以在评论中留言提问,这里答案我就不再写上了。

  1. RunLoop 原理?(其实就是解释RunLoop是个啥,能做啥,怎么做到的,重点在于mach port的休眠/唤醒机制)
  2. RunLoop应用场景?
  3. RunLoop和线程之间有什么关系?
  4. 如果让你写的话,该如何实现RunLoop?(可以写伪代码就行)

总结

OK,到这里,我们就对RunLoop有个较全面的理解了。
其实RunLoop并不神秘,只要搞懂了它的原理,一切就理顺了。
关于RunLoop,重点在于搞懂基于mach port的休眠/唤醒机制、runloop mode,input source,timer和observer这四个概念的理解,以及iOS是如何利用这些东西实现系统功能的。

参考资料

深入理解RunLoop
sunnyxx的runloop视频(优酷)
RunLoop总结:RunLoop的应用场景(四)App卡顿监测
CFRunLoopRun源码标注
Apple Run Loops
老司机出品——源码解析之RunLoop详解
从RunLoop源码探索NSTimer的实现原理

再推荐一个吊炸天的Wiki:
iPhoneDevWiki

猜你喜欢

转载自blog.csdn.net/u013378438/article/details/80239686