iOS中的RunLoop:什么是、工作模式及事件循环

作者: IT Sectr 发布日期: 2026-03-16 阅读时间: 11 分钟

RunLoop — iOS中的事件处理循环,由CFRunLoop(Core Foundation)和NSRunLoop(Foundation)对象实现。这是一种等待事件(触摸、定时器、输入源、通知)并将其分发给线程上相应处理程序的机制。iOS中的每个线程最多只有一个RunLoop,但它仅为主线程自动创建。根据Apple CFRunLoop文档,RunLoop对于后台线程中定时器、动画和源监控的工作至关重要。

要点

  • RunLoop — 事件循环,处理线程上的事件:触摸、定时器、输入源
  • 主线程拥有自动RunLoop(CFRunLoopGetMain()),后台线程需要手动启动
  • 三种模式:.default(主要)、.tracking(滚动)、.common(合并default+tracking)
  • NSTimer和CADisplayLink在没有活动RunLoop的线程上无法工作
  • RunLoop观察者可以对模式的进入/退出以及处理的开始/结束做出反应

什么是RunLoop

RunLoop — 是一个Core Foundation基础设施对象,用于组织线程上的事件处理。本质上它是一个无限循环(while true),等待事件(sources)的到来并将它们传递给处理程序。当没有事件时,RunLoop使线程进入睡眠状态(sleep),节省电池能量。当事件到达时,线程被唤醒,处理事件,然后重新入睡。RunLoop仅存在于iOS/macOS(XNU + Core Foundation)中 — 在Android中,其角色由Looper扮演。

每个线程最多有一个RunLoop,它在第一次访问时懒加载(lazy)创建。对于主线程,RunLoop在应用程序启动时自动创建。对于后台线程,RunLoop在调用CFRunLoopGetCurrent()或RunLoop.current之前不会创建。应用程序的主RunLoop负责处理触摸事件、屏幕渲染、执行DispatchQueue.main块和为Core Animation层服务。

RunLoop不是线程 — 它是线程内部的机制。线程可以在没有RunLoop的情况下存在(如果执行同步任务并结束),但RunLoop不能在没有线程的情况下存在。当具有活动RunLoop的线程没有事件时,它不会阻塞CPU,而是处于等待状态(waiting)— 这是与消耗100% CPU的busy-wait循环的关键区别。

RunLoop如何工作:事件循环的解剖

RunLoop处理两种类型的事件源:Input Sources(输入源)和Timer Sources(定时器)。Input Sources传递异步事件:触摸、鼠标移动、套接字数据、来自其他线程的消息(performSelector:onThread:)。Timer Sources按计划传递同步事件:NSTimer、CADisplayLink。还有观察者(Observers)— 用于监控RunLoop状态的入口点。

RunLoop循环由连续的阶段组成:进入模式(kCFRunLoopEntry)、处理定时器(kCFRunLoopBeforeTimers)、处理输入源(kCFRunLoopBeforeSources)、处理源(kCFRunLoopAfterWaiting)、等待(sleep)、退出模式(kCFRunLoopExit)。如果当前迭代中没有处理任何事件,RunLoop会将线程置于不确定的睡眠状态,直到被新事件唤醒。

swift
import Foundation

// 通过Observer演示RunLoop阶段
func observeRunLoopActivities() {
    let observer = CFRunLoopObserverCreateWithHandler(
        nil,
        CFOptionFlags([[.entry, .beforeTimers, .beforeSources,
                           .afterWaiting, .exit]]),
        true,           // repeats
        0               // priority
    ) { observer, activity in
        switch activity {
        case .entry:
            print("Entry — RunLoop已激活")
        case .beforeTimers:
            print("BeforeTimers — 处理定时器")
        case .beforeSources:
            print("BeforeSources — 处理源")
        case .afterWaiting:
            print("AfterWaiting — 从睡眠中唤醒")
        case .exit:
            print("Exit — RunLoop已结束")
        default:
            break
        }
    }

    CFRunLoopAddObserver(
        CFRunLoopGetCurrent(),
        observer,
        .commonModes
    )
}

