RunLoop

633 阅读16分钟

RunLoop

概念

Runloop,顾名思义就是跑圈,他的本质就是一个do,while循环,当有事做时就做事,没事做时就休眠。Runloop让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

看apple官方文档(多线程编程指南)描述: “run loop 是用来在线程上管理事件异步到达的基础设施......run loop在没有任何事件处理的时候会把它的线程置于休眠状态,它消除了消耗CPU周期轮询,并防止处理器本身进入休眠状态并节省电源。" 看见没,消除CPU空转才是它最大的用处。

RunLoop 和线程的关系

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。

苹果并没有为我们提供一个可以直接创建Runloop的接口,但是我们可以通过CFRunLoopGetMain()和CFRunLoopGetCurrent()两个方法来获取RunLoop对象

主线程的 RunLoop 是默认开启的,主线程的 RunLoop 的任务主要包含如下几项:

  1. 处理触摸事件
  2. 发送和接收网络数据包
  3. 执行使用 GCD 的代码
  4. 处理计时器行为
  5. 屏幕重绘

对于子线程,需要判断是否需要运行循环,如果需要,自行配置启动。您不需要在所有情况下都启动线程的运行循环。例如,如果您使用线程执行一些长时间运行的预定任务,您可能可以避免启动运行循环。运行循环适用于您希望与线程进行更多交互的情况。例如,如果您打算执行以下任何操作,则需要启动运行循环:

  1. 使用端口或自定义输入源与其他线程进行通信。
  2. 在线程上使用计时器。
  3. performSelector在 Cocoa 应用程序中 使用任何…方法。
  4. 保留线程以执行定期任务。

RunLoop 对外的接口

Mode

一个RunLoop包含了多个Mode,每个Mode又包含了若干个Source/Timer/Observer。每次调用 RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同Mode中的Source/Timer/Observer,让其互不影响。

kCFDefaultRunLoopMode App的默认Mode,通常主线程是在这个Mode下运行 ( 标准优先级)

UITrackingRunLoopMode 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响 ( 用于 UIScrollView 和别的控件的动画 )

UIInitializationRunLoopMode 在刚启动App时进入的第一个Mode,启动完成后就不再使用

GSEventReceiveRunLoopMode 接受系统事件的内部Mode,通常用不到

kCFRunLoopCommonModes 这是一个占位用的Mode,不是一种真正的Mode ( 高优先级 ) 目前被标记为Common Modes的模式:kCFRunLoopDefaultModeUITrackingRunLoopMode

Source

Source0

即非基于 port的,也就是用户自定义的事件。需要手动唤醒线程

Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

Source1

Source1 基于mach_Port的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop。mach_port大家就理解成进程间相互发送消息的一种机制就好。

Timer

基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop 执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。

NSTimer其实就是CFRunLoopTimerRef,他们之间是toll一free bridged的。一个NSTimer注册到RunLoop后, RunLoop会为其重复的时间点注册好事件。例如10:00,10:10,10:20这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个Timer。 Timer有个属性叫做Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

iOS中常NSTimer、CADisplayLink、GCD定时器,其中NSTimer、CADisplayLink基于NSRunLoop实现,故存在误差,GCD定时器只依赖系统内核,相对一前两者是比较准时的。

Observer

除了处理输入源之外,运行循环还生成有关运行循环行为的通知。注册的运行循环观察者可以接收这些通知并使用它们在线程上进行额外的处理。

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

kCFRunLoopEntry

RunLoop准备启动

kCFRunLoopBeforeTimers

RunLoop 将要处理一些Timer 相关事件

kCFRunLoopBeforeSources

RunLoop 将要处理一些Source 事件

kCFRunLoopBeforeWaiting

RunLoop 将要进行休眠状态,即将由用户态切换到内核态

kCFRunLoopAfterWaiting

RunLoop被唤醒,即从内核态切换到用户态后

kCFRunLoopExit

RunLoop退出

kCFRunLoopAllActivities

监听所有状态

Runloop执行流程

RunLoop.png

/// 用DefaultMode启动

void CFRunLoopRun(void) {

    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);

}

 

/// 用指定的Mode启动,允许设置RunLoop超时时间

int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {

    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);

}

 

/// RunLoop的实现

