聊聊runloop

5 阅读10分钟

RunLoop深度理解-面试必备

🔍 RunLoop的本质:一个"智能等待"的消息循环

想象一下,你是一个餐厅服务员:

  • 有客人来:你忙着服务(处理事件)
  • 没客人来:你站在那里发呆等待(浪费时间)
  • RunLoop就是:一个聪明的服务员,他知道"没客人时去休息,但随时准备被叫醒"

🏗️ 核心架构深度剖析

1. RunLoop的三大核心组件

RunLoop = {
    Input Sources(输入源): 触发RunLoop的事件源
    Timers(定时器): 定时触发的事件
    Observers(观察者): 监听RunLoop状态变化
}

Input Sources分为两种

  • Source0:用户主动触发的事件(触摸、点击)

    • 需要手动"叫醒"RunLoop
    • 比如:按钮点击事件
  • Source1:系统自动触发的事件(网络、硬件)

    • 可以自动"叫醒"RunLoop
    • 比如:网络数据到达

2. RunLoop的"五种状态"生命周期

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = 1 << 0,  // 1. 进入RunLoop
    kCFRunLoopBeforeTimers  = 1 << 1,  // 2. 准备处理定时器
    kCFRunLoopBeforeSources = 1 << 2,  // 3. 准备处理输入源
    kCFRunLoopBeforeWaiting = 1 << 3,  // 4. 准备进入休眠
    kCFRunLoopAfterWaiting  = 1 << 4,  // 5. 从休眠中唤醒
    kCFRunLoopExit          = 1 << 5,  // 6. 退出RunLoop
};

🔄 RunLoop的完整运行流程(用代码说话)

// RunLoop的核心就是一个do-while循环
void CFRunLoopRun(void) {
    do {
        // 1️⃣ 通知:我要开始工作了
        __CFRunLoopDoObservers(runLoop, kCFRunLoopEntry);

        // 2️⃣ 检查定时器:有没有到点的闹钟?
        __CFRunLoopDoTimers(runLoop);

        // 3️⃣ 处理Source0:用户主动触发的事件(触摸、点击)
        __CFRunLoopDoSources0(runLoop);

        // 4️⃣ 通知:我要休息了,有事叫我
        __CFRunLoopDoObservers(runLoop, kCFRunLoopBeforeWaiting);

        // 5️⃣ 进入休眠:等待被唤醒(核心!节省CPU)
        mach_msg(&msg, MACH_RCV_MSG, 0, sizeof(msg_buffer));

        // 6️⃣ 被唤醒:有人叫我干活了!
        __CFRunLoopDoObservers(runLoop, kCFRunLoopAfterWaiting);

        // 7️⃣ 处理唤醒事件:看看是谁叫醒的我
        if (是定时器唤醒的) {
            __CFRunLoopDoTimers(runLoop);
        } else if (是Source0事件) {
            __CFRunLoopDoSources0(runLoop);
        } else if (是Source1事件) {
            __CFRunLoopDoSource1(runLoop);
        }

    } while (!停止条件);
}

🎯 Mode机制:RunLoop的"工作模式"

Mode就像手机的飞行模式

  • 不同场景:开会用静音,娱乐用正常
  • RunLoop Mode:不同场景执行不同任务

系统预定义的Mode:

// 1. 默认模式:日常工作
NSString * const NSDefaultRunLoopMode = @"kCFRunLoopDefaultMode";

// 2. 跟踪模式:用户滑动时专用
NSString * const UITrackingRunLoopMode = @"UITrackingRunLoopMode";

// 3. 通用模式:Default + Tracking的组合
NSString * const NSRunLoopCommonModes = @"kCFRunLoopCommonModes";

Mode解决的核心问题

场景:滑动TableView时,定时器暂停的问题

// ❌ 问题代码:定时器在滑动时暂停
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3.0
                                                 target:self
                                               selector:@selector(timerAction)
                                               userInfo:nil
                                                repeats:YES];

