五.iOS RunLoop

5 阅读6分钟

RunLoop

面试回答版

一句话概括

RunLoop 是 iOS 基于 mach_msg 实现的线程事件循环机制,它可以管理输入源、定时器、观察者,并在没有事件时让线程进入休眠以节省 CPU。


RunLoop 与线程的关系

  • 每个线程有且仅有一个 RunLoop(通过 TLS 线程局部存储绑定)
  • 主线程的 RunLoop 在 UIApplicationMain 中自动创建并运行
  • 子线程的 RunLoop 默认不创建,调用 [NSRunLoop currentRunLoop] 时懒加载创建
  • 子线程 RunLoop 需要手动 run 才能启动,且需要 Source/Timer 维持
  • RunLoop 不能直接创建,只能通过 CFRunLoopGetMain() / CFRunLoopGetCurrent() 获取

五个核心类

+---------------------------+------------------------------------------+
| CFRunLoop                 | 事件循环本身,包含多个 Mode               |
| CFRunLoopMode             | 事件运行模式,一个 RunLoop 一次只能在一个  |
|                           | Mode 下运行                              |
| CFRunLoopSource (source0  | 输入源:source0(手动触发) / source1       |
|             / source1)    | (基于 mach_port 内核消息唤醒)             |
| CFRunLoopTimer            | 定时器(NSTimer 的底层)                  |
| CFRunLoopObserver         | 观察者,监听 RunLoop 各种状态变化         |
+---------------------------+------------------------------------------+
CFRunLoopMode

RunLoop 一次运行只能在一个 Mode 下。切换 Mode 时会退出当前循环,再进入新 Mode。

Mode说明
kCFRunLoopDefaultMode默认 mode,平时在此 mode 下运行
UITrackingRunLoopMode滑动追踪 mode(UIScrollView 滚动时)
kCFRunLoopCommonModes不是真正的 mode,是一个 Mode 集合标记
UIInitializationRunLoopModeApp 启动时进入,启动后不再使用
GSEventReceiveRunLoopMode图形事件接收

CommonModes

  • kCFRunLoopCommonModes 是一组 Mode 的标签
  • 默认包含 DefaultModeTrackingMode
  • 添加到 CommonModes 的 source/timer 会在集合内的所有 mode 下执行
  • 常用于解决 NSTimer 在滑动时不触发的问题

RunLoop 内部逻辑

CFRunLoopRun()
  │
  ├─ 1. 通知 Observer:kCFRunLoopEntry(即将进入 RunLoop)
  │
  ├─ 2. 通知 Observer:kCFRunLoopBeforeTimers(即将处理 Timer)
  │
  ├─ 3. 通知 Observer:kCFRunLoopBeforeSources(即将处理 Source)
  │
  ├─ 4. 执行 blocks(dispatch到当前 RunLoop 的 block)
  │
  ├─ 5. 处理 Source0(非 mach_port 事件,需手动唤醒)
  │     └─ 可能被 dispatch_async 到 main queue 的 block
  │
  ├─ 6. 如果有 Source1 → 跳到步骤 9
  │
  ├─ 7. 通知 Observer:kCFRunLoopBeforeWaiting(即将休眠)
  │
  ├─ 8. 休眠等待 mach_msg(内核态等待)
  │     ├─ Source1(mach_port 消息)唤醒
  │     ├─ Timer 到时唤醒
  │     ├─ 外部手动唤醒(CFRunLoopWakeUp)
  │     └─ 超时
  │
  ├─ 9. 通知 Observer:kCFRunLoopAfterWaiting(被唤醒)
  │
  ├─ 10. 处理事件
  │      ├─ Timer 到点 → 处理 Timer
  │      ├─ Source1 到来 → dispatch 到 Source0 处理
  │      └─ 手动唤醒 → 处理 block
  │
  ├─ 11. 通知 Observer:kCFRunLoopExit(即将退出)
  │
  └─ 12. 循环回到步骤 2(除非被要求退出)