int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

    

    /// 首先根据modeName找到对应mode

    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);

    /// 如果mode里没有source/timer/observer, 直接返回。

    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    

    /// 1. 通知 Observers: RunLoop 即将进入 loop。

    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

    

    /// 内部函数,进入loop

    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

        

        Boolean sourceHandledThisLoop = NO;

        int retVal = 0;

        do {

 

            /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。

            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);

            /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。

            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);

            /// 执行被加入的block

            __CFRunLoopDoBlocks(runloop, currentMode);

            

            /// 4. RunLoop 触发 Source0 (非port) 回调。

            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);

            /// 执行被加入的block

            __CFRunLoopDoBlocks(runloop, currentMode);

 

            /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。

            if (__Source0DidDispatchPortLastTime) {

                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)

                if (hasMsg) goto handle_msg;

            }

            

            /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。

            if (!sourceHandledThisLoop) {

                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);

            }

            

            /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。

            /// • 一个基于 port 的Source 的事件。

            /// • 一个 Timer 到时间了

            /// • RunLoop 自身的超时时间到了

            /// • 被其他什么调用者手动唤醒

            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {

                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg

            }

 

            /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。

            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

            

            /// 收到消息,处理消息。

            handle_msg:

 

            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。

            if (msg_is_timer) {

                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())

            } 

 

            /// 9.2 如果有dispatch到main_queue的block,执行block。

            else if (msg_is_dispatch) {

                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

            } 

 

            /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件

            else {

                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);

                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);

                if (sourceHandledThisLoop) {

                    mach_msg(reply, MACH_SEND_MSG, reply);

                }

            }

            

            /// 执行加入到Loop的block

            __CFRunLoopDoBlocks(runloop, currentMode);

            

 

            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;

            }

            

            /// 如果没超时,mode里没空,loop也没被停止,那继续loop。

        } while (retVal == 0);

    }

    

    /// 10. 通知 Observers: RunLoop 即将退出。

    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

}

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

runloop.png

  1. 通知 Observers: 进入 Loop
  2. 通知 Observers: 即将处理 Timers
  3. 通知 Observers: 即将处理 Sources
  4. 处理 Source0 (可能会再次处理 Blocks)
  5. 如果存在 Sourcel1,就跳转到第 9 步)
  6. 通知 Observers 即将休眠
  7. 休眠,等待消息唤醒,直到发生以下事件之一:
    • 基于端口的输入源(Sourcel1)的事件到达。
    • 计时器(Timer)触发。
    • 为运行循环设置的超时值到期。
    • 运行循环被显式唤醒。
  8. 通知 Observers: 线程刚被唤醒
  9. 处理挂起的事件
    • 如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第 2 步。
    • 如果触发了输入源,则传递事件。
    • 如果运行循环被显式唤醒但尚未超时,则重新启动循环。转到第 2 步。
  10. 通知 Observers: 退出 Loop
    • 告诉运行循环停止
    • 启动时设置超时时间:
      • runUntilDate:
      • runMode:beforeDate:
    • 没有输入源时 runloop 也会退出;但是苹果并不建议我们这么做,因为系统内部有可能会在当前线程的 runloop 中添加一些输入源,所以通过手动移除 input source 或者 timer 这种方式,并不能保证 runloop 一定会退出。
    • 线程结束时也会结束其对应的 runloop

loop 的六个状态

通过对 RunLoop 原理的分析,我们可以看出在整个过程中,loop 的状态包括 6 个,其代码定义如下:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry , // 进入 loop
    kCFRunLoopBeforeTimers , // 触发 Timer 回调
    kCFRunLoopBeforeSources , // 触发 Source0 回调
    kCFRunLoopBeforeWaiting , // 等待 mach_port 消息 用户态 —> 内核态
    kCFRunLoopAfterWaiting ), // 接收 mach_port 消息 内核态 -> 用户态
    kCFRunLoopExit , // 退出 loop
    kCFRunLoopAllActivities  // loop 所有状态改变
}

如果 RunLoop 的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步的话,就可以认为是线程受阻了。如果这个线程是主线程的话,表现出来的就是出现了卡顿。

所以,如果我们要利用 RunLoop 原理来监控卡顿的话,就是要关注这两个阶段。RunLoop 在进入睡眠之前和唤醒后的两个 loop 状态定义的值,分别是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting ,也就是要触发 Source0 回调和接收 mach_port 消息两个状态。

接下来,我们就一起分析一下,如何对 loop 的这两个状态进行监听,以及监控的时间值如何设置才合理。

如何用RunLoop原理去监控卡顿

要想监听 RunLoop,你就首先需要创建一个 CFRunLoopObserverContext 观察者,代码如下:

CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);

将创建好的观察者 runLoopObserver 添加到主线程 RunLoop 的 common 模式下观察。然后,创建一个持续的子线程专门用来监控主线程的 RunLoop 状态。

一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿。接下来,我们就可以 dump 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长。