// 示例:RunLoop在主线程上处理定时器
func timerOnMainRunLoop() {
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
        print("Tick: \(Date())")
    }

    // Main Thread上的RunLoop.current.run()由UIApplicationMain调用
    // 自动 — 无需手动启动
    RunLoop.current.run() // 此调用不会返回主线程
}

observeRunLoopActivities示例在主RunLoop上注册了一个Observer,记录循环的每个阶段。这对调试很有用:如果您看到.beforeTimers和.afterWaiting之间有很长的间隔,意味着RunLoop被主线程上的操作阻塞了。timerOnMainRunLoop展示了NSTimer如何自动在主RunLoop上工作 — 创建Timer.scheduledTimer时,定时器默认添加到当前RunLoop中(.default模式)。

RunLoop与Looper(Android)对比

Android Looper — RunLoop的对应物。Looper.prepare()在线程上创建消息队列(MessageQueue),Looper.loop()启动一个无限处理循环。Handler将消息和Runnable发送到此队列。主要区别:RunLoop支持模式(modes),而Android Looper不支持。Looper处理所有消息而不按模式过滤,这使其更简单,但在优先级场景中灵活性较低(例如,iOS中的滚动在.tracking模式下与其他事件分开处理)。

RunLoop模式:default, tracking, common

RunLoop模式 — 是当前活动的源、定时器和观察者的集合。模式允许按优先级隔离事件处理。当用户滚动UITableView时,RunLoop切换到.tracking模式,在该模式下仅处理滚动事件和相应的定时器/动画。所有其他源(例如NSURLConnection)被暂停,直到退出滚动模式。

三种主要模式:.default(NSDefaultRunLoopMode)— 处理除滚动外所有事件的主要模式;.tracking(UITrackingRunLoopMode)— 在滚动或手势导航时激活;.common(NSRunLoopCommonModes)— 不是单独的模式,而是包含.default + .tracking的别名集合。将源添加到.commonModes会自动将其添加到集合中的所有模式。

模式Core Foundation常量Foundation常量何时激活
.defaultkCFRunLoopDefaultModeRunLoop.Mode.default正常状态,无滚动
.trackingUITrackingRunLoopModeRunLoop.Mode.tracking滚动、手势识别
.commonkCFRunLoopCommonModesRunLoop.Mode.common伪模式:default + tracking
.initialRunkCFRunLoopInitialRunRunLoopModeRunLoop首次启动

为什么NSTimer在滚动时不工作

经典问题:添加到.default模式的NSTimer在滚动时停止工作,因为RunLoop切换到.tracking模式并不处理来自.default的定时器。解决方案 — 将定时器添加到.commonModes:RunLoop.current.add(timer, forMode: .common)。这将使定时器在.default和.tracking中都能工作。替代方案是使用DispatchQueue.main.async代替NSTimer,因为GCD在线程级别工作,而不是RunLoop模式级别。

后台线程中的RunLoop

后台线程默认没有RunLoop。如果需要在后台线程中启动NSTimer、处理NSInputStream/NSOutputStream或对performSelector:做出反应,必须手动创建和启动RunLoop。没有RunLoop,定时器和performSelector:永远不会工作 — 线程将执行代码并在不等待事件的情况下结束。

要在后台线程中创建RunLoop,只需在线程工作结束时调用RunLoop.current.run()。此调用无限期地阻塞线程,处理事件。要停止,请使用CFRunLoopStop(CFRunLoopGetCurrent())。重要提示:RunLoop.current在第一次访问时懒加载创建RunLoop — 如果您不调用run(),它不会处理事件。模式:配置源 -> 添加到RunLoop -> 调用run()。

swift
import Foundation

// 具有自己RunLoop的后台线程
class BackgroundRunLoopManager {

    private let thread: Thread
    private var isRunning = false

    init() {
        thread = Thread { [weak self] in
            // 调用RunLoop.current时自动创建RunLoop
            let runLoop = RunLoop.current

            // 添加端口以保持RunLoop活动
            runLoop.add(Port(), forMode: .default)

            // 开始处理事件
            var isFinished = false
            while !isFinished {
                // run(mode:before:)在事件处理时返回true
                isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
            }
        }
        thread.name = "com.app.background-runloop"
    }

