【碎片八股文 #006】RunLoop 为什么能让主线程不退出?

60 阅读5分钟

【碎片八股文 #006】RunLoop 为什么能让主线程不退出?

一、面试题原文

面试官:  为什么 iOS 的主线程不会像普通线程一样执行完就退出?RunLoop 在其中起什么作用?

候选人:  RunLoop 是个循环吧……一直在运行所以不会退出。

面试官心里想:  能说出事件循环、休眠唤醒、Source/Timer/Observer 就算过关了。


二、常见误答

很多人只知道"RunLoop 是个循环",但说不清楚:

  • RunLoop 的"循环"具体循环什么?
  • 为什么没事件时不会耗电?
  • RunLoop 和线程是什么关系?

这些都是面试官会追问的点。


三、正确理解

什么是 RunLoop?

RunLoop 是一个事件循环机制,让线程能够:

  • 有事做事(处理事件)
  • 没事休眠(节省 CPU)
  • 随时被唤醒(响应新事件)

核心作用:

  1. 保持线程不退出
  2. 监听并处理各种事件
  3. 节省系统资源

为什么需要 RunLoop?

普通线程的生命周期:

// 普通线程:执行完就退出
- (void)normalThread {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"线程开始");
        // 执行一些任务
        NSLog(@"线程结束");
    }];
    [thread start];
    // 线程执行完 block 后自动退出
}

主线程的特殊性:

主线程需要一直运行,处理用户交互、界面刷新等事件。如果执行完 main() 就退出,应用就结束了。

RunLoop 让主线程进入循环状态:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        // 这里会启动主线程的 RunLoop
        return UIApplicationMain(argc, argv, nil, 
                                NSStringFromClass([AppDelegate class]));
        // UIApplicationMain 内部会启动 RunLoop,永不返回
    }
}

四、RunLoop 的核心原理

1. RunLoop 和线程的关系

一对一绑定:

  • 每个线程都可以有一个 RunLoop
  • RunLoop 和线程一一对应
  • 主线程的 RunLoop 自动创建并启动
  • 子线程的 RunLoop 需要手动获取和启动
// 获取当前线程的 RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

// 获取主线程的 RunLoop
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];

线程保活:

// 子线程默认会退出
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"开始");
    // 执行完就退出
});

// 用 RunLoop 保活子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"开始");
    
    // 启动 RunLoop
    [[NSRunLoop currentRunLoop] run];  // 线程不会退出
    
    NSLog(@"永远不会执行");  // 这行代码到不了
});

2. RunLoop 的输入源

RunLoop 通过 输入源 接收事件,主要分为三类:

类型说明示例
Source0非基于 Port 的事件触摸事件、performSelector
Source1基于 Port 的进程间通信Mach Port、网络唤醒
Timer定时器事件NSTimer、CADisplayLink
ObserverRunLoop 状态观察监听 RunLoop 各个阶段

3. RunLoop 的运行循环

1. 通知 Observers:即将进入 Loop
   ↓
2. 通知 Observers:即将处理 Timers
   ↓
3. 通知 Observers:即将处理 Sources
   ↓
4. 处理 Source0(非 Port 事件)
   ↓
5. 如果有 Source1(Port 事件),跳到步骤 9
   ↓
6. 通知 Observers:即将休眠
   ↓
7. 休眠(等待被唤醒)
   ↓
8. 被唤醒(收到事件)
   ↓
9. 通知 Observers:被唤醒
   ↓
10. 处理唤醒事件
    - 处理 Timer
    - 处理 Source1
    - 处理 GCD Main Queue
   ↓
11. 通知 Observers:即将退出 Loop
   ↓
12. 回到步骤 1(继续循环)

关键点:  第 7 步的"休眠"是真正的休眠,不占用 CPU。


五、图解核心概念

RunLoop 的休眠唤醒机制

┌─────────────────────────────────────────────────┐
│                  RunLoop 循环                    │
│                                                 │
│  ┌──────────────┐                               │
│  │  处理事件     │                               │
│  └──────┬───────┘                               │
│         │                                       │
│         │ 没有事件了                              │
│         ▼                                       │
│  ┌──────────────┐        mach_msg_trap()        │
│  │  进入休眠     │◄─────────────────────┐        │
│  │ (不占 CPU)   │                      │        │
│  └──────┬───────┘                      │       │
│         │                              │       │
│         │ 有新事件                      │       │
│         ▼                              │       │
│  ┌──────────────┐        内核唤醒       │        │
│  │  被唤醒       │──────────────────────┘        │
│  └──────┬───────┘                               │
│         │                                       │
│         ▼                                       │
│  ┌──────────────┐                               │
│  │  处理事件     │                               │
│  └──────────────┘                               │
└─────────────────────────────────────────────────┘

唤醒来源:
- 触摸屏幕(Source0)
- 网络数据到达(Source1)
- Timer 触发
- GCD 任务到达主队列

