iOS RunLoop 深度解析:从原理到实战

18 阅读6分钟

iOS RunLoop 深度解析:从原理到实战

什么是 RunLoop?

简单来说,RunLoop 是一个事件循环机制。它的主要任务是让线程在有事件时处理事件,没有事件时进入休眠状态,从而避免线程资源的浪费。

你可以把它想象成一个"死循环",但这个循环非常智能,它知道什么时候该工作,什么时候该休息。

RunLoop 与线程的关系

  • 一一对应:一个线程对应一个 RunLoop(存储在 TLS 中)。
  • 懒加载:线程默认不会创建 RunLoop,只有在第一次获取时(如调用 [NSRunLoop currentRunLoop])才会创建。
  • 主线程:App 启动时,系统会自动创建并启动主线程的 RunLoop。
  • 子线程:需要手动获取和启动 RunLoop,且在线程结束时需手动停止。

RunLoop 的核心组件

一个 RunLoop 由以下几个关键部分组成:

  • CFRunLoopRef:RunLoop 的核心对象。
  • CFRunLoopModeRef:Mode 是事件的容器,用于隔离不同类型的事件。一个 RunLoop 可以有多个 Mode,但一次只能运行在一个 Mode 下。
    • NSDefaultRunLoopMode:默认模式,处理大部分普通事件。
    • UITrackingRunLoopMode:跟踪模式,在 UIScrollView 滚动时切换到此模式,保证滑动的流畅性。
    • NSRunLoopCommonModes:这是一个"伪模式",它代表一组模式的集合。将事件源添加到这里,意味着它在 DefaultMode 和 TrackingMode 下都会生效。
  • CFRunLoopSourceRef:事件源,是 RunLoop 需要处理的事件。
    • Source0:处理应用内部事件,如 performSelector:、GCD 主队列任务。它不会主动唤醒 RunLoop。
    • Source1:处理系统内核事件,如触摸、网络回调。它基于 Mach Port,可以主动唤醒 RunLoop。
  • CFRunLoopTimerRef:定时器,如 NSTimer、CADisplayLink。
  • CFRunLoopObserverRef:观察者,用于监听 RunLoop 的状态变化。

⚙️ RunLoop 的工作流程

RunLoop 的一次完整循环大致如下:

  1. 进入 (Entry):通知 Observer,RunLoop 即将开始。
  2. 处理定时器 (BeforeTimers):检查并处理到期的 Timer。
  3. 处理 Source0 (BeforeSources):处理应用内部的事件源。
  4. 检查 Source1 并休眠 (BeforeWaiting):如果没有 Source1 事件,RunLoop 会通知 Observer 并进入休眠。
  5. 被唤醒 (AfterWaiting):当有 Source1 事件(如触摸)到来时,RunLoop 被唤醒,并通知 Observer。
  6. 处理事件:处理唤醒它的 Source1 事件。
  7. 退出 (Exit):如果满足退出条件,则通知 Observer 并退出循环。

⚙️ RunLoop 的应用场景

1. 线程保活 (常驻线程)

让子线程在执行完任务后不立即销毁,而是通过启动 RunLoop 进入"休眠-唤醒"模式,随时准备处理新任务。

  • 关键点:启动 RunLoop 前,必须添加一个 Source(如 NSMachPort)或 Timer,否则 RunLoop 会因无事可做而立即退出。
  • 典型应用:AFNetworking 的后台网络请求线程。

2. 解决定时器 (NSTimer) 延迟问题

  • 现象:UIScrollView 滚动时,NSTimer 会暂停。
  • 原因:滚动时,RunLoop 切换到 UITrackingRunLoopMode,而 NSTimer 默认在 NSDefaultRunLoopMode 下,因此被忽略。
  • 解决方案:将 NSTimer 添加到 NSRunLoopCommonModes

3. 性能优化与卡顿监控