收到kCFRunLoopBeforeSources通知发出后开始处理事件,处理完之后状态会改变成kCFRunLoopBeforeWaiting。收到kCFRunLoopAfterWaiting也会开始处理事件,处理完之后变成kCFRunLoopBeforeTimers。 也就是说如果长时间停留在kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting状态,那么就发生了卡顿。

各数据结构之间的联系

RunLoop与mode的关系是一对多的关系,mode与timer、source、observer也是一对多的关系

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

RunLoop 的底层实现

从上面代码可以看到,RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。
RunLoop_3

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

我们在深入看一下 Darwin 这个核心的架构:
RunLoop_4

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

Mach 本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的 API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

Mach 的消息定义是在 <mach/message.h> 头文件的,很简单:

typedef struct {

  mach_msg_header_t header;

  mach_msg_body_t body;

} mach_msg_base_t;

typedef struct {

  mach_msg_bits_t msgh_bits;

  mach_msg_size_t msgh_size;

  mach_port_t msgh_remote_port;

  mach_port_t msgh_local_port;

  mach_port_name_t msgh_voucher_port;

  mach_msg_id_t msgh_id;

} mach_msg_header_t;

一条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port,
发送和接受消息是通过同一个 API 进行的,其 option 标记了消息传递的方向:

mach_msg_return_t mach_msg(

mach_msg_header_t *msg,

mach_msg_option_t option,

mach_msg_size_t send_size,

mach_msg_size_t rcv_size,

mach_port_name_t rcv_name,

mach_msg_timeout_t timeout,

mach_port_name_t notify);

为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:
RunLoop_5

这些概念可以参考维基百科: System_callTrap_(computing)

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

关于具体的如何利用 mach port 发送信息,可以看看 NSHipster 这一篇文章,或者这里的中文翻译 。

关于Mach的历史可以看看这篇很有趣的文章:Mac OS X 背后的故事(三)Mach 之父 Avie Tevanian

RunLoop 和 AutoreleasePool 的关系

主线程

App 启动后,苹果在主线程RunLoop 里注册了两个Observer。其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。

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

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

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

子线程

在多线程中,因为子线程中可能会使用便利构造器等方法来创建对象(类方法),那么这些对象的释放只能放在自动释放池中,此时需要在子线程中添加自动释放池。

使用便利构造器等方法来创建对象是autorelease的对象,需要自动释放池才能释放

默认主线的运行循环(runloop)是开启的,子线程的运行循环(runloop)默认是不开启的,也就意味着子线程中不会创建autoreleasepool,所以需要我们自己在子线程中创建一个自动释放池。(子线程里面使用的类方法都是autorelease,就会没有池子可释放,也就意味着后面没有办法进行释放,造成内存泄漏。)----在主线程中如果产生事件那么runloop才回去创建autoreleasepool,通过这个道理我们就知道为什么子线程中不会创建自动释放池了,因为子线程的runloop默认是关闭的,所以他不会自动创建autoreleasepool,需要我们手动添加 NSThread和NSOperationQueue开辟子线程需要手动创建autoreleasepool,GCD开辟子线程不需要手动创建autoreleasepool,因为GCD的每个队列都会自行创建autoreleasepool

总结

看到这里,相信你应该对 Objective-C 的内存管理机制有了更进一步的认识。通常情况下,我们是不需要手动添加 autoreleasepool 的,使用线程自动维护的 autoreleasepool 就好了。根据苹果官方文档中对 Using Autorelease Pool Blocks 的描述,我们知道在下面三种情况下是需要我们手动添加 autoreleasepool 的:

  1. 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
  2. 如果你编写的循环中创建了大量的临时对象;
  3. 如果你创建了一个辅助线程。

AFNetworking 中如何运用 Runloop?

AFURLConnectionOperation这个类是基于 NSURLConnection构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个RunLoop,这个线程就是常驻线程:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
    [[NSThread currentThread] setName:@"AFNetworking"J;
    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:selfselector:@selector(networkRequestThreadEntryPoint:)
object:nil];
    [_networkRequestThread start];
    });
    return _networkRequestThread;
}
  • AFNetworking 在 3.0 版本时,使用苹果公司新推出的 NSURLSession 替换了 NSURLConnection,从而避免了常驻线程这个坑。

  • NSURLSession 可以指定回调 NSOperationQueue,这样请求就不需要让线程一直常驻在内存里去等待回调了。

Crash起死回生

runloop 依赖于 mode 进行循环,自己建立一个 runloop,把崩溃前的 runloopmode 保存起来,让他一直转圈圈,让它不崩溃,把崩溃的堆栈进行保存上传服务器,进行处理,然后再进行崩溃,也可以让他不崩溃。

关于runloop的更多内容可参考:

深入理解RunLoop