文章目录
- 前言
- 一、Runloop的概念
- [二、RunLoop 与线程的关系](#二、RunLoop 与线程的关系)
- 三、Runloop对外的接口
- [四、RunLoop 的 Mode](#四、RunLoop 的 Mode)
- 五、Runloop的内部逻辑
- 六、Runloop应用
- 总结
前言
先前学习了Runtime的内容,现在来学习一下Runloop
一、Runloop的概念
一般来讲线程一次只能执行一个任务,执行完后就会退出,我们现在想实现一个功能:线程一直在处理事件并且不会退出,这就是我们Runloop
的作用,通常的代码逻辑我们用伪代码来表示一下:
bash
int main(void) {
初始化();
while (message != 退出) {
处理事件(message);
message = 获取下一个事件();
}
return 0;
}
这种模型也常常被称作Event Loop
,这种机制在很多地方都用到了,例如windows的事件循环,iOS中的runloop
实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
因此Runloop实际上就是一个对象,只不过这个对象是用来管理需要处理的消息与事件的,并且这个对象提供了一个入口函数 执行Event Loop
,线程执行函数后会一直进行事件循环,直到接收到退出消息
在iOS中提供了两个EventLoop的具体实现:
NSRunLoop
和 CFRunLoopRef
CFRunLoopRef
是在 CoreFoundation
框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全 的。
NSRunLoop
是基于 CFRunLoopRef
的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
二、RunLoop 与线程的关系
我们在先前的多线程里讲过,基本上所有的线程操作的底层都是对pthread_t
的封装
同时回到我们Runloop
与线程的关系,苹果不允许直接创建Runloop
,它只提供了两个自动获取的函数:CFRunLoopGetMain
() 和 CFRunLoopGetCurrent
()。 这两个函数内部的逻辑大概是下面这样:
bash
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里
线程刚创建时并没有runloop
,我们需要主动获取,系统才会自动帮我们创建runloop
并加到字典中
三、Runloop对外的接口
我们在上文说了iOS并不允许我们直接创建Runloop,但其提供了两个函数给我们,CFRunLoopGetCurrent 和 CFRunLoopGetMain
CFRunLoopGetCurrent
函数便是获取当前线程的CFRunLoop
对象,如果不存在的话会则会创建一个。CFRunLoopGetMain
则是获取主线程的CFRunLoop
对象。
同时我们需要明白我们这两个函数只是创建了线程对应的Runloop,但是创建之后的Runloop并没有运行起来,因此需要程序员让runloop运行起来
在研究如何使 run loop
运行起来和 run loop 运行起来后的行为
,需要先了解 run loop 的一些具体结构。
在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
一个Runloop中包含了若干个Mode, CFRunLoopModeRef
类并没有对外暴露,只是通过 CFRunLoopRef
的接口进行了封装,他们的关系如下:
同时通过这张图我们可以看到,每个Mode中游包含了Source/Timer/Observer
的集合
每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode
被称作 CurrentMode
如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer
,让其互不影响。
CFRunLoopSourceRef
CFRunLoopSourceRef
是事件产生的地方。Source有两个版本:Source0 和 Source1。
这里我们首先要明确一个概念,不是所有代码都是通过Runloop处理的,RunLoop主要用于处理异步事件,如用户输入、定时器触发、网络响应等。这些事件通常被封装成事件源,然后由RunLoop在适当的时机调度和处理。
比如NSLog函数就不会使用到Runloop进行事件循环
Source0
Source0 只包含了一个回调(函数指针)
它并不能主动触发事件
使用source0时,需要先调用CFRunLoopSourceSignal(source0)
将 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop)
来唤醒 RunLoop,让其处理这个事件,按钮的点击滑动等都是在此进行处理的
bash
// 假设有一个方法,用于处理按钮点击
- (void)buttonClicked {
// 手动触发RunLoop的Source0
CFRunLoopSourceSignal(source0);
CFRunLoopWakeUp(CFRunLoopGetCurrent()); // 唤醒RunLoop来处理事件
}
// 配置Source0
- (void)setupSource0 {
CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, NULL, NULL, &callout};
source0 = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source0, kCFRunLoopDefaultMode);
}
// Source0的回调函数
void callout(void *info) {
NSLog(@"Source0 event triggered.");
}
Source1
Source1
包含了一个 mach_port 和一个回调(函数指针)
被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
bash
// 配置Source1
- (void)setupSource1 {
CFRunLoopSourceContext1 context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, &perform, NULL};
CFMessagePortRef localPort = CFMessagePortCreateLocal(kCFAllocatorDefault, CFSTR("com.example.app.port"), &callback, &context, false);
CFRunLoopSourceRef source1 = CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, localPort, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source1, kCFRunLoopCommonModes);
}
// Source1的回调函数
CFDataRef callback(CFMessagePortRef local, SInt32 msgid, CFDataRef data, void *info) {
NSLog(@"Received message: %d", msgid);
return NULL;
}
// Source1的事件执行
void perform(void *info) {
NSLog(@"Performing work in response to external event.");
}
使用场景:
• 处理来自其他进程的数据或信号。
• 监听系统级事件或网络事件。
CFRunLoopTimerRef
CFRunLoopTimerRef
是基于时间的触发器,它和 NSTimer
是toll-free bridged
的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop
时,RunLoop
会注册对应的时间点,当时间点到时,RunLoop
会被唤醒以执行那个回调。
CFRunLoopObserverRef
CFRunLoopObserverRef
是 Core Foundation
框架中的一种对象,用于监视和响应 RunLoop
的特定活动。通过 CFRunLoopObserver
,开发者可以在 RunLoop
的不同阶段插入自定义的代码来执行特定的任务
可以观测的时间点有以下几个:
bash
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
};
我们在上面讲的 Source/Timer/Observer
被统称为 mode item,一个item被重复添加到同一个mode时不会多次执行,但是如果一个mode中一个item都没有,runloop会自动退出,不会进出循环
四、RunLoop 的 Mode
刚才在上文讲了mode item,modeitem是被加到mode中的,我们现在讲一下Runloop的Mode
CFRunLoopMode
和 CFRunLoop
的结构大致如下:
bash
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<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
这里有个概念叫CommonModes
,一个Mode可以把自己标记成"Common
"属性(通过将其 ModeName
添加到 RunLoop
的 "commonModes
" 中),每当Runloop的内容发生变化时,RunLoop
都会自动将 _commonModeItems
里的 Source/Observer/Timer
同步到具有 Common
标记的所有Mode
里。
应用场景举例 :
主线程的Runloop
中有两个预置的Mode
kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
。这两个 Mode
都已经被标记为"Common"
属性
应用场景举例:
DefalutMode
是App平时所处的状态,TrackingMode
是当滑动时所处的状态,当我们创建NSTimer
添加到DefalutMode
中,Timer会得到重复回调 ,但是当我们滚动我们的TableView
时,Runloop
会切换Mode
,由DefalutMode
切换为TrackingMode
,此时Timer
会停止同时不会进行回调,也不会影响到滑动的操作
但是如果我们想在滑动的时候NSTimer能够继续运作
- 一种方法就是将Timer分别加入到两个Mode
- 另一种方法就是将
NSTimer
加到最顶层的RunLoop
的commonModeItems
,加入后的ModeItems类型会被Runloop加到具有common属性的Mode中去
,也就是直接将Timer
同时加到defaultMode
与TrackMode
中去
上面既然讲到了RunLoop中的Mode,我们来分析一下iOS中到底有几种Mode:
苹果公开的三种 RunLoop Mode:
NSDefaultRunLoopMode
(kCFRunloopDefaultMode
):默认状态,app通常在这个mode下运行UITrackingRunLoopMode
:界面跟踪mode(例如滑动scrollview时不被其他mode影响)NSRunLoopCommonModes
(kCFRunLoopCommonModes
):是 前两个mode的集合,可以把自定义mode
用CFRunLoopAddCommonMode
函数加入到集合中
还有两种mode,只需做了解即可:
GSEventReceiveRunLoopMode
:接收系统内部mode,通常用不到UIInitializationRunLoopMode
:私有,只在app启动时使用,使用完就不在集合中了
五、Runloop的内部逻辑
来看一张经典的Runloop逻辑图
Runloop代码整理如下,后面会专门讲一下Runloop的源码
bash
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
可以看到Runloop
内部实际就是这样一个函数,其内部一直在进行do-while
循环,当你调用 CFRunLoopRun
() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
六、Runloop应用
事件响应
当一个硬件事件(触屏/锁屏/摇晃/加速)发生之后,首先在触摸时会生成一个IOHIDEvent
事件,之后由mach port
端口转发给App
进程
苹果同时也注册了一个source1来接受系统事件,通过回调函数出发Source0,(所以UIEvent实际上基于Source0)的,调用_UIApplicationHandleEventQueue()
进行应用内部的分发,_UIApplicationHandleEventQueue()
会把 IOHIDEvent
处理并包装成 UIEvent
进行处理或分发
,其实包括点击事件,手势处理等
界面更新
- 当UI发生改变时(
Frame
变化,UIView
/CALayer
结构变化),或手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay
方法之后,就将这个UIView、CALayer
就被标记为待处理 - 苹果注册了一个用来监听
BeforeWaiting
和Exit的Observer,在他的回调函数里会遍历所有待处理的UIView/CALayer
来执行实际的绘制和调整,并更新UI界面。
AutoreleasePool
主线程注册了两个Observers
,他们用来创建与释放AutoreleasePool
-
Observers1
监听Entry
事件: 优先级最高,确保在所有的回调前创建释放池 ,回调内调用_objc_autoreleasePoolPush
()创建自动释放池 -
Observers2
监听BeforeWaiting
和Exit
事件: 优先级最低,保证在所有回调后释放释放池。BeforeWaiting
事件:调用_objc_autoreleasePoolPop
()和_objc_autoreleasePoolPush
()释放旧池并创建新池,Exit事件: 调用_objc_autoreleasePoolPop
(),释放自动释放池
tableView延迟加载图片,保证流畅
我们在快速滑动的图片滑动过的图片会一直加载,但是快速滑动过的图片并不是我们想要的图片,如果进行加载就十分浪费CPU资源,我们现在有一种思路,让我们的tableview滑动时不加载图片
给ImgaeView的加载图片的方法指定只有在DefalutMode下才能加载,滑动时不加载图片,为实现这个功能我们用到了
bash
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName.png"] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
Timer不被ScrollView的滑动影响
我们在上面说了两种方法,一种是将定时器加到trackingMode中,另一种是加到CommonMode集合中,我们来介绍另一种方法
- 用
GCD
创建定时器,GCD
创建的定时器不会受RunLoop
影响
bash
// 获得队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建一个定时器(dispatch_source_t本质还是个OC对象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
// GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
// 比当前时间晚1秒开始执行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
//每隔一秒执行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 设置回调
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);
});
// 启动定时器
dispatch_resume(self.timer);
AFNetworking
在多线程中,线程常常执行完任务就会退出,这意味着如果我们需要反复执行任务例如网络请求,网络监听等,必须要频繁地创建与销毁线程,这样不仅效率低下,而且增加了系统的开销,因此我们希望实现一个常驻线程专门处理这些任务
常驻线程
我们在上面说过,一个RunLoop中如果没有Observer/Timer/Source等items,Runloop会自动退出,因此我们创建一个空的port发送消息给Runloop,以至于Runloop不会退出而是一直常驻
首先创建一个线程属性
其次验证是否我们的点击事件是在loop中执行且线程不销毁
bash
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
[self.thread start];}
- (void)runThread {
NSLog(@"开启子线程:%@", [NSThread currentThread]);
// 子线程的RunLoop创建出来需要手动添加事件输入源和定时器 因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
//下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 测试开始RunLoop
// 未进入循环就会执行该代码
NSLog(@"failed");
}
// 同时在我们自己新建立的这个线程中写一下touchesBegan这个方法测试点击空白处会不会在子线程相应方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(runTest) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)runTest {
NSLog(@"子线程点击空白:%@", [NSThread currentThread]);
}
可以看到点击之后出现如下情况
PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay:
后,其内部会自动创建一个Timer加到Runloop中,当时间到了执行回调,如果当前线程没有Runloop,此方法也会失效
总结
本文介绍玩Runloop,应该懂得:
Runloop事件上就是一个事件循环,也可以当作一个对象,这个对象实际上就是用来处理消息与事件的,其提供了一个入口函数去执行EventLoop
同时Runloop中又包含五种Mode,其中最常用的是CommonMode,DefaultMode,TrackingMode
,Mode中又有ModeItems的集合,如果一个Mode中没有items,那么runloop就会退出,items
是指Timer/Source/Observer
等,Source中又包括Source0与Source1,Source0主要处理点击事件,Source1主要处理线程之间发送消息等操作,但是Source0必须要手动出发Runloop
同时知道了Runloop
的基本逻辑,我们还可以在日常开发中使用它,例如解决定时器不准,实现ImageView
延迟加载,AFNetWorking
中实现常驻线程等功能,同时在iOS的底层实现界面更新以及事件响应都用到了Runloop
同时Runloop十分重要,后面还会分析Runloop的源码