通过 CFRunLoopObserver 监听 RunLoop 的状态。

  • 原理:在 kCFRunLoopBeforeWaiting(即将休眠)和 kCFRunLoopAfterWaiting(刚刚唤醒)记录时间戳。如果时间差超过阈值(如 50ms),则说明有耗时操作阻塞了主线程,即发生卡顿。

4. 界面更新 (UI Rendering)

当你调用 setNeedsLayoutsetNeedsDisplay 时,UI 并不会立即更新。系统会在 RunLoop 即将进入休眠(kCFRunLoopBeforeWaiting)时,统一处理所有待更新的视图,进行布局和绘制。这保证了在一次循环中,无论 UI 被修改多少次,最终只绘制一次,非常高效。

5. AutoreleasePool 的创建与释放

主线程的 RunLoop 通过 Observer 自动管理 @autoreleasepool

  • 进入时:创建一个自动释放池。
  • 即将休眠时:释放旧的池,并创建一个新的。
  • 退出时:释放最外层的池。

这解释了为什么在主线程中我们通常不需要手动管理 @autoreleasepool

关键细节与底层原理(进阶)

1. Source0 与 Source1 的本质区别

  • 唤醒能力:Source1 基于 Mach Port,由内核或其他进程主动发送消息,可以主动唤醒处于休眠状态的 RunLoop(如触摸事件)。Source0 只是应用内部的事件标记,不能唤醒 RunLoop,只有当 RunLoop 已经被其他事件唤醒并处于活动状态时,才会处理 Source0。
  • 处理时机:Source1 的优先级通常高于 Source0。

2. performSelector:withObject:afterDelay: 的陷阱

这个方法底层是通过 RunLoop 的定时器(Timer)实现的。

  • 依赖:它必须在 RunLoop 已经启动的线程中调用才会生效。
  • 场景:如果在没有启动 RunLoop 的普通子线程中调用该方法,定时器无法添加到 RunLoop 中,因此该选择器将永远不会执行。

3. GCD 主队列与 RunLoop 的关系

GCD 的主队列任务也是通过 RunLoop 执行的。

  • 机制:GCD 向主线程提交任务时,实际上是向主线程 RunLoop 的 Source1(dispatch 端口)发送了一个消息。
  • 流程:消息唤醒主线程 RunLoop → RunLoop 处理 Source1 → 触发 Source0 回调 → 执行 GCD Block。

4. 使用 CommonModes 的注意事项

虽然 NSRunLoopCommonModes 解决了定时器在滚动时暂停的问题,但需要谨慎使用。

  • 性能风险:如果一个高频执行的任务(如每 0.1 秒触发一次的 Timer)被添加到 CommonModes,它会在滚动时依然执行。这会抢占 CPU 资源,可能导致 UI 掉帧,影响滚动流畅度。只有那些确实需要在交互时同步更新的任务(如倒计时显示)才适合加入 CommonModes。

5. 子线程 RunLoop 的销毁

子线程的 RunLoop 不会自动销毁。如果使用了线程保活,在不需要该线程时,必须手动停止 RunLoop,否则线程会一直处于运行状态,导致内存泄漏或资源浪费。

©

著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

更多精彩内容,就在简书APP

"小礼物走一走,来简书关注我"

还没有人赞赏,支持一下

相关阅读更多精彩内容

  • RunLoop RunLoop概念 RunLoop理解为运行循环。其本质就是一个do-while,这里的do-wh...

  • 转载自ibireme-深入理解RunLoop看了很多遍,每次都有不同的收获.想要了解Runloop的朋友可以来看看...

  • 二、runloop应用 2.1 NSTimer 前面一直提到Timer Source作为事件源,事实上它的上层对应...

  • iOS Runloop [TOC] 官方文档[developer.apple.com/librar...

  • 本篇主要是对小码哥底层视频学习的总结。方便日后复习。上篇《iOS底层原理总结 - 探寻Runtime本质(四)》:...