本文主要回答三个问题:
- RunLoop是什么,目的是什么
- 数据结构,生命周期
- 和线程、Timer、GCD、AutoReleasePool的关系。
- 如何使用observer观察RunLoop
1 RunLoop是什么
- RunLoop,是一个消息处理模式。(包括接收、分发、处理)
- 对于main RunLoop来说,他做了一个保活,同时节省资源消耗。
- 主线程需要RunLoop来处理各种中断事件(如用户点击等)
上图只是为了演示方便,RunLoop并非基于自旋锁。
1-1 目的
目的是,使线程只在有工作的时候工作,没有工作的时候休眠。 再高一个层面来看,就是为了cpu利用率。
2 工作流程
- 线程中定义一个RunLoop,以及一些关心的事件源
- 将事件源加入到RunLoop中,进行观察
- 所观察的事件发生后,RunLoop会调用事件处理函数
2-1 输入源有哪些
- 基于端口的输入源 系统内核内置的源 a. 用户输入(点击等UI操作) b. 网络事件(收到数据等)
- 自定义输入源 手动添加的源 a. Timer事件 b. 其他手动加入的源
2.2 Runloop模式
- RunLoop包括输入源、Timer、observers、mode。
- RunLoop同一时刻只能处于一种mode
- 只有与当前mode相关的输入源,事件发生时,才通知observers
3 生命周期
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
};
3-1 唤醒
可以被source0/source1,Timer,dispatch block唤醒
3-2 RunLoop事件处理流程 以及 source0和source1
-
Source0:非基于Port的 Source1:基于Port的,跨进程通信
-
Source0: event事件,只含有回调,需要手动调用RunLoop。(先标记为待处理,然后唤醒RunLoop)
CFRunLoopSourceSignal(source)
CFRunLoopWakeUp(runloop)
- Source1: 包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息,能主动唤醒 RunLoop的线程。
以上是概念性的东西,下面是我测试及推测的东西:
- 就我的理解,RunLoop可以直接接受source1的事件,但更多时候,接受的是source1创建的source0事件(如触摸屏幕的source1事件,会创建UI相关的source0事件)
- RunLoop一次循环,会依次处理Timer、source0、dispatch block中的每一类事件,并不只处理一类。(至少通知是每轮所有通知都会发一遍。)
通过添加一个timer,及observer来看:
- 一次timer触发,会经历图中2-5的所有周期,并最终到6。
- 一次点击触发,会经历图中2-5的周期多次,并最终到6.
- 步骤5,不只是source1,也包括dispatch block和Timer。
补充一个图
4 RunLoop结构、模式
- 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
- 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
- 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
- 这样做主要是为了分隔开不同组的Source/Timer/Observer
系统默认注册了5个Mode:(前两个跟最后一个常用) kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行 UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用 GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到 kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
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 {
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 */
};
4-1 Timer
- NSTimer(CADisplayLink也是加到RunLoop),受RunLoop的Mode影响
- GCD的定时器不受RunLoop的Mode影响
- 推测:RunLoop的常规周期应该是小于60Hz,否则无法满足CADisplayLink的需求
5 RunLoop与线程的关系
子线程,没有执行时没有RunLoop,执行起来不会自动创建RunLoop。
- 一个线程有1个或0个RunLoop
- 延迟创建
- 线程结束时销毁RunLoop。 引申:如果一个线程有RunLoop,不主动销毁线程的情况下,RunLoop是否能保活?有observer不能保活;有port可以保活;有待办的task,暂时保活。
- 私有(mainThread的RunLoop除外)
- 子线程发送网络请求后,如果没有进行保活,就结束并销毁了。 收到并处理响应的并非子线程,虽然响应block的代码和子线程中发送网络请求的代码在一起。
//应用场景:经常在后台进行耗时操作,如:监控联网状态等 不希望线程处理完事件就销毁,保持常驻状态
//开启
- (void)run
{
//addPort:添加端口(就是source) forMode:设置模式
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//启动RunLoop
[[NSRunLoop currentRunLoop] run];
/*
//另外两种启动方式
[NSDate distantFuture]:遥远的未来 这种写法跟上面的run是一个意思
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
不设置模式
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
*/
}
//退出-退出当前线程
[NSThread exit];
6 RunLoop和AutoReleasePool的关系
- 进入RunLoop时,创建AutoReleasePool
- beforeWaiting时,因为所有事都处理完了,所以释放旧池,并创建新池。(这里没有延后创建新池,可能是因为现在最空,先创建出来,因为是高频使用的)
- 线程退出时,也就是RunLoop即将销毁时,释放AutoReleasePool。
7 RunLoop和GCD的关系
GCD的queue就相当于往RunLoop中添加dispatch block任务
- 对于主线程中的task queue,相当于加入到了main RunLoop的dispatch block
- 对于并发队列中的task,我的理解是,
相当于加入到了子线程的任务队列中
- 如果这些并发队列是特定的并发队列,并且是系统在保活,那么是有RunLoop的。
- 如果这些并发队列,系统并没有保活,而task最终也没有明确的使用port,那么应该就不涉及RunLoop
dispatch_after 3秒后交给RunLoop,但是RunLoop什么时候处理,就不一定了。
8 创建RunLoop & 创建RunLoopObserver
//Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
//Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0
, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);
参考文章