iOS - RunLoop 相关知识点

为什么要有 RunLoop?

  • 背景:线程执行完任务就会退出,但主线程(或者一些后台线程等)我们希望它能够一直存在、持续等待事件(触摸、定时器、网络回调等)。

原始的解决方案:

  • 如果写成 while(1) {} 类似的死循环,会出现问题:
    • 线程会持续占用 CPU(忙等待),浪费资源。

🍎给出的答案:

  • 👉 RunLoop —— 让线程在有事件时被唤醒执行,没有事件时进入休眠。
    • 在没有 RunLoop 的情况下,线程要么退出、要么忙等,无法高效等待事件。
    • Apple 的设计是 —— 让线程进入一个由内核管理的 事件循环系统,当有事件发生时唤醒线程,没有事件时进入休眠。
    • 这就是 RunLoop 的核心思想:事件驱动 + 内核唤醒 + 自动调度。

核心概念

本质runloop本质是一个基于 事件驱动 的循环机制,底层依托内核的 mach port 进行等待和消息分发。用于 调度 定时器、输入源和观察者,让线程在“有事活跃、无事休眠”之间高效切换。

核心功能

  1. 保持程序的持续运行(长连接、后台任务等)
  2. 监听 App 中的各种事件(触摸、滑动、定时器事件等)
  3. 自动管理 AutoreleasePool
  4. 控制线程的状态,节省 CPU 资源,提高程序性能

好处:相较于 线程轮询runloop的工作机制效率更高。它允许在闲置时将线程置于睡眠状态,CPU进入低功耗状态,从而节约能源资源。

地位:在 iOS 中,RunLoop 是整个系统事件机制的核心支柱。主线程的 RunLoop 是 UIKit 事件响应、定时器、动画、触摸、AutoreleasePool、GCD 主队列 的共同基础。


组成角色

image

/// CFRunLoop 核心成员

struct __CFRunLoop {
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    mach_port_t _wakeUpPort;  // 唤醒 RunLoop 的系统端口
};

一、输入源 (Sources)

  • Source0(非端口事件): 用户态事件源,App内部事件,不能自动唤醒 runloop,需要手动 signal。
    • 特点:
      • 不依赖系统内核或端口。
      • RunLoop 不会被系统自动唤醒,必须手动调用 signal + wakeup
      • 常用于线程间通信 或 自定义事件调度
    • 例:
      • performSelector: onThread:
        1. 系统把 selector 封装事件为 source0。
        2. 加入到目标线程 runloop 的 source0 队列。
        3. 内部调用唤醒 signal + wakeup 唤醒 runloop。
          • 如果目标线程 runloop 正在执行,依然等待到下一轮循环的 source0 阶段再执行。
  • Source1(端口事件): 内核级事件源,由RunLoop和内核管理,Mach port驱动。
    • 特点:
      • 依赖系统的端口(mach port),用于接收内核或者其他线程发送的消息。
      • 用于系统级事件(触摸、网络)或者跨线程通信(CFMessagePortCFMachPort)。
      • 当有端口消息到达时,内核通过 mach_msg 唤醒 RunLoop 所在线程。
    • 例:
      • UI事件:触摸、点击(基于 mach port 通知主线程 runloop)
      • 网络事件:Socket 数据到达 → 内核通知 → Source1 被唤醒 → 调用回调处理数据。
      • Port 消息:NSMachPortCFMessagePort 等。

二、定时器 (Timers)

独立于 source 的一种事件类型。

  • 机制:
    • runloop 维护的一组定时器(NSTimer/performSelector:afterDelay:)。
    • 每轮循环时检查定时器的 “下一次触发时间”。
    • 如果到达了触发点,执行对应回调。
    • 若 runloop 处于休眠状态,内核会在最近一个 Timer 到期时唤醒线程。
  • 注意:
    • Timer 属于 runloop 的事件调度机制,不依赖 mach port 消息。

三、观察者 (Observers)

image

