RunLoop 参考:深入理解RunLoop
ibireme:《深入理解RunLoop》
Runloop 的概念
首先,让一个线程随时能处理事件,但是并不退出,这样的模型通常称作 Event Loop
,如下:
funcation loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有消息处理时休眠以避免资源浪费,在消息到来时立刻被唤醒。
Runloop
实际上就是一个对象,这个对象管理了其所需要处理的事件和消息,并提供一个入口函数来执行上面的 Event Loop
逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接收消息->等待->处理”的循环中,直到这个循环结束(如返回 quit
消息),函数返回。
iOS/MacOS
提供了两个这样的对象:NSRunLoop
和 CFRunLoopRef
。
-
CFRunLoopRef
是在CoreFoundation
框架内的,它提供了纯C
的API
,所有这些API
都是线程安全的。 -
NSRunLoop
是基于CFRunLoopRef
的封装,提供了面向对象的API
,但这些API
不是线程安全的。
RunLoop 与线程的关系
-
RunLoop
是通过p_thread
管理的。苹果不允许直接创建RunLoop
,它提供了两个自动获取的方法:CFRunLoopGetCurrent()
和CFRunLoopGetMain()
。 - 线程和
RunLoop
是一一对应的,其关系保存在一个全局的Dictionary
里。线程刚创建时是没有RunLoop
的,如果不主动获取,那它就会一直没有。RunLoop
的创建是在第一次获取时,销毁发生在线程结束时。
RunLoop 对外的接口
在 CoreFoundation
里面关于 RunLoop
有五个类:
- CFRunLoopRef
- CFRunLoopSourceRef
- CFRunLoopObserverRef
- CFRunLoopTimerRef
- CFRunLoopModeRef
其中, CFRunLoopModeRef
类没有对外暴露,只是通过 CFRunLoopRef
的接口进行了封装,他们关系如下:
一个 RunLoop
包含若干 Mode
,每个 Mode
又包含若干 Source/Timer/Observer
。每次调用 RunLoop
的主函数时,只能指定其中一个 Mode
,这个 Mode
被称为 CurrentMode
。如果需要切换 Mode
,只能退出 Loop
,再重新指定一个 Mode 进入。这样做主要是为了分割不同组的 Source/Timer/Observer
,让其互不影响。这也是为啥 ScrollView
滑动时,默认 Mode 下计时器停止的原因。
Source/Timer/Observer
被统称为 mode item
,一个 item
可以同时加入多个 mode
。但一个 item
被重复加入同一个 mode
是不会有效果的。如果一个 mode
中一个 item
都没有,则 RunLoop
会直接退出,不进入循环。
CFRunLoopSourceRef
是事件产生的地方。 Source
有两个版本: Source0
和 Source1
。
-
Source0
只包含一个回调(指针),它并不能主动触发事件。使用时需要先调用CFRunLoopSourceSignal(source)
,将这个source
标记为待处理, 然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop
,让其处理事件。 -
Source1
包含了一个mach_port
和一个回调(指针),被用于通过内核和其他线程相互发送消息。这种Source
能主动唤醒RunLoop
的线程。
CFRunLoopTimerRef
CFRunLoopTimerRef
是基于时间的触发器,它和 NSTimer
是 toll-free bridged
的,可以混用。其包含一个时间长度和回调(指针)。当其加入到 RunLoop
时,RunLoop
会注册对应的时间点,当时间点到时,RunLoop
被唤醒执行那个回调。
CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者。 每个 Observer
都包含了一个回调(指针),当 RunLoop
状态发生变化时,观察者就能通过回调接收到这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
RunLoop 的 Mode
CFRunLoopMode
和 CFRunLoop` 的结构如下:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
筛选出比较关键的信息,大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
- 苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,可以用这两个 Mode Name 来操作其对应的 Mode。
- 可以自定义 Mode。RunLoop 内部 Mode 只能增加,不能减少。
RunLoop 内部逻辑
RunLoop 内部的逻辑大致如下:
苹果用 RunLoop 实现的功能
APP启动时,系统默认注册了5个Mode:
- kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
- UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
- kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
可以在 这里 看到更多的苹果内部的 Mode,但那些 Mode 在开发中就很难遇到了。
RunLoop 参考:iOS线下分享《RunLoop》by 孙源@sunnyxx
iOS线下分享《RunLoop》by 孙源@sunnyxx
RunLoop
机制
为什么要有 RunLoop
:
- 使程序一直运行并接受用户输入
- 决定程序在何时应该处理哪些
Event
- 调用解耦(
Message Quene
) - 节省
CPU
时间
-
RunLoop
与线程(Thread
)一一绑定并非说是一个Thread
只能对应一个RunLoop
, 而是对应一个在外层的RunLoop
,RunLoop
可以嵌套使用。 -
1
对n
是通过数组结构实现的。
CFRunLoopTimer
我们常用的 Timer
相关的,都是基于 CFRunLoopTimer
的封装,如:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
或者延迟执行:
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument
afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
或者屏幕刷新频率 CADisplayLink
:
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
CFRunLoopSource
-
Source
是RunLoop
的数据抽象类(Protocol) -
RunLoop
定义了两个Version
的Source
,可以从堆栈信息中查看:-
Source0
: 处理App
内部事件、App
自己负责管理(触发),如UIEvent
,CFSocket
等。 -
Source1
: 由RunLoop
内核管理,Mach Port
驱动,如CFMachPort
、CFMessagePort
(都是官方文档说的。。。) - 如果需要,可以选择一种来实现自己的
Source
。(这事儿知道就行了)
-
CFRunLoopObserver
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
- 向外部报告当前
RunLoop
状态的更改。 - 框架中很多机制都由
CFRunLoopObserver
触发,如CAAnimation
。(其实这个也是猜测,并没有实质文档说明)
Topic
: CFRunLoopObserver
与 Autorelease Pool
关系
UIKit
通过RunLoopObserver
在RunLoop
循环过程中,对Autorelease Pool
进行Pop
和Push
操作,将这次Loop
中产生的Autorelease
对象释放。视频中作者测试,是在两次Sleep
之间。
CFRunLoopMode
-
RunLoop
在同一段时间,只能,并且必须在一种特定的Mode
下Run
。 - 更换
Mode
时,当前Loop
会停止,然后重新启动Loop
。 -
Mode
是iOS App
滑动顺畅的关键。 - 可以定制自己的
Mode
。
-
NSDefaultRunLoopMode
: 默认状态,越是空闲时的状态。 -
UITrackingRunLoopMode
: 滑动时的状态ScrollView
。 -
UIInitializationRunLoopMode
: 私有(不可见,堆栈追踪能看到,其他均为猜测) -
NSRunLoopCommonModes
: 包含NSDefaultRunLoopMode
和UITrackingRunLoopMode
两种状态。:
Topic
: UITrackingRunLoopMode
与 Timer
- 这个方法,是将
Timer
加到默认的NSDefaultRunLoopMode
模式下。
[NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(timeCount)
userInfo:nil
repeats:YES];
- 如果存在
ScrollView
,滑动时,[NSRunLoop currentRunLoop].currentMode
从NSDefaultRunLoopMode
改变为UITrackingRunLoopMode
,此时,Timer
暂停,结束滑动后,Timer
继续。如果想要滑动时Timer
正常运行,则将Timer
添加到NSRunLoopCommonModes
模式下即可,即:
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
Topic
: RunLoopMode
的切换
- 滑动前:
NSDefaultRunLoopMode
- 滑动中:
UITrackingRunLoopMode
- 滑动结束后:
NSDefaultRunLoopMode
RunLoop
与 GCD
视频中这一块也是在讨论,并没有定论
-
GCD
本身与RunLoop
没有关系。 -
GCD
中dispatch
到main queue
的block
被分发到main runloop
中执行,dispatch_after
同理。
Runloop 的等待与唤醒
- 指定用户唤醒的
mach_port
端口。 - 调用
mach_msg
监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在mach_msg_trap
状态。 - 由另一个线程(或另一个进程中的某个线程)向内核发送这个端口的
msg
后,trap
状态被唤醒,Runloop
继续开始干活。