如何利用 RunLoop 实现线程保活 (Keep Alive)?
利用 RunLoop 实现线程保活(Thread Keep Alive)是 iOS/macOS 开发中的一个经典面试题,也是处理后台常驻任务(如网络监控、语音识别监听等)的常用技巧。
默认情况下,线程执行完入口方法(Block 或 Selector)后就会自动退出并释放。要让线程“活”下来等待下一次调用,我们需要启用该线程的 RunLoop。
以下是实现线程保活的核心原理和具体步骤。
核心原理
- RunLoop 与线程的关系:每个线程都有一个对应的 RunLoop,但只有主线程的 RunLoop 是默认启动的。子线程的 RunLoop 需要手动获取并启动。
- RunLoop 的退出机制:如果 RunLoop 中没有任何 Source (输入源/Port)、Timer (定时器) 或 Observer (观察者),RunLoop 启动后会立刻退出。
- 保活的关键:为了不让 RunLoop 退出,我们需要在启动 RunLoop 之前,向其添加一个占位的 Port(或者 Timer),即使这个 Port 什么都不做。
实现步骤 (Swift 示例)
我们将创建一个自定义的线程类,并提供启动和停止的方法。
1. 简单的实现 (无法优雅停止)
如果你不需要停止这个线程(比如它伴随 App 的整个生命周期),可以使用最简单的方法:
var keepAliveThread: Thread?
func startSimpleThread() {
keepAliveThread = Thread {
// 1. 获取当前 RunLoop
let runLoop = RunLoop.current
// 2. 添加一个 Port 防止 RunLoop 退出
// NSMachPort 是一个基于 Mach 端口的输入源
runLoop.add(NSMachPort(), forMode: .default)
// 3. 启动 RunLoop
// 注意:run() 方法是死循环,很难从外部停止,除非线程强行销毁
runLoop.run()
}
keepAliveThread?.start()
}
2. 健壮的实现 (支持优雅停止 - 推荐)
在实际开发中,我们通常需要控制线程的生命周期(可以随时停止)。直接调用 run() 会导致无法停止,因此我们需要使用 run(mode:before:) 配合一个 Bool 标记。
class PermenantThread {
private var thread: Thread?
private var isStopped = false
init() {
thread = Thread { [weak self] in
guard let self = self else { return }
// 1. 向 RunLoop 添加 Source (Port)
// 这一步是必须的,否则 runMode 会立刻返回
RunLoop.current.add(NSMachPort(), forMode: .default)
// 2. 启动 RunLoop
// 使用 while 循环配合 runMode
// runMode:before: 会让 RunLoop 处理一次事件或等待直到超时后返回
while !self.isStopped {
RunLoop.current.run(mode: .default, before: .distantFuture)
}
print("线程已结束运行")
}
thread?.name = "MyKeepAliveThread"
thread?.start()
}
// 执行任务
func executeTask(_ task: @escaping () -> Void) {
guard let thread = thread else { return }
// 利用 perform 在指定线程执行
perform(#selector(runTask(_:)), on: thread, with: task, waitUntilDone: false)
}
@objc private func runTask(_ task: @escaping () -> Void) {
task()
}
// 停止线程
func stop() {
guard let thread = thread else { return }
// 在该线程中执行停止逻辑
perform(#selector(stopThread), on: thread, with: nil, waitUntilDone: false)
}
@objc private func stopThread() {
// 1. 设置标记位
isStopped = true
// 2. 停止 RunLoop
// CFRunLoopStop 能够强制唤醒并停止当前的 runMode 循环
CFRunLoopStop(CFRunLoopGetCurrent())
// 3. 清理引用
thread = nil
}
deinit {
stop()
print("PermenantThread deinit")
}
}
关键点解析
1. 为什么要加 NSMachPort?
RunLoop 的设计理念是“有事做事,没事睡觉”。但是,如果 RunLoop 启动时检测到没有任何输入源(Source/Timer/Observer),它会认为“没事可做且未来也不会有事”,于是直接退出。添加 NSMachPort 相当于给它挂了一个空的输入源,告诉 RunLoop:“这里有个端口,虽然现在没消息,但你要等着。”
2. 为什么不用 RunLoop.current.run()?
run() 方法的文档说明它会无限循环调用 run(mode:before:)。一旦调用 run(),代码就会卡在那一行,你无法通过简单的标志位来终止它。
正确的做法是使用 run(mode:before:) 放在一个 while 循环中,这样每次 RunLoop 唤醒并处理完事件(或超时)后,会检查 while 的条件(!isStopped),从而决定是否继续。
3. 为什么停止时需要 CFRunLoopStop?
当你设置 isStopped = true 时,线程可能正处于 runMode 的休眠状态(等待事件)。如果不唤醒它,它可能要等到 distantFuture 才会醒来检查 while 条件。CFRunLoopStop(CFRunLoopGetCurrent()) 的作用是强制停止当前的 RunLoop 运行,让代码立即跳出 run(mode:before:),进而走到 while 判断逻辑,实现线程的优雅退出。
C 语言 (Core Foundation) 写法
有时候为了更底层的控制,开发者会直接使用 C 语言的 API:
// Objective-C 示例
NSThread *thread = [[NSThread alloc] initWithBlock:^{
// 创建上下文(Source)
CFRunLoopSourceContext context = {0};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// 添加 Source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 启动循环
// returnAfterSourceHandled: false 表示处理完事件不退出,一直运行
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
// 清理
CFRelease(source);
}];
[thread start];
总结
实现线程保活的三板斧:
- 创建线程。
- 添加 Source:在线程入口处,给
RunLoop添加一个NSMachPort或CFRunLoopSource。 - 循环启动:使用
while循环调用runMode:beforeDate:来维持 RunLoop 运转,并配合Bool标记和CFRunLoopStop来控制退出。