RunLoop — iOS中的事件处理循环,由CFRunLoop(Core Foundation)和NSRunLoop(Foundation)对象实现。这是一种等待事件(触摸、定时器、输入源、通知)并将其分发给线程上相应处理程序的机制。iOS中的每个线程最多只有一个RunLoop,但它仅为主线程自动创建。根据Apple CFRunLoop文档,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处理两种类型的事件源:Input Sources(输入源)和Timer Sources(定时器)。Input Sources传递异步事件:触摸、鼠标移动、套接字数据、来自其他线程的消息(performSelector:onThread:)。Timer Sources按计划传递同步事件:NSTimer、CADisplayLink。还有观察者(Observers)— 用于监控RunLoop状态的入口点。
RunLoop循环由连续的阶段组成:进入模式(kCFRunLoopEntry)、处理定时器(kCFRunLoopBeforeTimers)、处理输入源(kCFRunLoopBeforeSources)、处理源(kCFRunLoopAfterWaiting)、等待(sleep)、退出模式(kCFRunLoopExit)。如果当前迭代中没有处理任何事件,RunLoop会将线程置于不确定的睡眠状态,直到被新事件唤醒。
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模式)。
Android Looper — RunLoop的对应物。Looper.prepare()在线程上创建消息队列(MessageQueue),Looper.loop()启动一个无限处理循环。Handler将消息和Runnable发送到此队列。主要区别:RunLoop支持模式(modes),而Android Looper不支持。Looper处理所有消息而不按模式过滤,这使其更简单,但在优先级场景中灵活性较低(例如,iOS中的滚动在.tracking模式下与其他事件分开处理)。
RunLoop模式 — 是当前活动的源、定时器和观察者的集合。模式允许按优先级隔离事件处理。当用户滚动UITableView时,RunLoop切换到.tracking模式,在该模式下仅处理滚动事件和相应的定时器/动画。所有其他源(例如NSURLConnection)被暂停,直到退出滚动模式。
三种主要模式:.default(NSDefaultRunLoopMode)— 处理除滚动外所有事件的主要模式;.tracking(UITrackingRunLoopMode)— 在滚动或手势导航时激活;.common(NSRunLoopCommonModes)— 不是单独的模式,而是包含.default + .tracking的别名集合。将源添加到.commonModes会自动将其添加到集合中的所有模式。
| 模式 | Core Foundation常量 | Foundation常量 | 何时激活 |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | 正常状态,无滚动 |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | 滚动、手势识别 |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | 伪模式:default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | RunLoop首次启动 |
经典问题:添加到.default模式的NSTimer在滚动时停止工作,因为RunLoop切换到.tracking模式并不处理来自.default的定时器。解决方案 — 将定时器添加到.commonModes:RunLoop.current.add(timer, forMode: .common)。这将使定时器在.default和.tracking中都能工作。替代方案是使用DispatchQueue.main.async代替NSTimer,因为GCD在线程级别工作,而不是RunLoop模式级别。
后台线程默认没有RunLoop。如果需要在后台线程中启动NSTimer、处理NSInputStream/NSOutputStream或对performSelector:做出反应,必须手动创建和启动RunLoop。没有RunLoop,定时器和performSelector:永远不会工作 — 线程将执行代码并在不等待事件的情况下结束。
要在后台线程中创建RunLoop,只需在线程工作结束时调用RunLoop.current.run()。此调用无限期地阻塞线程,处理事件。要停止,请使用CFRunLoopStop(CFRunLoopGetCurrent())。重要提示:RunLoop.current在第一次访问时懒加载创建RunLoop — 如果您不调用run(),它不会处理事件。模式:配置源 -> 添加到RunLoop -> 调用run()。
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周期中发生。
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模式下工作。
CFRunLoopObserver — 用于跟踪RunLoop阶段的机制。通过Observer,您可以收到关于进入模式、定时器处理开始、源处理开始、从睡眠中唤醒、退出模式的通知。框架使用Observer满足其自身需求:Core Animation使用它们在RunLoop睡眠前渲染层,UIKit — 在处理事件后更新布局。
开发人员也可以添加Observer用于自己的目的。例如:测量事件处理时间(性能分析)、在RunLoop睡眠前执行延迟操作(当UI已更新且用户无交互时)、长时间不活动时自动保存数据。Observer通过CFRunLoopAddObserver注册,指定模式和跟踪活动的位掩码。
Observer最有用的点:.afterWaiting — 在RunLoop唤醒后执行,可以包含应在事件处理后运行的代码;.beforeTimers — 在定时器处理之前,允许测量自上次处理以来的时间;.exit — 在RunLoop停止时触发,用于清理后台线程资源。
CFRunLoopStop — 强制终止当前RunLoop迭代的函数。调用CFRunLoopStop(CFRunLoopGetCurrent())时,RunLoop完成当前事件的处理并退出run(),返回false。这是在后台线程上停止RunLoop的标准方式。在主线程上,不推荐使用CFRunLoopStop — 主RunLoop应在应用程序的整个生命周期内运行。对于后台线程,在CFRunLoopStop之后,线程可以结束或继续执行run()之后的后续代码。
常见问题
RunLoop — iOS中的事件循环,由CFRunLoop(Core Foundation)和NSRunLoop(Foundation)实现。它等待事件(触摸、定时器、输入源)并将它们分发给线程上的处理程序。每个线程可以有一个RunLoop,但仅为主线程自动创建。RunLoop管理模式(.default、.tracking、.common),按优先级隔离处理。
NSTimer默认添加到RunLoop的.default模式。当用户滚动时,RunLoop切换到.tracking模式并不处理来自.default的定时器。解决方案:通过RunLoop.current.add(timer, forMode: .common)将定时器添加到.common模式。.common合并了.default和.tracking,因此定时器在两种模式下都工作。
仅当后台线程使用定时器(NSTimer)、performSelector:onThread:、NSInputStream/NSOutputStream或Source事件时。如果线程执行同步任务(下载文件、计算)并结束 — 不需要RunLoop。要启动,请在配置源后调用RunLoop.current.run()。要停止 — CFRunLoopStop(CFRunLoopGetCurrent())。
RunLoop在线程级别工作,并带有模式(modes)支持地顺序处理事件。DispatchQueue — 线程池的抽象,任务在任何空闲线程上执行。GCD不支持模式并且独立于RunLoop存在。DispatchQueue.main使用主RunLoop执行块 — 这是唯一的交叉点。对于后台任务,GCD更受欢迎。
CADisplayLink — 与VSync(屏幕刷新率)同步的定时器。它被添加到RunLoop中并在BeforeTimers阶段在每个渲染帧之前触发。CADisplayLink仅在主线程上工作,因为屏幕渲染在那里进行。对于滚动时的连续动画,将其添加到.common模式:displayLink.add(to: .current, forMode: .common)。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。