iOS: GCD进阶,全局队列与自定义队列

RunLoop

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

普通线程:

objc 复制代码
- (void)threadMain {
    NSLog(@"开始执行");

    // 执行完
    // 线程结束
}

如果希望线程一直存活:

objc 复制代码
while (YES) {
    // 等待任务
}

这种方式会导致 CPU 空转。

RunLoop 解决的就是:

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

所以可以把它理解成:

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

RunLooP作用

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

RunLoop 和 线程的关系

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

主线程:

objc 复制代码
    NSRunLoop* runLoop = [NSRunLoop mainRunLoop]; 

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

子线程则不同:

objc 复制代码
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"子线程");

    [[NSRunLoop currentRunLoop] run];
}];

[thread start];

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

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

为什么 RunLoop 能让线程休眠

底层并不是简单的:

objc 复制代码
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
UIInitializationRunLoopMode App 启动时的临时 Mode,启动完成后不再使用
GSEventReceiveRunLoopMode 系统内部接收事件用(一般碰不到)

常见的 Mode 有:

objc 复制代码
NSDefaultRunLoopMode
NSRunLoopCommonModes
UITrackingRunLoopMode

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

Timer 在滚动的时候会失效

例如:

objc 复制代码
[NSTimer scheduledTimerWithTimeInterval:1
                                 target:self
                               selector:@selector(test)
                               userInfo:nil
                                repeats:YES];

默认Timer被加入 NSDefaultRunLoopMode

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

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

objc 复制代码
RunLoop
    ↓
进入 UITrackingRunLoopMode
    ↓
Default Mode 暂时不处理
    ↓
Timer 不触发

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

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

objc 复制代码
NSTimer *timer =
[NSTimer timerWithTimeInterval:1
                         target:self
                       selector:@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

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

例如:

objc 复制代码
performSelector

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

Source 1

基于 Mach Port 等底层事件机制

例如:

objc 复制代码
触摸事件
IPC
系统事件

可以粗略理解为:

复制代码
Source 1
    ↓
底层事件到达
    ↓
唤醒 RunLoop
    ↓
处理事件

Timer

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

例如:

objc 复制代码
NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval: 1
                                                  target: self
                                                selector: @selector(sayHello)
                                                userInfo: nil
                                                 repeats: YES];

Timer 是注册到 RunLoop 中

objc 复制代码
NSTimer
   ↓
RunLoop
   ↓
时间到了
   ↓
RunLoop 检查
   ↓
执行 selector

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

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 创建一个 Timer,每隔 1 秒执行一次 tick 方法
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 
                                                      target:self 
                                                    selector:@selector(tick) 
                                                    userInfo:nil 
                                                     repeats:YES];
    
    for (int i = 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 的状态变化

例如:

objc 复制代码
RunLoop 即将进入
        ↓
处理 Timer
        ↓
处理 Source
        ↓
即将休眠
        ↓
被唤醒
        ↓
退出 RunLoop

可以通过CFRunLoopObserver 监听

常见状态:

objc 复制代码
kCFRunLoopEntry
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting
kCFRunLoopExit

RunLoop 一次完整运行过程

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

可以把它浓缩成:

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

RunLoop 和 线程保活

比如创建一个常驻线程:

objc 复制代码
    NSThread* thread = [[NSThread alloc] initWithBlock:^{
        
        [[NSRunLoop currentRunLoop] addPort:
               [NSPort port]
               forMode:NSDefaultRunLoopMode];

           [[NSRunLoop currentRunLoop] run];
    }];

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

  • 作用: 创建一个新的子线程对象
  • initWithBlock: 告诉这个线程; "当启动时候,执行 Block 里面的代码"
  • 此时,线程还没有启动, 知识准别好了
objc 复制代码
 [[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 提供的一系列方法,允许你延迟执行在指定线程上执行某个方法。

objc 复制代码
[self performSelector:@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 有密切关系

objc 复制代码
主线程
  ↓
performSelector
  ↓
目标线程 RunLoop
  ↓
唤醒
  ↓
执行 test

如果创建了一个子线程:

objc 复制代码
NSThread* thread = ... 

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

objc 复制代码
[[NSRunloop currentRunLoop] run]; 

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

RunLoop 与 GCD 区别

GCD更偏向 任务调度

例如:

objc 复制代码
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 后台任务
}); 

关注 任务 -> Queue -> Thread

RunLoop 更偏向 事件循环

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

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

相关推荐
黑科技iOS上架2 小时前
iOS提审5.1.1频繁被拒
ios·审核
蓝丶曦月2 小时前
Notion MacBook Apple M4 Pro 安装更新
macos·notion
我有满天星辰2 小时前
【从 0 打造我的本地 AI 知识库】在 M1 Mac 上搭建 Ollama:我的本地 AI 模型到底应该怎么选?
人工智能·macos
我是Superman丶16 小时前
Arduino IDE 安装教程:Windows/macOS/Linux 开发环境搭建
ide·windows·macos
k4m7v2pz17 小时前
Mac 上用 brew 安装 UTM 与 QEMU(代理配置全记录)
macos
麒麟马20 小时前
Swift 扩展归纳---Array (一)
ios·swift
用户21816970493020 小时前
iOS 底层原理 block
objective-c
千码君201621 小时前
提词器项目V1.0功能分析
ide·macos·xcode
2501_915106321 天前
Swift 4开发环境设置教程:xCode安装与Playground入门
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程