核心概念
什么是 RunLoop ?
RunLoop
是 iOS 和 macOS 应用程序框架中的一个核心概念,用于管理线程的事件处理。它可以看作是一个循环,用于持续接收和处理各种事件,如用户输入、定时器、网络事件等。RunLoop 在保持应用程序响应用户交互和系统事件方面起着关键作用。
网络上常见的 source1
具体指什么 ?
其实在苹果的官方文档中是没有这个概念的, 网络上大多数稍有深度的 RunLoop 文章, 基体都会提到 source1 这个概念, source1首次出现已无从考证, 其指代的是基于 Mach 端口的输入源
, 两者指的是同一个东西.
在 macOS 和 iOS 中,很多系统事件是通过 Mach 端口传递的。Mach 是底层内核的一部分,提供了进程间通信(IPC)的机制。基于 Mach 端口的输入源(Source1)用于处理这种通信。
苹果用 RunLoop 实现的功能
结合 AutoreleasePool 实现自动清理
在主线程中, 每次RunLoop循环开始和结束时,系统会自动创建和销毁AutoreleasePool。这就像你在每次做完一顿饭后统一清理脏碗碟:
- RunLoop开始:系统会自动创建一个新的AutoreleasePool,开始处理事件。
- RunLoop循环中:你可能会创建很多临时对象,这些对象会被添加到当前的AutoreleasePool中。
- RunLoop结束:系统会自动释放并销毁这个AutoreleasePool,清理所有在这个循环中创建的临时对象。
为什么需要这种机制?
这种机制保证了在每个事件循环结束时,所有临时对象都能被及时释放,避免内存泄漏。如果没有AutoreleasePool,你需要手动管理所有临时对象的释放,增加了代码复杂性和错误的风险。
事件响应
1. 创建 Mach 端口:
系统会为应用程序创建一个 Mach 端口,用于接收来自内核的事件。这些事件可能包括用户输入事件(如触摸、按键)和系统通知等。
2. 将 Mach 端口添加到 RunLoop:
应用程序会将这个 Mach 端口作为 RunLoop 的一个输入源进行注册 (source1)。这是通过创建一个基于端口的 CFRunLoopSourceRef(Core Foundation Run Loop Source)来实现的。
3. 事件的传递与处理:
-
3.1. 当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个
IOHIDEvent
事件并由 SpringBoard 接收; -
3.2. SpringBoard 将接收的
IOHIDEvent
事件, 通过 mach port 转发给对应的 App 进程; -
3.3. 应用进程中的 Source1 被触发后,调用 __IOHIDEventSystemClientQueueCallback()。这个回调函数进一步调用 _UIApplicationHandleEventQueue() 进行应用内部的事件分发;
-
3.4. _UIApplicationHandleEventQueue() 将 IOHIDEvent 事件转换成 UIEvent 事件进行处理或分发,其中包括触摸事件的处理(如 touchesBegan/Move/End/Cancel 事件)、按钮点击事件、手势识别(UIGestureRecognizer)等;
手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
定时器
在 iOS 中,RunLoop
和定时器(NSTimer
和 CADisplayLink
)有着密切的关系。定时器依赖于 RunLoop
来触发回调函数。
NSTimer 和 RunLoop
工作机制
NSTimer
是一个基于时间间隔的触发器,用于在指定的时间间隔之后向目标对象发送消息。它依赖于 RunLoop
来定期检查和触发, NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。
-
创建定时器:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
-
添加到 RunLoop:
当你使用scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
方法创建定时器时,它会自动添加到当前线程的默认RunLoop
模式中。你也可以手动将NSTimer
添加到RunLoop
中:[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-
RunLoop 处理定时器:
RunLoop
每次循环时会检查定时器的触发时间。如果定时器的触发时间已到或已过,RunLoop
会触发定时器的回调方法(例如timerFired:
)。
RunLoop 模式对定时器的影响
- 默认模式 (
NSDefaultRunLoopMode
):通常用于普通的事件处理。如果NSTimer
被添加到此模式中,当用户进行滚动操作(导致RunLoop
切换到UITrackingRunLoopMode
)时,定时器将暂停。 - 通用模式 (
NSRunLoopCommonModes
):可以确保NSTimer
在不同的RunLoop
模式下都能触发。例如,在滚动视图时,RunLoop
会切换到UITrackingRunLoopMode
,如果定时器添加到通用模式,它仍然会触发。
CADisplayLink 和 RunLoop
CADisplayLink
是一个特殊的定时器,与屏幕刷新率同步,通常用于动画。
-
创建和添加 CADisplayLink:
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkFired:)]; [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
-
屏幕刷新同步:
CADisplayLink
会在屏幕每次刷新时调用其目标对象的选择器方法。屏幕通常每秒刷新60次(即60Hz),因此CADisplayLink
的回调方法也会以相同的频率调用。 -
处理动画:
在回调方法中,可以更新动画状态或执行其他需要在每帧更新的操作。由于它与屏幕刷新率同步,CADisplayLink
非常适合实现平滑的动画。
RunLoop 模式对 CADisplayLink 的影响
与 NSTimer
类似,如果 CADisplayLink
被添加到默认模式,则在滚动视图时可能会暂停。为确保它在滚动期间也能触发,可以将它添加到通用模式:
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
PerformSelecter
在 iOS 中,RunLoop 和 performSelector 方法之间的关系主要体现在线程间通信和延迟执行这两个方面。RunLoop 负责调度 performSelector 方法的执行时机,确保方法在合适的时机被调用。
关于GCD
GCD 的绝大多数实现并不依赖于 RunLoop,它们是两个独立的机制,各自有不同的应用场景和实现方式。
但是, GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async(), 当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
关于网络请求
RunLoop 的实际应用举例
ReactNative创建常驻JS线程
AFNetworking创建后台常驻线程接受回调任务
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
AsyncDisplayKit在主线程的 RunLoop 中添加一个 Observer
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。