高薪·进阶之 RunLoop 篇

3,369 阅读5分钟

概述

什么是 RunLoop

Run Loop: 顾名思义是一种循环,不同于其他的循环,runloop 是一种闲等待,没有 event 则进入休眠,否则找到对应的 handler 处理。 Run Loop 接收两个不同类型资源的 eventInput/Timer sources

anatomy_runloop.jpg

Runloop 和线程是绑定在一起的。每个线程(包括主线程)都有一个对应的 Runloop 对象。我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象。 主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动

Run Loop Modes

RunLoopMode 是被监听资源和观察者的集合,每次运行 runloop 都需要一个特殊的 mode,在运行过程中,只有关联的 mode 是被监听和接受 event 的,也就是说没有 mode,runloop 是无法 handle event。Apple 在Cocoa和Core Foundation 中有不同的声明,大部分时间我们直接使用系统定义的 “default” mode。

Mode Name Description
Default NSDefaultRunLoopMode(Cocoa)、kCFRunLoopDefaultMode(Core Foundation) 使用最多,大部分场景下使用都可以启动 runloop
Connection NSConnectionReplyMode 处理 NSConnection 对象相关事件,系统内部使用,这个 mode 表明 NSConnection 对象等待 reply,用户基本不会使用。
Modal NSModalPaneRunLoopMode 处理 modal panels 事件,需要等待处理的 input source 为 modal panel 时设置,比如 NSSavePanel 和 NSOpenPanel
Event tracking NSEventTrackingRunLoopMode 使用该模式来处理用户界面相关的事件,例如在拖动 loop 或其他 user interface tracking loops 时处于此种模式下,在此模式下会限制输入事件的处理。例如,当手指按住 UITableView 拖动时就会处于此模式
Common NSRunLoopCommonModes(Cocoa)、kCFRunLoopCommonModes(Core Foundation) 这是一个伪模式,其为一组 run loop mode 的集合,将输入源加入此模式意味着在 Common Modes 中包含的所有模式下都可以处理。在 Cocoa 应用程序中,默认情况下 Common Modes 包含 default modes,modal modes,event Tracking modes, 可使用 CFRunLoopAddCommonMode 方法向 Common Modes中添加自定义 modes

Sources(Input/Timer)

事件源:目标监测源,使 RunLoop 在需要的时候唤醒线程,没事的时候 sleep

Input Sources

input source 主要是一些异步的事件,比如来自其它线程或者其它app的消息

  • Port-Based Sources 系统底层的 Port 事件,例如 CFSocketRef ,在应用层基本用不到
  • Custom Input Source 用户手动创建的 Source
  • Cocoa Perform Selector Sources Cocoa 提供的 performSelector 系列方法,也是一种事件源

Timer Source

顾名思义就是指定时器事件了。 创建 NSTimer 添加到 run loop 中的时候,这里需要注意的是,NSTimer 默认是处于 NSDefaultRunloopMode,这也就可以解释为什么如果你在你的控制器中添加了一个 timer 定时刷新你的界面,而你在拖动视图的时候 timer 不回 fire,因为这个时候你的 runloop 是 NSEventTrackingRunloopMode,在这个 mode 下 timer 不回 fire

Observers

RunLoop 通过监听 Source 来决定没有任务要做,当然我们也可以使用 RunLoop Observer 来监控 RunLoop 本身的状态。可以监听以下的事件:

  • RunLoop 的开始
  • 即将开始 Timer 处理
  • 即将开始 Source 处理
  • 即将进入休眠
  • 从休眠状态唤醒
  • 退出 RunLoop

伴随着监听到的事件,runloop 也会将事件传至事件观察者,根据苹果的文档,大概是下面的流程:

RunLoop_process.png

RunLoop和线程

  • 每条线程都有唯一的 RunLoop 对象与之对应
  • main 线程颐景创建,自线程需要主动创建
  • RunLoop 在第一次获取的时候创建,在线程结束时销毁

需要注意的是,Apple 不允许直接创建 RunLoop,但可以通过 CFRunLoopGetMain()CFRunLoopGetCurrent() 两个方法获取 CFRunLoopRef 源码:

// 全局的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());
}

应用

AutoreleasePool

iOS 应用启动后会注册两个 Observer 管理和维护 AutoreleasePool,

UI更新

比如 AsyncDisplayKit,其在主线程 RunLoop 中添加一个 Observer 监听即将进入休眠和退出 RunLoop 两种状态,然后在回调中遍历队列中的待处理任务。

GCD

调用 GCD 中 dispatch_async() 时会像主线程的 RunLoop 发送消息唤醒 RunLoop,不过这个操作只限于主线程,其他线程 dispatch 都是由 libDispatch 驱动的

问答

  1. 延迟执行 performSelector 相关方法是怎样被执行?在子线程内也一样么?
  2. 事件响应和手势识别底层处理是一致么?为什么?
  3. 界面刷新时,是在什么时候会执行真正的刷新,为什么会不及时?
  4. 自动释放池的创建、销毁都是在什么时候发生的?
  5. 当子线程需要执行回调时,怎么确保当前线程没有被销毁?

答案请参考 iOS RunLoop 详解

参考

深入理解 RunLoop Apple 文档 iOS刨根问底-深入理解 RunLoop

更多阅读,请关注 SwiftOldBird 官方微信公众号

微信公众号

原文来自:swiftoldbird.loveli.site/2019/08/19/…