    func start() {
        thread.start()
        isRunning = true
    }

    func stop() {
        // 在后台线程上停止RunLoop
        self.perform(
            #selector(BackgroundRunLoopManager.stopRunLoop),
            on: thread,
            with: nil,
            waitUntilDone: false
        )
    }

    @objc
    private func stopRunLoop() {
        CFRunLoopStop(CFRunLoopGetCurrent())
        isRunning = false
    }
}

// 使用:后台RunLoop上的定时器
let manager = BackgroundRunLoopManager()
manager.start()

// 通过performSelector将任务发送到后台RunLoop
manager.perform(
    #selector(BackgroundRunLoopManager.backgroundTask),
    on: manager.thread,
    with: nil,
    waitUntilDone: false
)

BackgroundRunLoopManager创建一个具有永久RunLoop的后台线程。添加一个空的Port()是必要的,以防止RunLoop立即结束 — 没有源,RunLoop.run()返回false并退出。performSelector:onThread:向后台RunLoop发送消息 — 当RunLoop进入BeforeSources阶段时将被处理。Stop在后台线程上调用CFRunLoopStop,结束循环。

NSTimer创建一个定时器事件,RunLoop在BeforeTimers阶段处理该事件。定时器可以是repeating(重复)和non-repeating(一次性)。NSTimer不保证精确性:如果RunLoop被长时间操作阻塞,定时器将在阻塞解除后激活,并且所有错过的激活将合并为一个(对于重复定时器 — 最多一次“追赶”激活)。

CADisplayLink — 一种与屏幕刷新率(60/120/144 Hz)同步的专用定时器。用于动画和视频更新。CADisplayLink被添加到RunLoop中,并在每个渲染帧之前(在Core Animation将层发送到渲染之前)激活。如果帧被跳过(display link未能在16毫秒内激活),下一次调用将在下一个VSync周期中发生。

swift
import UIKit

class AnimationController {

    private var displayLink: CADisplayLink?
    private var displayLinkTimer: Timer?
    private var startTime: CFTimeInterval = 0

