RunLoop 8连问答

52 阅读3分钟

参考源码位置:在 Apple Open Source

🔹 8连问+答 全览

问题答案简述
RunLoop 可以做什么?管理事件循环、保活线程、调度 Timer、处理异步事件
线程与 RunLoop关系每线程一个 RunLoop,主线程自动启动,子线程需手动启动
RunLoop组成Mode、Input Source、Timer Source、Observer
如何启动主线程自动,子线程需 run()
启动条件线程存在+至少一个事件源
状态Entry、BeforeTimers、BeforeSources、BeforeWaiting、AfterWaiting、Exit
子线程需要保活吗需要;主线程不需要
保活方式启动RunLoop、死循环、GCD Timer、持续添加任务

1️⃣ RunLoop 可以做什么?

RunLoop 是线程的事件循环,用来管理和调度事件、定时器、输入源等。它的主要作用:

  • 保持线程在有任务时运行、没任务时休眠,提高性能。
  • 监听各种事件源(触摸事件、网络、Port、Timer等),并分发到对应的处理函数。
  • 定时任务调度(Timer)。
  • 处理异步事件(例如 performSelector:onThread:)。
  • 保持线程“保活”(不退出)。

简单理解:RunLoop 就像线程的“心脏”,不停地循环等待、接收、处理事件。


2️⃣ 线程跟 RunLoop 有什么关系?

  • 每条线程都有一个唯一的 RunLoop(由系统创建,但只有在第一次访问时才会真正创建)。(_CFRunLoopGet0方法中建议看看)
  • 主线程的 RunLoop 在 App 启动时由系统自动启动,用来处理 UI 事件。
  • 子线程默认没有启动 RunLoop,如果需要事件驱动或保活,需要手动启动。
  • RunLoop 是线程的辅助工具,不是线程本身;它依赖线程存在,线程结束时 RunLoop 也随之销毁。

3️⃣ RunLoop 的组成

RunLoop 主要由以下几个部分组成:

  1. Mode(运行模式)

    • 不同模式包含不同的事件源。
    • 常见模式:
      • NSDefaultRunLoopMode:默认模式,处理普通事件。
      • UITrackingRunLoopMode:处理 UI 滑动等高优先级事件。
      • NSRunLoopCommonModes:公共模式组,包含多个模式。
  2. Input Source(输入源)

    • Port Source:基于内核的消息源(Mach Port)。
    • Custom Source:自定义事件源。
  3. Timer Source(定时器源)

    • 基于时间的事件(Timer)。
  4. Observer(观察者)

    • 监听 RunLoop 状态变化(进入循环、处理事件、休眠、退出等)。

4️⃣ 如何启动 RunLoop?

  • 主线程:系统自动启动,不需要手动启动。
  • 子线程:需要手动调用 run()CFRunLoopRun()

示例:子线程启动 RunLoop

Thread {
    print("子线程启动")
    let runLoop = RunLoop.current
    runLoop.add(NSMachPort(), forMode: .default)
    runLoop.run() // 启动循环
}.start()

5️⃣ 启动 RunLoop 有哪些条件?

RunLoop 启动的前提:

  1. 线程必须存在。
  2. 必须有至少一个事件源(Input Source 或 Timer),否则启动后会立即退出。
  3. 调用 run()CFRunLoopRun() 开始循环。

示例:无事件源会立即退出

Thread {
    RunLoop.current.run() // 没有事件源,RunLoop立刻退出
    print("RunLoop退出")
}.start()

6️⃣ RunLoop 有几种状态?

RunLoop 内部状态(CFRunLoopActivity):

  1. kCFRunLoopEntry:进入 RunLoop。
  2. kCFRunLoopBeforeTimers:即将处理 Timer。
  3. kCFRunLoopBeforeSources:即将处理 Source。
  4. kCFRunLoopBeforeWaiting:即将休眠。
  5. kCFRunLoopAfterWaiting:刚从休眠中唤醒。
  6. kCFRunLoopExit:退出 RunLoop。

示例:监听 RunLoop 状态

let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.allActivities.rawValue, true, 0) { _, activity in
    print("RunLoop状态变化: \(activity)")
}
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, .defaultMode)

7️⃣ 子线程需要保活,主线程需要吗?为什么?

  • 主线程不需要保活:因为系统在 App 启动时就启动了主线程的 RunLoop,并一直运行,负责 UI 事件处理。
  • 子线程需要保活:子线程执行完任务后会退出,如果需要持续处理事件(如网络回调、定时器、Port通信),必须启动 RunLoop 保活。

8️⃣ 线程保活有几种方式?

常见方式:

  1. 启动 RunLoop
Thread {
    RunLoop.current.add(NSMachPort(), forMode: .default)
    RunLoop.current.run()
}.start()
  1. 使用 while(true) 循环(不推荐,CPU占用高)
Thread {
    while true {
        Thread.sleep(forTimeInterval: 1)
    }
}.start()
  1. 使用 GCD 的 dispatch_source 保活
let queue = DispatchQueue(label: "keepAliveThread")
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: 2)
timer.setEventHandler {
    print("定时任务执行")
}
timer.resume()
  1. 使用 NSOperationQueue + Operation 保活(任务持续添加)