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。
- Source0:处理应用内部事件,如
- CFRunLoopTimerRef:定时器,如 NSTimer、CADisplayLink。
- CFRunLoopObserverRef:观察者,用于监听 RunLoop 的状态变化。
⚙️ RunLoop 的工作流程
RunLoop 的一次完整循环大致如下:
- 进入 (Entry):通知 Observer,RunLoop 即将开始。
- 处理定时器 (BeforeTimers):检查并处理到期的 Timer。
- 处理 Source0 (BeforeSources):处理应用内部的事件源。
- 检查 Source1 并休眠 (BeforeWaiting):如果没有 Source1 事件,RunLoop 会通知 Observer 并进入休眠。
- 被唤醒 (AfterWaiting):当有 Source1 事件(如触摸)到来时,RunLoop 被唤醒,并通知 Observer。
- 处理事件:处理唤醒它的 Source1 事件。
- 退出 (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)
当你调用 setNeedsLayout 或 setNeedsDisplay 时,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本质(四)》:...