// ✅ 解决方案:使用CommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:3.0
                                       target:self
                                     selector:@selector(timerAction)
                                     userInfo:nil
                                      repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

为什么这样就能解决问题?

  • DefaultMode:日常工作模式
  • TrackingMode:滑动时的专用模式
  • CommonModes:包含Default和Tracking,定时器在这两种模式下都能运行

🚀 RunLoop在项目中的实际应用

1. 常驻线程(网络监听)

- (void)createNetworkThread {
    NSThread *networkThread = [[NSThread alloc] initWithTarget:self
                                                      selector:@selector(networkLoop)
                                                        object:nil];
    [networkThread start];
}

- (void)networkLoop {
    @autoreleasepool {
        // 创建RunLoop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

        // 添加端口,保持RunLoop运行(没有输入源RunLoop会立即退出)
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

        // 启动RunLoop(这是一个死循环,直到线程结束)
        [runLoop run];
    }
}

2. 延迟执行的本质

// 这个方法底层就是用Timer实现的
[self performSelector:@selector(doSomething) withObject:nil afterDelay:2.0];

// 等价于:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0
                                       target:self
                                     selector:@selector(doSomething)
                                     userInfo:nil
                                      repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

3. 屏幕刷新(CADisplayLink)

// CADisplayLink也是基于RunLoop的
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self
                                                         selector:@selector(refresh)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop]
                  forMode:NSDefaultRunLoopMode];

🤔 深入理解的关键问题

Q1: 为什么主线程不会死掉?

A: 因为主线程的RunLoop一直在运行,处理各种UI事件和用户交互。

Q2: 子线程默认没有RunLoop吗?

A: 是的,子线程需要手动创建RunLoop,否则执行完任务就销毁了。

Q3: RunLoop的休眠机制是什么?

A: 使用mach_msg()系统调用进入内核休眠,等待消息唤醒。这就是为什么RunLoop能节省CPU的原因。

Q4: Source0和Source1的本质区别?

A:

  • Source0:需要"手动叫醒"(用户事件)
  • Source1:能"自动叫醒"(系统事件)

Q5: RunLoop Observer有什么用?

A: 可以监听RunLoop的状态变化,用于性能监控、调试、自定义事件处理等。


💡 面试时的回答技巧

不要只是背概念,要能解释原理:

// 好的回答示例:
"RunLoop本质上是一个事件循环,它让线程在有任务时工作,无任务时休眠。
比如NSTimer为什么需要RunLoop?因为Timer的触发依赖RunLoop的循环检查。
又比如为什么滑动时Timer暂停?因为RunLoop切换到TrackingMode,而Timer默认在DefaultMode。"

关键是要理解:RunLoop不是什么高深的技术,它就是操作系统为了节省CPU资源而设计的一个智能等待机制。


📚 RunLoop面试高频问题详解

1. iOS 项目中有用到RunLoop的吗?举例

答案:是的,RunLoop在iOS开发中被广泛使用,虽然很多时候我们是间接使用的:

举例场景

// 1. NSTimer的使用(最常见)
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                 target:self
                                               selector:@selector(timerAction)
                                               userInfo:nil
                                                repeats:YES];
// 这其实是在主线程RunLoop中添加了Timer

// 2. performSelector:afterDelay:(延迟执行)
[self performSelector:@selector(doSomething) withObject:nil afterDelay:2.0];
// 底层通过Timer实现

// 3. 常驻线程(网络监听)
- (void)createNetworkThread {
    NSThread *thread = [[NSThread alloc] initWithTarget:self
                                               selector:@selector(networkTask)
                                                 object:nil];
    [thread start];
}

- (void)networkTask {
    @autoreleasepool {
        // 创建RunLoop保持线程存活
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        // 添加端口保持RunLoop运行
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

// 4. 图片异步加载完成后更新UI
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 下载图片
    UIImage *image = [self downloadImage];

    // 回到主线程更新UI(主线程RunLoop处理)
    dispatch_async(dispatch_get_main_queue(), ^{
        self.imageView.image = image;
    });
});

