基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

如何利用 RunLoop 实现线程保活 (Keep Alive)?

知识点图片

利用 RunLoop 实现线程保活(Thread Keep Alive)是 iOS/macOS 开发中的一个经典面试题,也是处理后台常驻任务(如网络监控、语音识别监听等)的常用技巧。

默认情况下,线程执行完入口方法(Block 或 Selector)后就会自动退出并释放。要让线程“活”下来等待下一次调用,我们需要启用该线程的 RunLoop。

以下是实现线程保活的核心原理和具体步骤。


核心原理

  1. RunLoop 与线程的关系:每个线程都有一个对应的 RunLoop,但只有主线程的 RunLoop 是默认启动的。子线程的 RunLoop 需要手动获取并启动。
  2. RunLoop 的退出机制:如果 RunLoop 中没有任何 Source (输入源/Port)、Timer (定时器) 或 Observer (观察者),RunLoop 启动后会立刻退出。
  3. 保活的关键:为了不让 RunLoop 退出,我们需要在启动 RunLoop 之前,向其添加一个占位的 Port(或者 Timer),即使这个 Port 什么都不做。

实现步骤 (Swift 示例)

我们将创建一个自定义的线程类,并提供启动和停止的方法。

1. 简单的实现 (无法优雅停止)

如果你不需要停止这个线程(比如它伴随 App 的整个生命周期),可以使用最简单的方法:

plaintext
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 标记。

plaintext
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:

plaintext
// 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];

总结

实现线程保活的三板斧:

  1. 创建线程
  2. 添加 Source:在线程入口处,给 RunLoop 添加一个 NSMachPortCFRunLoopSource
  3. 循环启动:使用 while 循环调用 runMode:beforeDate: 来维持 RunLoop 运转,并配合 Bool 标记和 CFRunLoopStop 来控制退出。
00:00
00:00