iOS RunLoop 介绍

732 阅读4分钟

原文地址

RunLoop

一般来说,一个线程一次只能执行一个任务,任务执行完成后线程就会退出。为了保持线程的忙碌状态并在没有任务时将线程置于休眠状态,我们需要一种机制,这就是运行循环(RunLoop)。 截屏2024-08-12 17.41.30.png 简单来说,运行循环是一个事件驱动的大循环,它确保线程在有任务需要处理时保持忙碌状态,并在没有任务时进入休眠状态。下面是一个简单的伪代码示例,展示了运行循环的基本结构:

int main(int argc, char * argv[]) {
     //程序一直运行状态
     while (AppIsRunning) {
          //睡眠状态,等待唤醒事件
          id whoWakesMe = SleepForWakingUp();
          //得到唤醒事件
          id event = GetEvent(whoWakesMe);
          //开始处理事件
          HandleEvent(event);
     }
     return 0;
}

Runloop Mode

运行循环模式是一种机制,用于管理运行循环中的输入源(input sources)、定时器(timers)以及运行循环观察者(run loop observers)。

通过这种机制,运行循环可以在不同的模式下运行,以控制不同类型的事件源和任务,确保只有与当前模式相关联的源被处理,从而增强了对事件源和观察者的精确控制能力。一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。

系统提供了以下几种常见的 Mode :

  • NSDefaultRunLoopMode: 默认 Mode,通常主线程是在这个 Mode 下运行的。
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • NSRunLoopCommonModes:这是一个可配置的常用模式组合。将输入源与这个模式关联也会将其与组中的每个模式关联起来。Timer 计时会被 scrollView 的滑动影响的问题可以通过将 timer 添加到NSRunLoopCommonModes来解决。

事件源

CFRunLoopSource 是对 input sources 的抽象。CFRunLoopSource 分为 source0 和 source1 两种类型:

Source1 是内核自动发送的,包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息。

Source0 是App内部事件,由 App 自己管理的 UIEvent、CFSocket 都是 source0,只包含了一个回调,它并不能主动触发事件。使用时,需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

timer 基于时间的触发器,在未来的指定时间将事件同步传递到线程。通过 CFRunLoopTimerRef 来实现,它和 NSTimer 是 toll-free bridged 的。当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到达时,RunLoop 会被唤醒以执行那个回调。

// timer 会自动添加到当前线程的 NSDefaultRunLoopMode 中
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    // 执行定时器触发的操作
    self.timerTick(timer)
}

// 将 timer 手动添加到当前线程的 NSRunLoopCommonModes 中
let timer = Timer(timeInterval: 1.0, repeats: true) { timer in
    // 执行定时器触发的操作
    self.timerTick(timer)
}

RunLoop.current.add(timer, forMode: .common)

运行循环观察者

Runloop 会在特定时刻发送状态变化的通知。当 RunLoop 的状态发生变化时,观察者就能通过回调来接收这个变化。Runloop 可观察时间点有以下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入 Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   // 观察所有状态变化
};

Runloop 事件队列

每次运行 runloop, 线程的 runloop 会处理挂起的事件,并通知相关的观察者。其具体的流程如下: 截屏2024-08-12 17.41.47.png

源码地址:opensource.apple.com/source/CF/C…

参考

  1. blog.ibireme.com/2015/05/18/…
  2. developer.apple.com/library/arc…