休眠机制
有事件 → 处理事件(用户态)
    ↓
无事件 → mach_msg 系统调用(用户态 → 内核态)
    ↓
内核态等待事件到来(线程休眠,不消耗 CPU)
    ↓
事件到达 → mach_msg 返回(内核态 → 用户态)
    ↓
处理事件

这是 RunLoop 省电的核心机制:没有事件时线程在内核态休眠,不消耗 CPU 时间片


卡顿监控原理

// 使用 CFRunLoopObserver 监听 RunLoop 状态切换
// 当 BeforeSources 和 AfterWaiting 之间间隔超过阈值 → 卡顿

static void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (activity == kCFRunLoopBeforeSources || activity == kCFRunLoopAfterWaiting) {
        // 记录时间戳
        CFAbsoluteTime currentTime = CFAbsoluteTimeGetCurrent();
        // 对比上次记录的 BeforeSources 时间
        // 如果差 > 100ms → 卡顿,采集堆栈
    }
}

// 在主线程 RunLoop 添加 observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
    kCFAllocatorDefault,
    kCFRunLoopBeforeSources | kCFRunLoopAfterWaiting | kCFRunLoopBeforeWaiting,
    YES, 0,
    &runLoopObserverCallback,
    &context
);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

RunLoop 与 autoreleasepool 的关系

RunLoop 循环周期:
  1. objc_autoreleasePoolPush()          ← 打一个哨兵
  2. 处理事件:Timer、Source0、Source1
  3. objc_autoreleasePoolPop(哨兵)       ← 释放本周期产生的临时对象
  4. 回到步骤 1

所以主线程 RunLoop 每次循环结束都会释放临时对象。
手势识别、dispatch block 等也会由各自的 autoreleasepool 包裹。

高频追问清单