// 5. CADisplayLink(屏幕刷新)
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self
                                                         selector:@selector(displayLinkAction)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop]
                  forMode:NSDefaultRunLoopMode];

2. RunLoop内部实现逻辑?

RunLoop的核心是一个do-while循环,其内部实现逻辑如下:

// 伪代码表示RunLoop的核心循环
int32_t __CFRunLoopRun() {
    do {
        // 1. 通知观察者:即将进入RunLoop
        __CFRunLoopDoObservers(runLoop, kCFRunLoopEntry);

        // 2. 处理Timer(到期的定时器)
        __CFRunLoopDoTimers(runLoop);

        // 3. 处理Source0(非端口事件,如触摸事件)
        __CFRunLoopDoSources0(runLoop);

        // 4. 通知观察者:即将进入休眠
        __CFRunLoopDoObservers(runLoop, kCFRunLoopBeforeWaiting);

        // 5. 休眠等待(核心!节省CPU)
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer));

        // 6. 被唤醒后通知观察者
        __CFRunLoopDoObservers(runLoop, kCFRunLoopAfterWaiting);

        // 7. 处理唤醒事件
        if (msg_is_timer) {
            __CFRunLoopDoTimers(runLoop);
        } else if (msg_is_source) {
            __CFRunLoopDoSources0(runLoop);
        } else if (msg_is_source1) {
            __CFRunLoopDoSource1(runLoop);
        }

    } while (!stop);
}

核心流程

  1. 进入循环 → 通知Observers
  2. 处理Timers → 检查到期的定时器
  3. 处理Sources → 处理输入源事件
  4. 休眠等待 → 没有事件时进入休眠
  5. 唤醒处理 → 有事件时被唤醒处理
  6. 循环重复 → 回到步骤2

3. RunLoop和线程的关系?

答案:RunLoop与线程是一一对应的关系。

  • 主线程:系统自动创建并启动RunLoop,无需手动管理
  • 子线程:默认没有RunLoop,需要手动创建和启动
  • 生命周期:RunLoop的生命周期与线程一致
  • 管理方式:每个线程最多只能有一个RunLoop
// 主线程RunLoop(自动存在)
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];

// 子线程RunLoop(需要手动创建)
NSThread *thread = [[NSThread alloc] initWithTarget:self
                                           selector:@selector(threadTask)
                                             object:nil];
[thread start];

- (void)threadTask {
    // 获取当前线程的RunLoop(自动创建)
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

    // 添加输入源保持RunLoop运行
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

    // 启动RunLoop
    [runLoop run];
}

4. Timer与RunLoop的关系?

答案:Timer必须依赖RunLoop才能工作。

  • 依赖关系:NSTimer需要添加到RunLoop中才能被调度执行
  • 运行机制:RunLoop在每次循环中检查Timer是否到期
  • 默认行为:scheduledTimerWithTimeInterval自动添加到当前RunLoop
// 方式1:自动添加到当前RunLoop
NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self
                                                selector:@selector(timerAction)
                                                userInfo:nil
                                                 repeats:YES];

// 方式2:手动添加到指定RunLoop
NSTimer *timer2 = [NSTimer timerWithTimeInterval:1.0
                                        target:self
                                      selector:@selector(timerAction)
                                      userInfo:nil
                                       repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSDefaultRunLoopMode];

5. 程序中添加每3秒响应一次的NSTimer,当拖动tableView时timer可能无法响应要怎么解决?

问题分析:拖动TableView时,RunLoop切换到UITrackingRunLoopMode,而Timer默认在NSDefaultRunLoopMode运行,导致暂停。

解决方案:将Timer添加到NSRunLoopCommonModes

// 问题代码(拖动时会暂停)
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3.0
                                                 target:self
                                               selector:@selector(timerAction)
                                               userInfo:nil
                                                repeats:YES];

// 解决方案
NSTimer *timer = [NSTimer timerWithTimeInterval:3.0
                                       target:self
                                     selector:@selector(timerAction)
                                     userInfo:nil
                                      repeats:YES];

