iOS: RunLoop入门
2026/9/12 21:35:42 网站建设 项目流程

文章目录

  • RunLoop
    • RunLoop 和 线程的关系
    • 为什么 RunLoop 能让线程休眠
    • RunLoop Mode (运行模式)
        • Timer 在滚动的时候会失效
      • NSRunLoopCommonModes
    • RunLoop 的 Source
    • Timer
    • Observer
    • RunLoop 一次完整运行过程
    • RunLoop 和 线程保活
      • 为什么必须要有 `addPort`?
      • 没有 `addPort` 的情况:
      • 有 `addPort` 的情况:
    • RunLoop 与 performSelector
      • 常见的几种形式:
    • RunLoop 与 GCD 区别

RunLoop

RunLoop 本质上是一个事件循环机制, 让线程在没有任务是休眠, 有事件时被唤醒并处理事件

普通线程:

-(void)threadMain{NSLog(@"开始执行");// 执行完// 线程结束}

如果希望线程一直存活:

while(YES){// 等待任务}

这种方式会导致 CPU 空转。

RunLoop 解决的就是:

没有事件 ↓ 线程进入休眠 ↓ 有事件到来 ↓ 唤醒线程 ↓ 处理事件 ↓ 继续等待

所以可以把它理解成:

RunLoop = 线程的“事件调度中心” + 休眠/唤醒机制

RunLooP作用

  • 保持线程 (尤其是主线程) 持续运行, 不退出
  • 处理 App 中的各种事件 (触摸, Timer, performSelector, GCD主队列任务, 界面刷新)
  • 节省CPU 资源: 有事做事, 无事休眠

RunLoop 和 线程的关系

一个线程对应一个RunLoop, 但不是说每个线程启动后都有一个已经运行的RunLoop,

主线程:

NSRunLoop*runLoop=[NSRunLoop mainRunLoop];

主线程的RunLoop会被系统自动启动

子线程则不同:

NSThread*thread=[[NSThread alloc]initWithBlock:^{NSLog(@"子线程");[[NSRunLoop currentRunLoop]run];}];[thread start];

如果不调用:[[NSRunLoop currentRunLoop] run];,那么子线程执行完任务之后就结束了

  • 主线程的 RunLoop 在App启动时自动创建并执行
  • 子线程默认没有RunLoop, 需要手动获取并启动 (否则线程执行完成任务就退出)

为什么 RunLoop 能让线程休眠

底层并不是简单的:

while(1)

而是:

RunLoop ↓ 检查有没有事件 ↓ 有事件 → 处理 ↓ 没事件 → 让线程进入休眠 ↓ Mach Port / Source / Timer 等事件到来 ↓ 系统唤醒线程

所以 RunLoop 的核心价值之一就是:

避免线程在没有任务的时候持续占用 CPU。

RunLoop的核心组成: Mode, Source, Timer, Observer

RunLoop Mode (运行模式)

RunLoop 并不是把所有的事件全部一起处理, 它通过Mode对事件进行分类

Mode:

Mode 名称说明
kCFRunLoopDefaultMode / NSDefaultRunLoopMode默认模式,App 平时运行在这里
UITrackingRunLoopMode界面追踪模式(UIScrollView 滑动时切换到这个模式)
kCFRunLoopCommonModes / NSRunLoopCommonModes伪模式 / 标记,不是真实 Mode。默认包含 Default + Tracking
UIInitializationRunLoopModeApp 启动时的临时 Mode,启动完成后不再使用
GSEventReceiveRunLoopMode系统内部接收事件用(一般碰不到)

常见的 Mode 有:

NSDefaultRunLoopMode NSRunLoopCommonModes UITrackingRunLoopMode

主线程的 RunLoop 是在 UIApplicationMain 内部自动创建并启动的 (最终调用CFRunLoop), 所以App可以一直运行

Timer 在滚动的时候会失效

例如:

[NSTimer scheduledTimerWithTimeInterval:1target:selfselector:@selector(test)userInfo:nil repeats:YES];

默认Timer被加入NSDefaultRunLoopMode

那么这时候有一个TableView, 在滑动TableView的时候, Timer就会失效

这是因为在UITableView上滑动的时候:

RunLoop ↓ 进入 UITrackingRunLoopMode ↓ Default Mode 暂时不处理 ↓ Timer 不触发

那么如何解决这个问题呢?

只需要将 Timer 加入到NSRunLoopCommonModes就可以解决这个问题

NSTimer*timer=[NSTimer timerWithTimeInterval:1target:selfselector:@selector(test)userInfo:nil repeats:YES];[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

NSRunLoopCommonModes

NSRunLoopCommonModes不是一个独立运行的 Mode

它可以理解为 Mode集合, 包含多个Common Mode

当把 Timer 加入到NSRunLoopCommonModes, 就是相当于就把 Timer 添加到当前RunLoop标记为 Common 的那些 Mode 中

RunLoop 的 Source

Source 是 RunLoop 的事件来源

常见可以分为:

Source 0

需要手动唤醒/处理的事件源

例如:

performSelector

这类机制可以和 Source 0 联系起来

Source 1

基于 Mach Port 等底层事件机制

例如:

触摸事件 IPC 系统事件

可以粗略理解为:

Source 1 ↓ 底层事件到达 ↓ 唤醒 RunLoop ↓ 处理事件

Timer

NSTimer也是RunLoop 的一个输入源, 这里需要注意的是: NSTimer 本身不是一个独立线程

例如:

NSTimer*timer=[NSTimer scheduledTimerWithTimeInterval:1target:selfselector:@selector(sayHello)userInfo:nil repeats:YES];

Timer 是注册到 RunLoop 中

NSTimer ↓ RunLoop ↓ 时间到了 ↓ RunLoop 检查 ↓ 执行 selector

如果 RunLoop 不运行, NSTimer 就不会正常触发

-(void)viewDidLoad{[superviewDidLoad];// 1. 创建一个 Timer,每隔 1 秒执行一次 tick 方法NSTimer*timer=[NSTimer scheduledTimerWithTimeInterval:1.0target:selfselector:@selector(tick)userInfo:nil repeats:YES];for(inti=0;i<5;i++){sleep(1);// 休眠 1 秒,模拟重度计算NSLog(@"正在计算... 第 %d 秒",i+1);}}-(void)tick{NSLog(@"定时器触发!");}

打印:

正在计算... 第 1 秒 正在计算... 第 2 秒 正在计算... 第 3 秒 正在计算... 第 4 秒 正在计算... 第 5 秒 定时器触发

从上面的代码运行结果可以观察到, 定时器事件在for循环结束之前没有触发

让我们看看 CPU 实际执行指令的顺序:

// ========== RunLoop 的伪代码 ========== function CFRunLoopRun() { while (true) { // ← RunLoop 的 while 循环 // 1. 检查 Timer // 2. 检查 Source // 3. 如果没有事件,休眠 // 4. 如果有事件,执行它 if (有事件需要处理) { // 这里会调用你的代码 [target performSelector:action]; // ← 在这里调用了 viewDidLoad // 你的代码执行完后,才继续下一轮循环 } } }

Observer

Observer 用来监听 RunLoop 的状态变化

例如:

RunLoop 即将进入 ↓ 处理 Timer ↓ 处理 Source ↓ 即将休眠 ↓ 被唤醒 ↓ 退出 RunLoop

可以通过CFRunLoopObserver监听

常见状态:

kCFRunLoopEntry kCFRunLoopBeforeTimers kCFRunLoopBeforeSources kCFRunLoopBeforeWaiting kCFRunLoopAfterWaiting kCFRunLoopExit

RunLoop 一次完整运行过程

RunLoop 开始 ↓ BeforeTimers ↓ BeforeSources ↓ 处理 Timer / Source ↓ 是否还有事件? ↙ ↘ 有 没有 ↓ ↓ 继续 BeforeWaiting ↓ 休眠 ↓ 事件到达 / Timer 到期 ↓ AfterWaiting ↓ 处理事件 ↓ 循环

可以把它浓缩成:

进入 ↓ 处理事件 ↓ 没有事件 ↓ 休眠 ↓ 被唤醒 ↓ 处理事件 ↓ 循环

RunLoop 和 线程保活

比如创建一个常驻线程:

NSThread*thread=[[NSThread alloc]initWithBlock:^{[[NSRunLoop currentRunLoop]addPort:[NSPort port]forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop]run];}];

NSThread* thread = [[NSThread alloc] initWithBlock:^{ ... }];

  • 作用: 创建一个新的子线程对象
  • initWithBlock: 告诉这个线程; “当启动时候,执行 Block 里面的代码”
  • 此时,线程还没有启动, 知识准别好了
[[NSRunLoop currentRunLoop]addPort:[NSPort port]forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop]run];

[NSRunLoop currentRunLoop]

  • 获取当前线程的 RunLoop
  • 因为这段代码写在 Block 里, 而 Block 会在子线程中执行, 所以这里拿到的是子线程的 RunLoop

[NSPort port]

  • 创建一个 Port (端口)
  • 可以把它理解成一个虚拟的 “信箱“, 有了这个信箱, RunLoop 就会认为: “我有东西要监听,不能退出”

add:forMode:

  • 把上面的信箱加入到 RunLoop 中
  • 作用: 给RunLoop添加一个永久有效的事件源, 让它有事可做, 不会空转退出

[thread start]

  • 启动子线程
  • 这时, 子线程开始执行 Block 里的代码

为什么必须要有addPort

这是一个极其关键的问题。

没有addPort的情况:

[[NSRunLoop currentRunLoop] run];
  • RunLoop 启动后,会检查自己有没有事件源(比如 Timer、Port、Observer)。
  • 如果什么都没有,RunLoop 会认为:“我没事可做,没必要继续跑。”
  • 于是,RunLoop 立即退出,子线程执行完所有代码,线程被销毁

addPort的情况:

[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run];
  • 添加 Port 后,RunLoop 发现:“我有一个 Port 要监听,我不能退出。”
  • 于是,RunLoop 进入永久等待状态,线程永不销毁。

RunLoop 与 performSelector

performSelectorNSObject提供的一系列方法,允许你延迟执行在指定线程上执行某个方法。

[selfperformSelector:@selector(test)onThread:thread withObject:nil waitUntilDone:NO];

常见的几种形式:

方法作用
[obj performSelector:@selector(method)]在当前线程立即执行
[obj performSelector:withObject:afterDelay:]延迟执行(依赖 RunLoop)
[obj performSelector:onThread:withObject:waitUntilDone:]在指定线程执行
[obj performSelectorOnMainThread:withObject:waitUntilDone:]在主线程执行

这个机制和目标线程的RunLoop 有密切关系

主线程 ↓ performSelector ↓ 目标线程 RunLoop ↓ 唤醒 ↓ 执行 test

如果创建了一个子线程:

NSThread*thread=...

但是如果没有正确启动它的 RunLoop:

[[NSRunloop currentRunLoop]run];

那么一些基于RunLoop的任务就有可能无法进行.

RunLoop 与 GCD 区别

GCD更偏向 任务调度

例如:

dispatch_async(dispatch_get_global_queue(0,0),^{// 后台任务});

关注任务 -> Queue -> Thread

RunLoop 更偏向 事件循环

关注:事件 -> RunLoop -> 处理 -> 休眠 -> 唤醒

两者不是互相替代的关系 ,GCD 和 RunLoop 在iOS 系统中是协同工作的

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询