监听 RunLoop 状态变化的钩子(CFRunLoopObserver

  • 用途:
    • 监听 runloop 各个阶段(Entry/BeforeTimers/BeforeSources/BeforeWaiting/AfterWaiting/Exit)。
    • 常用于:
      • 性能调试(卡顿监测)
      • 帧率刷新(CADisplayLink 内部机制)
      • 自动管理 AutoreleasePool(系统就在此阶段创建/销毁池子)

四、模式 (Modes)

image

每个 RunLoop 必须且只能运行在一个 Mode 下(Mode 可切换)。
每个 Mode 定义了 RunLoop 本轮循环中要监听哪些事件源(SourcesTimersObservers)。

为什么需要 Mode ?

  • 因为不同场景下需要“隔离事件源”。
  • 比如用户滚动时,RunLoop 会切换到 tracking 模式以屏蔽 default 模式下的定时器。

常用 Mode

标题 描述 应用场景
NSDefaultRunLoopMode 默认模式 普通任务、定时器
UITrackingRunLoopMode UI追踪模式 滚动、拖拽时系统使用
NSRunLoopCommonModes 一种“标记集合”(非真正的Mode) 用来把 Source/Timer 同时加入多个 Mode
GSEventReceiveRunLoopMode 系统底层模式 接收系统级输入事件(触摸、键盘)
kCFRunLoopCommonModes Core Foundation 层关键字 与 NSRunLoopCommonModes 一致
  1. NSDefaultRunLoopMode

    • 主线程默认运行在该模式下。
    • 通过 NSTimerperformSelector:afterDelay: 创建的定时器都会加入这个 Mode
    • 缺点:当用户滚动 UIScrollView 时,RunLoop 会自动切换到 UITrackingRunLoopMode,导致 Default 模式下的 Timer 暂停。
    • 例子
      • Source0: performSelector:onThread:
      • Timer: NSTimerafterDelay:
      • Observer: AutoreleasePool create/drain(注册在 CommonModes 下)
  2. UITrackingRunLoopMode

    • 用户触摸、滚动、拖动页面时,runloop 会临时切换到 tracking 模式。
    • 滚动结束后,runloop 会自动切回 Default 模式。
    • 例子
      • Source1: 触摸 / 滑动事件(基于 Mach port
      • Observer: AutoReleasePool create/drain(注册在 CommonModes 下)
  3. NSRunLoopCommonModes

    • 不是 Mode,而是一个“模式集合标签”

    • 任何被加入到 CommonModesSource/Timer,会同时参与多个 Mode

    • 例子

      • 让某些 NSTimer 同时参与 default + tracking
      NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
          NSLog(@"Tick");
      }];
      [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
      
  4. 其他内部 Mode

    • UIInitializationRunLoopMode: App 启动阶段使用(私有)。
    • GSEventReceiveRunLoopModeCoreGraphics 层,用于接收系统级输入事件(触摸、键盘)。
    • kCFRunLoopCommonModes:CFRunLoop 层关键字,等价于 NSRunLoopCommonModes

模式之间的关系与区别

从上述文章看来,UITrackingRunLoopMode 和 GSEventReceiveRunLoopMode 都参与了 UI 事件处理,那他们是否冲突呢?

模式 所属层级 作用 谁在用?
GSEventReceiveRunLoopMode CoreGraphics/CoreAnimation 接收来自内核(Mach Port)的原始触摸、键盘事件 系统私有
UITrackingRunLoopMode UIKit 当用户开始滚动/拖拽时,UIKit 临时切换到此模式来追踪触摸事件 UIKit 控制的住 runloop
[ 内核 mach_msg ]
       ↓
[ CoreGraphics 接收原始触摸事件 ] (GSEventReceiveRunLoopMode)
       ↓
[ UIKit 处理触摸与滚动逻辑 ](UITrackingRunLoopMode)
       ↓
[ App 代码响应事件 ]

从他们的分工与所属不难看出,他们并不冲突,而是事件分发链路的上下层关系。

GSEventReceive 在底层接收输入,UITracking 在上层进行滚动和触摸追踪。


RunLoop 的工作原理

image


线程与 RunLoop

特性 主线程 runloop 子线程 runloop
自动创建
自动启动 ❌需要手动启动
自动管理 Pool
生命周期 app退出 手动退出、线程结束
常见用途 UI事件、主队列任务、动画刷新 后台任务、定时器、网络回调、自定义输入源
  1. 子线程为什么 Timer 不会触法?
    答:子线程没有 runloop 或没有进入循环。timer 依赖于 runloop,需要手动启动。
  1. 为什么 runloop 让线程“有事活跃,无事休眠”?
    答:sleep 阶段阻塞在内核消息等待上,不消耗 CPU

AutoreleasePool 与 RunLoop

AutoreleasePool:内存回收的延迟释放机制

RunLoop:基于事件驱动的循环机制

总体关系RunLoop 在每一轮循环中自动创建和销毁 AutoreleasePool,以此用来管理这一轮产生的 autorelease 对象,确保在循环结束时被及时释放。

线程与 AutoreleasePool 栈

  • 每个线程都有自己的 AutoreleasePool 栈结构(由 runtime 维护),哪怕没有显示写 @autoreleasepool
  • 但是线程启动时栈是空的,只有在执行 autorelease 操作时,系统才会 懒加载创建池页(AutoreleasePoolPage) 来接收对象。

主线程的行为

  • 主线程拥有默认的 RunLoop(由系统在 UIApplicationMain 启动时创建)。
  • 主线程 RunLoopUIKit 注册并托管,含有 pool observer,自动在每轮循环创建/销毁 AutoreleasePool
RunLoop 阶段 行为
Entry 创建新的 AutoreleasePool
BeforeWaiting 销毁旧 Pool 并新建一个空 Pool(防止长时间未释放)
Exit 销毁当前 Pool
  • 因此,主线程在每一轮事件循环结束时,都会清理掉该轮产生的临时对象。

子线程的行为

  • 子线程默认 没有 RunLoop,系统也不会自动为它管理 AutoreleasePool

    • 子线程 RunLoop 默认也没有 pool observer,不会自动管理 autoreleasepool,,因此必须在启动前包一层 @autoreleasepool,否则 RunLoop 循环期间 autorelease 对象不会及时释放, 直到线程退出才清空,可能引发内存峰值或泄漏。
    - (void)startBackgroundThread {
        NSThread *thread = [[NSThread alloc] initWithBlock:^{
            @autoreleasepool {
                [[NSRunLoop currentRunLoop] run];
            }
        }];
        [thread start];
    }
    
  • 但是子线程仍有 Pool 栈结构,只是如果不手动创建 Pool,也没有启动 RunLoopautorelease 对象会延迟到线程结束才被释放。

    • 若希望子线程能够周期性释放对象,有 2 种做法:
      1. 手动添加 @autoreleasepool;
      2. 启动该线程的 RunLoop,让系统周期性创建和销毁 Pool

GCD 与 RunLoop

GCD:线程的“任务调度机制”

RunLoop:线程的“事件循环机制”

总体关系GCD 负责把任务派发到线程,RunLoop 负责让线程保活,并在空闲时处理事件,两者是协同关系。

GCD 与 RunLoop 的底层交互机制

GCDRunLoop 的通信,是通过 Mach port 完成的。

  1. 当调用 dispatch_async(dispatch_get_main_queue(), block);
  2. 系统把 block 放入主队列;
  3. 发送 Mach 消息,通知主线程的 RunLoop;
  4. RunLoop 被唤醒;
  5. RunLoop 调用 _dispatch_main_queue_callback_4CF();
  6. 执行队列里的 GCD block。

所以 RunLoop 驱动 GCD 主队列的执行,没有 RunLoop,主线程就不会被唤醒,主队列的任务也不会被执行。

主线程上的关系

  • 主线程的 RunLoop 是由系统自动创建并运行的(在 UIApplicationMain 内部)。
  • 同时,主线程上也有 GCD 主队列(Main Queue)。

image

当你向主队列派任务时,任务不会立即执行,而是由系统通过 RunLoop 的唤醒机制唤醒主线程,然后在 RunLoopAfterWaiting 阶段执行这些主队列任务。

简言之: 主线程的 GCD 主队列,是通过 RunLoop 驱动执行的。

/// 二者关系:
[ GCD Main Queue ]
       ↓
[ dispatch_async(dispatch_get_main_queue(), ^{ ... }); ]
       ↓
[ 主线程执行任务(依靠 RunLoop 唤醒)]

子线程上的关系

  • 子线程默认没有 RunLoop,因此 GCD 线程池 中的 线程 在执行完任务后就会销毁;
  • 如果想让子线程 “常驻”,就要显示启动 RunLoop,然后 GCD 派发的任务才能在子线程上 持续被处理

参考链接

posted @ 2025-11-26 12:40  齐生  阅读(0)  评论(0)    收藏  举报