    // CADisplayLink — 与vsync的动画
    func startDisplayLinkAnimation() {
        displayLink = CADisplayLink(target: self,
                                       selector: #selector(step))
        // 添加到.common模式 — 滚动时也能工作
        displayLink?.add(to: .current, forMode: .common)
        startTime = CACurrentMediaTime()
    }

    @objc
    private func step(displayLink: CADisplayLink) {
        let elapsed = CACurrentMediaTime() - startTime
        // 每帧调用(60 FPS → 每16.6毫秒)
        print("Frame at \(elapsed) seconds")

        if elapsed > 5.0 {
            displayLink.invalidate() // 5秒后停止
        }
    }

    // NSTimer — 周期性任务
    func startTimerInCommonMode() {
        displayLinkTimer?.invalidate()
        displayLinkTimer = Timer.scheduledTimer(
            withTimeInterval: 1.0,
            repeats: true
        ) { [weak self] timer in
            print("Timer tick")
        }

        // 关键:添加到.common,否则定时器在滚动时冻结
        RunLoop.current.add(displayLinkTimer!, forMode: .common)
    }

    func stop() {
        displayLink?.invalidate()
        displayLinkTimer?.invalidate()
    }
}

AnimationController中,CADisplayLink被添加到.common模式,这保证了step在每个帧上被调用,无论滚动如何。displayLink.add(to: .current, forMode: .common) — 对于在滚动时不应中断的动画的标准模式。NSTimer也被添加到.common模式,以便在滚动时计时。没有这个,定时器只会在.default模式下工作。

RunLoop观察者:监控事件循环

CFRunLoopObserver — 用于跟踪RunLoop阶段的机制。通过Observer,您可以收到关于进入模式、定时器处理开始、源处理开始、从睡眠中唤醒、退出模式的通知。框架使用Observer满足其自身需求:Core Animation使用它们在RunLoop睡眠前渲染层,UIKit — 在处理事件后更新布局。

开发人员也可以添加Observer用于自己的目的。例如:测量事件处理时间(性能分析)、在RunLoop睡眠前执行延迟操作(当UI已更新且用户无交互时)、长时间不活动时自动保存数据。Observer通过CFRunLoopAddObserver注册,指定模式和跟踪活动的位掩码。

Observer最有用的点:.afterWaiting — 在RunLoop唤醒后执行,可以包含应在事件处理后运行的代码;.beforeTimers — 在定时器处理之前,允许测量自上次处理以来的时间;.exit — 在RunLoop停止时触发,用于清理后台线程资源。

CFRunLoopStop和循环终止

CFRunLoopStop — 强制终止当前RunLoop迭代的函数。调用CFRunLoopStop(CFRunLoopGetCurrent())时,RunLoop完成当前事件的处理并退出run(),返回false。这是在后台线程上停止RunLoop的标准方式。在主线程上,不推荐使用CFRunLoopStop — 主RunLoop应在应用程序的整个生命周期内运行。对于后台线程,在CFRunLoopStop之后,线程可以结束或继续执行run()之后的后续代码。

常见问题

iOS中的RunLoop是什么?

RunLoop — iOS中的事件循环,由CFRunLoop(Core Foundation)和NSRunLoop(Foundation)实现。它等待事件(触摸、定时器、输入源)并将它们分发给线程上的处理程序。每个线程可以有一个RunLoop,但仅为主线程自动创建。RunLoop管理模式(.default、.tracking、.common),按优先级隔离处理。

为什么NSTimer在滚动时不工作?

NSTimer默认添加到RunLoop的.default模式。当用户滚动时,RunLoop切换到.tracking模式并不处理来自.default的定时器。解决方案:通过RunLoop.current.add(timer, forMode: .common)将定时器添加到.common模式。.common合并了.default和.tracking,因此定时器在两种模式下都工作。

需要在后台线程中启动RunLoop吗?

仅当后台线程使用定时器(NSTimer)、performSelector:onThread:、NSInputStream/NSOutputStream或Source事件时。如果线程执行同步任务(下载文件、计算)并结束 — 不需要RunLoop。要启动,请在配置源后调用RunLoop.current.run()。要停止 — CFRunLoopStop(CFRunLoopGetCurrent())。

RunLoop与GCD DispatchQueue有何不同?

RunLoop在线程级别工作,并带有模式(modes)支持地顺序处理事件。DispatchQueue — 线程池的抽象,任务在任何空闲线程上执行。GCD不支持模式并且独立于RunLoop存在。DispatchQueue.main使用主RunLoop执行块 — 这是唯一的交叉点。对于后台任务,GCD更受欢迎。

CADisplayLink与RunLoop如何关联?

CADisplayLink — 与VSync(屏幕刷新率)同步的定时器。它被添加到RunLoop中并在BeforeTimers阶段在每个渲染帧之前触发。CADisplayLink仅在主线程上工作,因为屏幕渲染在那里进行。对于滚动时的连续动画,将其添加到.common模式:displayLink.add(to: .current, forMode: .common)。

总结

  • RunLoop — iOS事件循环:在线程上处理触摸、定时器、输入源;主线程具有自动RunLoop
  • 三种模式:.default(通用)、.tracking(滚动)、.common(default + tracking)— 管理事件过滤
  • .default中的NSTimer在滚动时不工作 — 解决方案:通过RunLoop.current.add(timer, forMode: .common)添加到.common模式
  • 后台线程默认没有RunLoop — 对于定时器和performSelector:需要通过RunLoop.current.run()手动启动
  • CADisplayLink — 每个VSync帧的定时器,对平滑动画至关重要;添加到.common以在滚动时工作
  • RunLoop Observer — 阶段监控:Entry、BeforeTimers、BeforeSources、AfterWaiting、Exit;用于性能分析
  • RunLoop ≠ Looper:iOS RunLoop支持模式和定时器,Android Looper更简单 — 无模式,Handler + MessageQueue

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读