RunLoop 的 Mode 机制

RunLoop 运行在不同的 Mode(模式)  下:

┌─────────────────────────────────────────────┐
│            RunLoop Mode                     │
│                                             │
│  ┌──────────────────┐                       │
│  │  NSDefaultRunLoopMode  (默认模式)        │
│  │  - 普通事件处理                            │
│  │  - NSTimer                               │
│  └──────────────────┘                       │
│                                             │
│  ┌──────────────────┐                       │
│  │  UITrackingRunLoopMode (滚动模式)        │
│  │  - 滚动 ScrollView 时                     │
│  │  - 拖动界面时                              │
│  └──────────────────┘                       │
│                                             │
│  ┌──────────────────┐                       │
│  │  NSRunLoopCommonModes (组合模式)         │
│  │  - 包含 Default + Tracking                │
│  │  - Timer 加到这个 Mode 就不会被滚动影响      │
│  └──────────────────┘                       │
└─────────────────────────────────────────────┘

常见问题:  Timer 在滚动时不触发

// 问题代码:Timer 加到默认模式
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 
                                                  target:self
                                                selector:@selector(update)
                                                userInfo:nil
                                                 repeats:YES];
// 滚动时 RunLoop 切换到 Tracking 模式,Timer 不会触发

// 解决方案:把 Timer 加到 Common 模式
[[NSRunLoop currentRunLoop] addTimer:timer 
                             forMode:NSRunLoopCommonModes];

六、延伸提问

1. RunLoop 的底层实现是什么?

RunLoop 基于 mach_msg_trap()  系统调用实现休眠和唤醒。

// 简化版 RunLoop 伪代码
void CFRunLoopRun() {
    while (true) {
        // 处理事件
        handleEvents();
        
        // 如果没有事件,进入休眠
        if (noEventsToProcess) {
            mach_msg_trap();  // 内核级休眠,不占 CPU
        }
        
        // 被内核唤醒后继续循环
    }
}

mach_msg_trap() 的作用:

  • 让线程进入内核态休眠
  • 等待 Mach Port 消息唤醒
  • 被唤醒后返回用户态继续执行

2. AutoreleasePool 和 RunLoop 的关系

AutoreleasePool 的创建和释放由 RunLoop Observer 控制:

RunLoop 启动时:
    - 创建 AutoreleasePool(最外层 pool)

RunLoop 即将休眠时:
    - 释放旧 pool
    - 创建新 pool

RunLoop 退出时:
    - 释放 pool

这就是为什么主线程不需要手动创建 @autoreleasepool。

3. 子线程如何正确使用 RunLoop?

// 错误示例:Run 后无法停止
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    [[NSRunLoop currentRunLoop] run];  // 永远不会返回
}];
[thread start];

// 正确示例:使用 runMode:beforeDate: 可控制
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    while (!self.shouldStop) {
        // 运行一小段时间,可随时退出
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode 
                                 beforeDate:[NSDate distantFuture]];
    }
}];
[thread start];

4. performSelector 在 RunLoop 中的执行时机

// 立即执行(不需要 RunLoop)
[self performSelector:@selector(test)];

// 下一个 RunLoop 循环执行
[self performSelector:@selector(test) 
           withObject:nil 
           afterDelay:0];  // 需要 RunLoop

// 在指定线程的 RunLoop 中执行
[self performSelector:@selector(test) 
             onThread:otherThread 
           withObject:nil 
        waitUntilDone:NO];  // 目标线程必须有 RunLoop

关键点:  afterDelay 和 onThread 的方法都依赖 RunLoop。


七、记忆口诀

"RunLoop 循环不停转,有事处理没事眠;Source 触发 Timer 到,Observer 观察全过程;主线程常驻靠它管,子线程保活也能玩。"


八、碎片笔记

核心关键词:  RunLoop、事件循环、休眠唤醒、Source/Timer/Observer、Mode、mach_msg_trap

重点记忆:

  • RunLoop 通过 mach_msg_trap 实现休眠,不占 CPU
  • 主线程的 RunLoop 自动创建并启动,子线程需要手动
  • RunLoop 运行在不同 Mode 下,Mode 之间互不影响
  • Timer 要加到 NSRunLoopCommonModes 才不会被滚动影响

实际应用:

  • 子线程保活:手动启动 RunLoop
  • 避免 Timer 在滚动时失效:用 Common Mode
  • performSelector:afterDelay: 需要目标线程有 RunLoop
  • AutoreleasePool 的创建释放由 RunLoop 控制

性能优化:

  • 不要在主线程 RunLoop 中做耗时操作
  • 长时间运行的任务放到子线程 + RunLoop
  • 监听 RunLoop 状态可以做卡顿检测

今天的碎片,帮你面试少挂一次。


下一篇预告:  【碎片八股文 #007】Handler 消息机制底层是怎么实现的?