问题关键要点
主线程 RunLoop 为什么是开启的?UIApplicationMain 内部调用了 CFRunLoopRun,默认在 DefaultMode 下运行
子线程如何保活?添加一个 port source / timer,然后 run。不添加 source 会在第一次 run 后立刻退出
Timer 在滑动时不触发怎么办?加入 CommonModes([[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]
Source0 和 Source1 的区别?Source0 需手动唤醒(CFRunLoopSourceSignal + CFRunLoopWakeUp);Source1 基于 mach_port 内核自动唤醒
RunLoop 如何节省能耗?通过 mach_msg 让线程进入内核态休眠,不消耗 CPU
performSelector:afterDelay: 的机制?创建 Timer 加到当前 RunLoop 的当前 Mode。当前 Mode 不是 DefaultMode 时不会触发
卡顿监控的原理?Observer 监听状态切换,计算 BeforeSources 到 AfterWaiting 的间隔

项目落地版

场景 1:子线程保活(常驻线程)

@interface PermanentThread : NSObject
@property (nonatomic, strong) NSThread *thread;
- (void)executeTask:(void(^)(void))task;
@end

@implementation PermanentThread {
    NSRunLoop *_runLoop;
}

- (instancetype)init {
    if (self = [super init]) {
        self.thread = [[NSThread alloc] initWithBlock:^{
            // 添加 port 让 RunLoop 有事件源可处理
            [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
            _runLoop = [NSRunLoop currentRunLoop]; // 持有引用用于后续操作

            // 启动 RunLoop(永不超时)
            while (YES) {
                [_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }];
        [self.thread start];
    }
    return self;
}

- (void)executeTask:(void(^)(void))task {
    [self performSelector:@selector(performTask:)
                 onThread:self.thread
               withObject:[task copy]
            waitUntilDone:NO];
}

- (void)performTask:(void(^)(void))task {
    task();
}

- (void)stop {
    // 退出 RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.thread = nil;
}

@end

⚠️ 子线程保活需注意线程生命周期管理,避免创建过多常驻线程。

场景 2:卡顿监控器

final class LagMonitor {
    static let shared = LagMonitor()
    private var observing = false
    private var lastTimestamp: CFAbsoluteTime = 0

    func start() {
        guard !observing else { return }
        observing = true

        let observer = CFRunLoopObserverCreateWithHandler(
            kCFAllocatorDefault,
            CFOptionFlags([.beforeSources, .afterWaiting, .beforeWaiting]),
            true, 0
        ) { [weak self] _, activity in
            guard let self else { return }

            switch activity {
            case .beforeSources, .afterWaiting:
                self.lastTimestamp = CFAbsoluteTimeGetCurrent()
            case .beforeWaiting:
                // 即将休眠,检查卡顿
                let now = CFAbsoluteTimeGetCurrent()
                let diff = (now - self.lastTimestamp) * 1000
                if diff > 100 {
                    // 卡顿超过 100ms,采集堆栈
                    print("[LagMonitor] 卡顿: \(diff)ms")
                    // BSBacktraceLogger 或 PLCrashReporter 采集堆栈
                }
            default:
                break
            }
        }

        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
    }
}

场景 3:NSTimer 在滑动时保持触发

// 问题:NSTimer 加到 DefaultMode,滑动时 Timer 不触发

// 修复方案 1:加入 CommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"timer fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

// 修复方案 2:使用 dispatch_source 定时器(不受 RunLoop Mode 影响)
dispatch_source_t dispatchTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(dispatchTimer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(dispatchTimer, ^{
    NSLog(@"dispatch timer fired");
});
dispatch_resume(dispatchTimer);

场景 4:子线程 RunLoop 延迟执行

// 子线程正常执行 performSelector:afterDelay:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // ⚠️ 子线程 RunLoop 默认不开启,直接调 afterDelay 不会触发
    [self performSelector:@selector(task) withObject:nil afterDelay:2.0];

    // 需要手动启动 RunLoop
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];
});

场景 5:空闲任务调度(IDLE 时执行低优任务)

// 利用 RunLoop 空闲时执行非关键任务
static void addIdleTask(void (^task)(void)) {
    CFRunLoopRef runLoop = CFRunLoopGetMain();

    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
        kCFAllocatorDefault,
        kCFRunLoopBeforeWaiting | kCFRunLoopExit,
        YES, 0,
        ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            task();
            CFRunLoopObserverInvalidate(observer); // 执行一次后移除
            CFRelease(observer);
        }
    );

    CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
}

学习路径与优先级

初级(P1)— 理解 RunLoop 基本概念

  • 理解 RunLoop 是什么、主线程 RunLoop 如何启动
  • 理解 Timer 添加到 RunLoop 的基本方式
  • 知道滑动时 Timer 不触发的问题
  • 知道 performSelector:afterDelay: 依赖 RunLoop

自检

  • 解释为什么 NSTimer 在滑动时不准确
  • 写出 Timer 添加到 CommonModes 的代码

中级(P0)— 掌握实践

  • 理解 Mode 的概念和 CommonModes 的机制
  • 能实现子线程保活
  • 理解卡顿监控的底层原理和实现
  • 理解 RunLoop 休眠机制(mach_msg)
  • 能区分 Source0/Source1/Timer/Observer 的行为差异

动手实践

  1. 写一个子线程保活的 PermanentThread
  2. 实现一个简单的卡顿监控器
  3. 验证 Timer 在不同 Mode 下的行为差异

高级(P1)— 深度应用

  • 能设计基于 RunLoop 的空闲任务调度框架
  • 理解 GCD 和 RunLoop 的关系(dispatch 到 main queue 的 block 如何被 RunLoop 处理)
  • 能结合 RunLoop 设计性能监控体系
  • 理解 CFRunLoop 源码的关键路径

实战项目

  1. 实现一个卡顿堆栈采集 + 上报系统
  2. 设计 RunLoop 驱动的空闲任务执行器
  3. 分析 App 启动过程中 RunLoop 的状态变化