// 添加到CommonModes,这样在DefaultMode和TrackingMode下都会执行
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

CommonModes包含

  • NSDefaultRunLoopMode(默认模式)
  • UITrackingRunLoopMode(跟踪模式)

6. RunLoop是怎么响应用户操作的,具体流程是什么样的?

响应流程

  1. 用户操作发生 → 触摸屏幕、点击按钮等
  2. 系统生成事件 → 转换为UIEvent对象
  3. 事件入队 → 加入到UIApplication的事件队列
  4. RunLoop唤醒 → Source1(端口事件)唤醒主线程RunLoop
  5. 事件分发 → UIApplication将事件分发给UIWindow
  6. 视图查找 → UIWindow找到最合适的UIView处理事件
  7. 事件传递 → 通过响应链(Responder Chain)传递
  8. 事件处理 → 目标对象处理事件(touchesBegan/Moved/Ended等)
  9. RunLoop继续 → 处理完后继续下一个循环
// 触摸事件的处理流程(简化)
触摸发生 → UIEvent创建 → Source1唤醒RunLoop →
UIApplication分发 → UIWindow查找 → UIView响应 →
touchesBegan/Moved/Ended调用 → 完成处理

关键点

  • Source1事件:系统事件可以自动唤醒RunLoop
  • 主线程RunLoop:UI事件都在主线程处理
  • 事件循环:每个事件处理完后,RunLoop继续等待下一个事件

7. 说说RunLoop的几种状态?

RunLoop有以下几种状态(通过CFRunLoopActivity枚举定义):

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),              // 即将进入RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),       // 处理Timer前
    kCFRunLoopBeforeSources = (1UL << 2),      // 处理Source前
    kCFRunLoopBeforeWaiting = (1UL << 3),      // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 4),       // 从休眠中唤醒
    kCFRunLoopExit = (1UL << 5),               // 退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU      // 所有状态
};

状态详解

  • kCFRunLoopEntry:RunLoop开始运行
  • kCFRunLoopBeforeTimers:处理Timer之前
  • kCFRunLoopBeforeSources:处理Source之前
  • kCFRunLoopBeforeWaiting:即将进入休眠,等待事件
  • kCFRunLoopAfterWaiting:从休眠中被唤醒
  • kCFRunLoopExit:RunLoop退出

8. RunLoop的Mode作用是什么?

Mode是RunLoop的核心机制,用于控制在不同场景下执行不同的任务。

系统预定义Mode

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认模式
  • UITrackingRunLoopMode:界面跟踪模式(滑动ScrollView时)
  • NSRunLoopCommonModes:通用模式(Default+Tracking的组合)

Mode的作用

  1. 任务隔离:不同Mode下任务互不干扰
  2. 优先级控制:特定场景优先执行特定任务
  3. 性能优化:滑动时暂停耗时操作,避免卡顿
  4. 事件过滤:只处理当前Mode相关的输入源
// Mode使用示例
// Timer默认在DefaultMode,滑动时暂停
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                 target:self
                                               selector:@selector(updateUI)
                                               userInfo:nil
                                                repeats:YES];

// 滑动时切换到TrackingMode,只处理滑动相关事件
// Timer暂停执行,避免UI卡顿

// 解决方法:添加到CommonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

面试时重点强调

  • Mode隔离机制保证滑动流畅性
  • CommonModes解决跨场景任务执行
  • 理解Mode是掌握RunLoop的关键

🎯 总结

RunLoop不是什么高深的技术,它就是操作系统为了节省CPU资源而设计的一个智能等待机制。通过让线程在有任务时忙碌,没有任务时休眠,RunLoop保证了应用的响应性和性能。

核心理解

  • RunLoop = 事件循环 + 智能休眠
  • Mode = 场景隔离机制
  • Source0/Source1 = 用户事件/系统事件
  • Timer = 定时任务
  • Observer = 状态监听

掌握RunLoop,就是掌握了iOS事件处理的核心机制!