RunLoop 实现原理
🔗 原文:RunLoop 实现原理 - 李峰峰博客
1、RunLoop 的概念
我们都知道,APP 运行过程中有一个很重要的线程,就是主线程。但是,一般线程执行完任务后就会退出,而 APP 需要持续运行,所以就需要一个机制使主线程持续运行并随时处理用户事件,在 iOS 里,程序的持续运行就是通过 RunLoop 实现的。
RunLoop 的作用:
-
保持程序持续运行; 程序一启动就会开启一个主线程,主线程开启之后会自动运行一个主线程对应的 RunLoop,RunLoop 保证主线程不会被销毁,也就保证了程序的持续运行;
-
处理 App 中的各种事件 比如:触摸事件,定时器事件等;
-
节省 CPU 资源,提高程序性能 程序运行起来时,当什么操作都没有做的时候,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情,当有事情做的时候 RunLoop 就会立马起来去做事情;
在 iOS 中,NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,CFRunLoopRef 这些 API 都是线程安全的,Apple 在其文档上对 NSRunLoop 非线程安全的提示:
通常不将 RunLoop 类视为线程安全的,并且只能在当前线程的上下文中调用其方法。永远不要尝试调用在不同线程中运行的 RunLoop 对象的方法,因为这样做可能会导致意外的结果。
CFRunLoopRef 是开源的,源码下载地址:opensource.apple.com/tarballs/CF…
为了源码阅读更容易,便于理解 RunLoop 代码关键逻辑,本文所贴出的源码部分有删减,只留下了关键部分。
2、RunLoop 的数据结构
在 CoreFoundation 中,RunLoop 主要有 5 个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
(1) CFRunLoopRef
NSRunLoop 是基于 CFRunLoopRef
封装的,提供了面向对象的 API,接下来看下 NSRunLoop(即 CFRunLoopRef
)的数据结构:
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop
{
// ......
// runloop 所对应线程
pthread_t _pthread;
// 存放所有标记为 common 的 mode
CFMutableSetRef _commonModes;
// 存放 common mode item 的集合(source、timer、observer)
CFMutableSetRef _commonModeItems;
// 当前所在 mode
CFRunLoopModeRef _currentMode;
// 存放 mode 的集合
CFMutableSetRef _modes;
// ......
};
根据以上源码可知,RunLoop 也是一个结构体,即 __CFRunLoop
,并且可以看到其中几个关键的成员变量:
-
_commonModes RunLoop 的内容发生变化时,RunLoop 会自动将
_commonModeItems
里的 Source/Observer/Timer 同步到_commonModes
中所有Mode
里。主线程的 RunLoop 中kCFRunLoopDefaultMode
和UITrackingRunLoopMode
都已经被标记为“Common”。 我们可以通过下面方法把一个Mode
标记为 “Common”:CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
-
_commonModeItems 添加 mode item 的时候,如果
modeName
传入NSRunLoopCommonModes
,则该 mode item 会被保存到 RunLoop 的_commonModeItems
中,例如:[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-
_currentMode runloop 当前所在
mode
-
_modes 存放
mode
的集合
也就是说,RunLoop 可以有多个 mode(CFRunLoopModeRef) 对象,但是同一时间只能运行某一种特定的 Mode。
CFRunLoop 对外暴露的管理 Mode 接口只有下面 2 个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop 会自动创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。
(2) CFRunLoopModeRef
CFRunLoopModeRef
其实是指向 __CFRunLoopMode
结构体的指针,其源码如下:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode
{
// ...
// mode 的名称
CFStringRef _name;
// mode 是否停止
Boolean _stopped;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
// ...
};
从以上源码可知,每个 mode 对象中,可以存储多个 source、observer、timer(source/observer/timer 被统称为 mode item)。
系统默认注册了 5 个 Mode:
-
kCFRunLoopDefaultMode App的默认 Mode,通常主线程是在这个 Mode 下运行的,是大多数操作使用的模式。一般情况下,使用此模式来启动运行循环并配置输入源。
-
UIInitializationRunLoopMode 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
-
UITrackingRunLoopMode 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
-
GSEventReceiveRunLoopMode 接受系统事件的内部 Mode。
-
kCFRunLoopCommonModes 这是一个占位用的 Mode,作为标记
kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
用,并不是一种真正的 Mode
(3) CFRunLoopSourceRef
CFRunLoopSourceRef
是事件源(输入源)。其分为 source0
和 source1
:
-
source0 非基于 port 的,接收点击事件,触摸事件等 APP 内部事件,也就是用户触发的事件。这种 source 是不能主动唤醒 RunLoop 的。 使用时,需要先调用 :
CFRunLoopSourceSignal(source)
将这个 Source 标记为待处理,然后再调用:CFRunLoopWakeUp(runloop)
来主动唤醒 RunLoop,让其处理这个事件。 -
source1 基于 Port 的,能主动唤醒 RunLoop,通过内核和其他线程通信,接收分发系统事件;触摸硬件,通过 Source1 接收和分发系统事件到 Source0 处理。
关于 Port 内容后文会进行总结。
CFRunLoopSourceRef
源码如下:
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
struct __CFRunLoopSource
{
// ...
// source 的优先级,值为小,优先级越高
CFIndex _order; /* immutable */
// runloop 集合
CFMutableBagRef _runLoops;
// 联合体,表示 source 是 source0 还是 source1
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
// ...
};
(4) 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
};
CFRunLoopObserverRef 源码如下:
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver
{
// ...
// observer 对应的 runloop
CFRunLoopRef _runLoop;
// observer 观察了多少个 runloop
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
// observer 优先级
CFIndex _order; /* immutable */
// observer 回调函数
CFRunLoopObserverCallBack _callout; /* immutable */
// ...
};
typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
例如,监听 RunLoop 的状态:
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
});
// 把 observer 添加到 RunLoop 中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
(5) CFRunLoopTimerRef
CFRunLoopTimerRef
是基于时间的触发器,我们常用的 NSTimer
其实就是 CFRunLoopTimerRef
,他们之间是 toll-free bridged 的,可以相互转换。其包含一个时间长度和一个回调(函数指针)。
当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行那个回调。
总结:
一个 RunLoop 中,只能对应一个线程,但是可以包含多个 Mode,每个 mode,可以包含多个 source、observer、timer,其关系如下:
- RunLoop 启动时只能选择其中一个 Mode,作为 currentMode。
- 如果需要切换 Mode,只能退出当前 Loop,再重新选择一个 Mode 进入。
- 不同 Mode 的 Source0/Source1/Timer/Observer 能分隔开来,互不影响。
- 如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出。
3、RunLoop 的执行流程
当 APP 没有任何任务的时候,RunLoop 会进入休眠,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情。当下次有任务的时候,例如用户点击了屏幕,RunLoop 就会结束休眠开始处理用户的点击事件。所以,为了看到 RunLoop 执行流程,可以在点击事件里加个断点,查看 RunLoop 相关的方法调用栈:
根据上图发现,分析 RunLoop 执行流程,可以从 CFRunLoopRunSpecific
、__CFRunLoopRun
函数入手,而对 CFRunLoopRunSpecific
函数的调用,可以在源码中找到,是在 CFRunLoopRun
函数中,源码如下:
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
由以上源码可知:
- 默认底层是通过
CFRunLoopRun
开启 RunLoop 的,并且超时时间设置的非常大:1.0e10,可以理解为不超时。 - 我们也可以通过
CFRunLoopRunInMode
函数设置自定义启动方式,可以自定义超时时间、mode。
然后进入 CFRunLoopRunSpecific
函数,这是 RunLoop 的核心逻辑:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
/// __CFRunLoopRun中 具体要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
/// 11. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 4. 处理block
__CFRunLoopDoBlocks(rl, rlm);
// 5. 处理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
// 如果处理Source0的结果是true
if (sourceHandledThisLoop) {
// 再次处理block
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 6. 如果有Source1 (基于port) 处于ready状态,直接处理这个Source1然后跳转去处理消息。
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
// 如果有Source1, 就跳转到handle_msg
goto handle_msg;
}
// 7. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// 调用mach_msg等待接受mach_port的消息。线程将进入休眠, 等待别的消息来唤醒当前线程:
// 一个基于 port 的Source 的事件。
// 一个 Timer 到时间了
// RunLoop 自身的超时时间到了
// 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
__CFRunLoopUnsetSleeping(rl);
// 8. 通知Observers: 结束休眠, RunLoop的线程刚刚被唤醒了
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 收到消息,处理消息。
handle_msg:;
if (/* 被timer唤醒 */) {
// 01. 处理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
} else if (/* 被gcd唤醒 */) {
// 02. 处理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Source1唤醒
// 处理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
}
// 9. 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 10. 设置返回值, 根据不同的结果, 处理不同操作
if (sourceHandledThisLoop && stopAfterHandle) {
// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (0 == retVal);
return retVal;
}
由以上源码可知,RunLoop 内部是一个 do-while 循环;当调用 CFRunLoopRun()
时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
RunLoop 执行流程可用下面这张图概括:
通过上面的执行流程可以发现,RunLoop 处理了很多次 Block,即调用了很多次
__CFRunLoopDoBlocks
,那这里处理的 Block 到底是什么 Block 呢?
前面提到了 __CFRunLoop
结构体中的一些常见成员,其实还有两个和 Block 相关的成员:
struct __CFRunLoop {
// ...
struct _block_item *_blocks_head; // 存放 CFRunLoopPerformBlock 函数添加的 Block 的双向链表的头指针
struct _block_item *_blocks_tail; // 存放 CFRunLoopPerformBlock 函数添加的 Block 的双向链表的尾指针
// ...
};
_blocks_head
和 _blocks_tail
就是用于存放 CFRunLoopPerformBlock
函数添加的 Block 的,可见 RunLoop 是将添加的 Block 任务保存在双向链表中的。
我们可以通过 CFRunLoopPerformBlock
将一个 Block 任务加入到 RunLoop:
void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void(block)( void));
可以看出添加 Block 任务的时候,是绑定到某个 runloop mode 的。调用上面的 api 之后,runloop 在执行的时候,会通过如下 API 执行对应 mode 中所有的 block:
__CFRunLoopDoBlocks(rl, rlm);
需要注意的是,CFRunLoopPerformBlock
不会主动唤醒 RunLoop,添加完 Block 之后可以使用 CFRunLoopWakeUp
来主动唤醒 RunLoop。
4、RunLoop 与线程的关系
CFRunLoop
是基于 pthread
来管理线程的,苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
。 这两个函数内部的逻辑大致如下:
// 获得当前线程的 RunLoop 对象,内部调用 _CFRunLoopGet0 函数
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// 查看_CFRunLoopGet0方法
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 如果为空则t设置为主线程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
// 如果不存在 RunLoop,则创建
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 根据传入的主线程获取主线程对应的 RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key 和 RunLoop-Value 保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 从字典里面取 RunLoop:将线程作为 key 从字典里获取 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果 RunLoop 为空,则创建一个新的 RunLoop,所以 RunLoop 会在第一次获取的时候创建
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 创建好之后,以线程为 key,RunLoop 为 value,一对一存储在字典中,下次获取的时候,则直接返回字典内的 RunLoop
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// do not release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
// 线程结束是销毁 loop
CFRelease(newLoop);
}
// 如果传入线程和当前线程相同
if (pthread_equal(t, pthread_self())) {
// 注册一个回调,当线程销毁时,顺便也销毁对应的 RunLoop
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
通过源码分析可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个 Dictionary 字典里。
所以我们创建子线程 RunLoop 时,只需在子线程中获取当前线程的 RunLoop 对象即可 [NSRunLoop currentRunLoop]
。如果不获取,那子线程就不会创建与之相关联的 RunLoop,并且只能在一个线程的内部获取其 RunLoop。
当通过调用 [NSRunLoop currentRunLoop]
方法获取 RunLoop 时,会先看一下字典里有没有子线程对应的 RunLoop,如果有则直接返回 RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。当线程结束时,RunLoop 会被销毁。
Runloop 与线程的关系总结:
- 每条线程都有唯一的一个与之对应的 RunLoop 对象;
- RunLoop 保存在一个全局的 Dictionary 里,线程作为 key,RunLoop 作为 value
- 调用
[NSRunLoop currentRunLoop]
方法获取 RunLoop 时,会先看一下字典里有没有子线程对应的 RunLoop,如果有则直接返回 RunLoop,如果没有则会创建一个,并将对应关系保存到字典里。 - 主线程的 RunLoop 已经自动创建好了,子线程的 RunLoop 需要主动创建;
- RunLoop 在第一次获取时创建,在线程结束时销毁;
5、Runloop 的启动与退出
(1) 创建 Runloop
无法直接创建 RunLoop,但是 RunLoop 在第一次获取时自动创建,获取 RunLoop:
Foundation
// 获得当前线程的 RunLoop 对象
[NSRunLoop currentRunLoop];
// 获得主线程的 RunLoop 对象
[NSRunLoop mainRunLoop];
Core Foundation
// 获得当前线程的 RunLoop 对象
CFRunLoopGetCurrent();
// 获得主线程的 RunLoop 对象
CFRunLoopGetMain();
(2) 启动 Runloop
Apple 把 Runloop 启动方式分成了三种:
- 无条件地(Unconditionally)
- 有时间限制(With a set time limit)
- 指定 Mode(In a particular mode)
这三种方式分别对应下面三个方法:
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
- 第 1 种方式,本质就是在
NSDefaultRunLoopMode
模式下无限循环调用runMode:beforeDate:
方法,在此期间会处理来自输入源的数据; - 第 2 种方式,本质也是在
NSDefaultRunLoopMode
模式下无限循环调用runMode:beforeDate:
方法,区别在于它达到指定的超时时间后就不会再调用,在此期间会处理来自输入源的数据。 - 第 3 种方式,Runloop 只会运行一次,达到指定超时时间或者第一个 input source 被处理,则 Runloop 就会退出,这个方法会阻塞当前线程,直到返回结果(YES:输入源被处理或者达到指定的超时值,NO:没有启动成功)。
(3) 退出 Runloop
相较于 Runloop 的启动,它的退出就比较简单了,只有两种方法:
- 设置超时时间
- 手动结束
针对前面提到的第 2、3 中启动方式,可以直接设置超时时间控制退出。如果想要手动退出,可以使用下面函数,其参数就是 Runloop 对象:
void CFRunLoopStop(CFRunLoopRef rl)
但是 Apple 文档中在介绍利用 CFRunLoopStop()
手动退出时提到:
The difference is that you can use this technique on run loops you started unconditionally.
这里的解释非常容易产生误会,如果在阅读时没有注意到 exit 和 terminate 的微小差异就很容易掉进坑里,因为在 run 方法的文档中还有这句话:
If you want the run loop to terminate, you shouldn't use this method
也就是说,前面三种 Runloop 启动方式,对应退出方式如下:
-
run 无法退出
-
runUntilDate: 只能通过设置超时时间进行退出
-
runMode:beforeDate: 可以通过设置超时时间或者使用
CFRunLoopStop
方法来退出
CFRunLoopStop()
函数只会结束当前的 runMode:beforeDate:
调用,而不会结束后续的调用,这也就是为什么 Runloop 的文档中说 CFRunLoopStop()
可以 exit(退出) 一个 Runloop,而在 run 等方法的文档中又说这样会导致 Runloop 无法 terminate(终结)。
如果既让 Runloop 长时间运行,又要在必要时刻手动退出 Runloop,Apple 官方文档提供了推荐方式:
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
在对应线程中通过如下逻辑退出 Runloop:
shouldKeepRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
6、RunLoop 的底层实现
(1) RunLoop 与 mach port
Apple 将 iOS 系统大致划分为下面 4 个层次:
Darwin 的架构如下:
Darwin 是 macOS 和 iOS 操作环境的操作系统部分,Darwin 是一种类 Unix 操作系统(即 Unix 衍生出的系统,在一定程度上继承了原始 Unix 特性),Darwin 的内核是 XNU,XNU 是 Apple 开发的用于 macOS、iOS、tvOS、watchOS 操作系统的内核,XNU 是 X is Not Unix 的缩写。它是一个宏内核 BSD 与微内核 Mach 混合内核,以期将两者的特性兼收并蓄,同时拥有两种内核的优点。
关于 iOS 系统架构相关更多内容,可以看下我的这篇博客:《深入解析 iOS 系统架构》
Mach: Mach 是一个由卡内基梅隆大学开发的计算机操作系统微内核,Mach 核心之上可平行运行多个操作系统,XNU 内核以一个被深度定制的 Mach 内核作为基础。Mach 提供了诸如处理器调度、IPC (进程间通信)等少量且不可或缺的基础 API。在 Mach 中,所有东西都是“对象”,进程(在 Mach 中称为任务)、线程和虚拟内存都是对象。但是,在 Mach 架构中,对象间不能相互调用,对象间通信只能通过消息传递。“消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。
BSD: XNU 中的 BSD 代码来自 FreeBSD 内核,FreeBSD 是一种开放源代码的类 Unix 的操作系统,基于 BSD Unix 的源代码派生发展而来。BSD 层确保了 Darwin 系统的 UNIX 特性,真正的内核是 Mach,但是对外部隐藏。BSD 提供了更高层次的抽象 API,例如:基于 Mach 的任务之上的 Unix 进程模型、文件系统、网络协议栈等相关 API。
I/O Kit: I/O Kit 为设备驱动提供了一个面向对象(C++)的一个框架,框架提供每种设备驱动需要的常见特性,以使驱动程序可以用更少的时间和代码完成。
用户态与内核态: 内核控制着操作系统最核心的部分,为了防止应用程序崩溃而导致的内核崩溃,内核与应用程序之间需要进行严格的分离。基于软件的分离会产生巨大的开销,因此现代的操作系统都是依靠硬件来分离。分离的结果就是用户态与内核态。
用户态和内核态的切换有两种类型:
- 自愿转换:比如系统调用;
- 非自愿转换:当发生异常、中断或处理器陷阱的时候,代码的执行会被挂起,并且保留发生错误时候的完整状态。控制权被转交给预定义的内核态错误处理程序或中断服务程序。
在 XNU 中,系统调用有四种类别:
- BSD 系统调用
- Mach 陷阱
- 机器相关调用
- 诊断调用
Mach 消息的发送和接收都是通过同一个 API 函数 mach_msg()
进行的,这个函数在用户态和内核态都有实现。mach_msg()
函数调用了一个 Mach 陷阱(trap),在用户态调用 mach_msg_trap()
会引发陷阱机制,切换到内核态,在内核态中,内核实现的 mach_msg()
会完成实际的工作,如下图:
RunLoop 的核心就是基于 mach port 的,其进入休眠时调用的函数是
mach_msg()
,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。
前面提到的 source1
就是基于 mach port 的,它用来接收系统事件。当对应系统事件发生后(例如用户点击了屏幕),最终会通过 mach port 将事件转发给需要的 App 进程。随后苹果注册的那个 source1
就会触发回调,RunLoop 被唤醒,APP 开始处理对应事件。
(2) RunLoop 输入源
Runloop 作为线程的入口用来响应传入事件,Runloop 从两种不同类型的源接收事件:
-
输入源(Input Source) 用于传输异步事件,通常是来自另一个线程或者其他程序的消息。输入源将异步事件传递给相应的处理程序,并调用 runUntilDate: 方法(在线程的关联 NSRunLoop 对象上调用)退出。
-
定时器源(Timer Source) 提供同步事件,预定的时间或者固定的时间间隔重复执行,计时器源将事件传递给其处理程序,但不会导致 Runloop 退出。
输入源(Input Source) 创建输入源时,可以将其分配给 Runloop 的一种或多种 mode。一般情况下应该在默认模式下运行 Runloop,但也可以指定自定义 mode。如果输入源不在当前监视的 mode 下,则它生成的任何事件都将保留,直到 Runloop 以正确的 mode 运行,输入源主要有:基于的端口的输入源、自定义输入源、Perform Selector 源。
基于的端口的输入源(Port-based Source)
监听应用程序的 Mach 端口,由内核自动发出信号,对应源码中的 source1
。
Cocoa 和 Core Foundation 都提供了创建基于的端口输入源相关的对象和函数,如果使用 Cocoa 提供的相关方法,不需要直接创建输入源,可以使用 NSPort
相关的方法来创建一个 Port
对象,并将该对象添加到 Runloop 中,该 Port 对象会负责创建和配置输入源。使用 Core Foundation 函数实现稍微复杂些,我们需要手动的创建 Port 和它的 Runloop 源。使用 CFMachPortRef
, CFMessagePortRef
, 或者 CFSocketRef
函数来创建适当地对象。
例如:
- (void)testsource1 {
// 声明两个端口
NSPort *mainPort = [NSMachPort port];
NSPort *threadPort = [NSMachPort port];
// 设置线程的端口的代理回调为自己
threadPort.delegate = self;
// 给主线程 Runloop 加一个端口
[[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 子线程
// 给子线程添加一个 Port,并运行子线程中的 Runloop
[[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
// 2 秒后,从主线程向子线程发送一条消息
NSString *s1 = @"hello";
NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
// 发送一条消息
// 参数:date(发送时间)、msgid(消息标识)、components(发送消息附带参数)、reserved(预留参数,暂时用不到)
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
});
}
// 这个 NSMachPort 收到消息的回调,注意这个参数,可以先给一个id
- (void)handlePortMessage:(id)message
{
NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);
NSArray *array = [message valueForKeyPath:@"components"];
NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);
}
打印结果:
RunLoopTest[10368:5612468] 收到消息了,线程为:<NSThread: 0x6000015acf80>{number = 6, name = (null)}
RunLoopTest[10368:5612468] hello
自定义输入源(Custom input Source)
监听自定义事件源,必须从另一个线程手动发信号通知自定义源,对应源码中的 source0
。
可以使用 CoreFoundation
中 CFRunLoopSourceRef
相关的函数来创建自定义输入源,可以使用多个回调函数配置自定义输入源,CoreFoundation 在必要时候调用这些函数来配置 source
,处理传入的事件,并在从 Runloop 中移除 source
时将其移除。
除此之外,还需要定义事件的传递机制,这部分是运行在单独的线程上,负责向输入源提供数据并在适当的时候发出信号,事件的传递机制可自行定义。
例如:
@implementation ViewController{
CFRunLoopRef _runLoopRef;
CFRunLoopSourceRef _source;
CFRunLoopSourceContext _source_context;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self testsource0];
}
- (void)testsource0 {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"starting thread.......");
_runLoopRef = CFRunLoopGetCurrent();
// 初始化_source_context。
bzero(&_source_context, sizeof(_source_context));
// 这里创建了一个基于事件的源,绑定了一个函数
_source_context.perform = fire;
//参数
_source_context.info = "hello";
// 创建一个source
_source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
// 将 source 添加到当前 RunLoop 中去
CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);
// 开启 RunLoop 第三个参数设置为 YES,执行完一次事件后返回
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 9999999, YES);
NSLog(@"end thread.......");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (CFRunLoopIsWaiting(_runLoopRef)) {
NSLog(@"RunLoop 正在等待事件输入");
// 添加输入事件
CFRunLoopSourceSignal(_source);
// 唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
CFRunLoopWakeUp(_runLoopRef);
}else {
NSLog(@"RunLoop 正在处理事件");
// 添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
CFRunLoopSourceSignal(_source);
}
});
}
static void fire(void* info) {
NSLog(@"我现在正在处理后台任务");
printf("%s",info);
}
@end
打印结果:
RunLoopTest[10630:5649707] starting thread.......
RunLoopTest[10630:5649457] RunLoop 正在等待事件输入
RunLoopTest[10630:5649707] 我现在正在处理后台任务
RunLoopTest[10630:5649707] end thread.......
Perform Selector 源(Cocoa Perform Selector Source) 除了基于端口的源外,Cocoa 还定义了一个自定义输入源,允许在任何线程上 Perfrom Selector。与基于端口的源一样,Perfrom Selector 请求在目标线程上序列化,缓解了在一个线程上运行多个方法时可能出现的许多同步问题。与基于端口的源不同的是,Perform Selector 源在 Perfrom Selector 后会从 Runloop 中删除自己。
在另一个线程上 Perfrom Selector 时,目标线程必须具有活动的 Runloop。这意味对于我们创建的子线程,需要显式创建 Runloop。由于主线程的 Runloop 是自动创建的,所以可以在 applicationDidFinishLaunching:
方法后随时 Perfrom Selector。Runloop 每次循环时,Runloop 都会处理所有的 Perfrom Selector 调用,而不是在每次循环时都只处理一个。
在其他线程上 Perfrom Selector 的相关方法:
// 在主线程的下一个 Runloop 周期中,执行指定的选择器。这些方法允许您选择阻塞当前线程,直到执行选择器结束。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
// 对拥有 NSThread 对象的任何线程执行指定的选择器。这些方法允许您选择阻塞当前线程,直到执行选择器结束。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
// 在下一个 Runloop 周期和指定时长延迟(可选)后,在当前线程上执行指定的选择器。由于它要等到下一个 Runloop 周期来执行选择器,所以这些方法提供了一个来自当前执行代码的自动最小延迟,多个选择器时会按照顺序依次执行。
// performSelector:afterDelay: 方法内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
// 取消使用 performSelector:withObject:afterDelay: 或 performSelector:withObject:afterDelay:inModes: 发送的到当前线程的消息
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
计时器源(Timer Source) 定时器源在预定时间内同步地将事件传递给线程,定时器可以让线程在对应时刻通知自己执行一些事务,尽管定时器是基于时间的通知方式,但是并不是真的时间机制。就像输入源,定时器在 Runloop 中也是和特定 Mode 相关联的。如果定时器没有处在 Runloop 正在监视的 Mode 中的话,该定时器是不会触发的。必须要等到 Runloop 在定时器支持的 Mode 中运行时,该定时器才会正常运行。如果定时器被触发时机正好是在 Runloop 执行任务中,那么这个定时器源的相关事件只有在 Runloop 下一次运行循环时才能被执行。如果 Runloop 停止运行,那么该定时器源的事件将永远没办法执行。
反复执行的定时器会根据它的触发时间自动配置,并不是真实的触发时间。例如,一个定时器设置的是每 5 秒触发一次,在真实时间上可能是有点延迟的,如果真实时间的延迟大于定时器触发时间的话,那么这次触发时机将被错过。
创建定时器源,可以使用下面两个方法:
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:
上面这两个方法创建了定时器并添加到当前线程的默认 mode (NSDefaultRunLoopMode
)中,但是也可以通过 NSRunLoop
的下面的实例方法来将 NSTimer
对象添加到其他 mode 中:
addTimer:forMode:
例如,下面两种实现方式效果是一样的:
/// 分开处理,我们可以通过更多的自定义方式来处理timer,比如添加到不同的NSDefaultRunLoopMode。
NSDate *futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer *myTimer = [[NSTimer alloc] initWithFireDate:futureDate interval:0.1 target:self selector:@selector(timedothing:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];
/// 将创建和调度同时进行
[NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(timedothing:) userInfo:nil repeats:YES];
7、RunLoop 目前的应用
(1) AutoreleasePool
- RunLoop 的进入的时候会调用
objc_autoreleasePoolPush()
创建新的自动释放池。 - RunLoop 的进入休眠的时候会调用
objc_autoreleasePoolPop()
和objc_autoreleasePoolPush()
销毁自动释放池、创建一个新的自动释放池。 - RunLoop 即将退出时会调用
objc_autoreleasePoolPop()
释放自动自动释放池内对象。
(2) 事件响应
苹果注册了一个 Source1
(基于 mach port 的) 用来接收系统事件,当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent
事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程。随后苹果注册的那个 Source1
就会触发回调,并调用 _UIApplicationHandleEventQueue()
进行应用内部的分发。
_UIApplicationHandleEventQueue()
会把 IOHIDEvent
处理并包装成 UIEvent
进行处理或分发,其中包括识别 UIGesture
/处理屏幕旋转/发送给 UIWindow
等。通常事件比如 UIButton
点击、touchesBegin
/Move
/End
/Cancel
事件都是在这个回调中完成的。
(3) 手势识别
当上面的 _UIApplicationHandleEventQueue()
识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin
/Move
/End
系列回调打断。随后系统将对应的 UIGestureRecognizer
标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting
(Loop 即将进入休眠) 事件,这个 Observe r的回调函数是 _UIGestureRecognizerUpdateObserver()
,其内部会获取所有刚被标记为待处理的 GestureRecognizer
,并执行 GestureRecognizer
的回调。
当有 UIGestureRecognizer
的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
(4) 界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView
/CALayer
的层次时,或者手动调用了 UIView/CALayer
的 setNeedsLayout
/setNeedsDisplay
方法后,这个 UIView
/CALayer
就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting
(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
。这个函数里会遍历所有待处理的 UIView
/CAlayer
以执行实际的绘制和调整,并更新 UI 界面。
这个函数内部的调用栈大概是这样的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
(5) 定时器
NSTimer
其实就是 CFRunLoopTimerRef
,他们之间是 toll-free bridged 的。一个 NSTimer
注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
(6) PerformSelector
当调用 NSObject
的 performSelecter:afterDelay:
后,实际上其内部会创建一个 Timer
并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread:
时,实际上其会创建一个 Timer
加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
performSelector:withObject:
只是发消息,不会有 Timer ,所以不会有上面的问题,在子线程调用,不需要开启 Runloop
(7) 关于 GCD
根据前面 RunLoop 的执行流程可以知道,GCD 也是可以唤醒 RunLoop 的,GCD 由子线程返回到 主线程,只有在这种情况下才会触发 RunLoop,会触发 RunLoop 的 Source 1
事件:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"main queue");
});
当调用 dispatch_async(dispatch_get_main_queue(), block)
时,libDispatch
会向主线程的 RunLoop 发送消息,RunLoop 会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()
里执行这个 block。但这个逻辑仅限于 dispatch
到主线程,dispatch
到其他线程仍然是由 libDispatch
处理的。
(8) 关于网络请求
iOS 中,关于网络请求的接口有如下几层: CFSocket
是最底层的接口,只负责 socket 通信。
-
CFNetwork
是基于CFSocket
等接口的上层封装,ASIHttpRequest
工作于这一层。 -
NSURLConnection
是基于CFNetwork
的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
+ NSURLSession
是 iOS7 中新增的接口,表面上是和 NSURLConnection
并列的,但底层仍然用到了 NSURLConnection
的部分功能 (比如 com.apple.NSURLConnectionLoader
线程),AFNetworking 2 和 Alamofire 工作于这一层。
下面主要介绍下 NSURLConnection
的工作过程。
通常使用 NSURLConnection
时,你会传入一个 Delegate
,当调用了 [connection start]
后,这个 Delegate
就会不停收到事件回调。实际上,start
这个函数的内部会会获取 CurrentRunLoop
,然后在其中的 DefaultMode
添加了 4 个 Source0
(即需要手动触发的 Source)。其中 CFMultiplexerSource
是负责各种 Delegate
回调的,CFHTTPCookieStorage
是处理各种 Cookie
的。
当开始网络传输时,我们可以看到 NSURLConnection
创建了两个新线程:com.apple.CFSocket.private
和 com.apple.NSURLConnectionLoader
。其中 CFSocket
线程是处理底层 socket 连接的。NSURLConnectionLoader
这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0
通知到上层的 Delegate
。
NSURLConnectionLoader
中的 RunLoop 接收来自底层 CFSocket
的 Source1
通知。当收到通知后,在合适的时机向 Delegate
线程 RunLoop 发送 CFMultiplexerSource
等 Source0
通知,同时唤醒 Delegate
线程的 RunLoop 来让其处理这些通知。接收到 CFMultiplexerSource
通知后,Delegate 线程的 RunLoop 执行对应 Delegate
回调。
8、Runloop 开发中的使用场景
唯一需要显式运行 Runloop 是在创建子线程时。主线程的 Runloop 已自动创建并运行。对于子线程,需要自行判断是否需要 Runloop,如果需要,则开发者自行创建。例如,下列操作需要启动 Runloop:
- 使用端口或自定义输入源与其他线程通信。
- 在子线程上使用计时器。
- 调用 performSelector… 相关方法。
- 线程保活,以执行周期性任务。
以下是在实际开发是,Runloop 的一些使用场景:
(1) 线程保活
当子线程中的任务执行完毕后,线程就被会被立刻销毁。如果 APP 中需要经常在子线程中执行任务,频繁的创建和销毁线程,会造成资源的浪费,这时候我们就可以使用 Runloop 来让该线程长时间存活而不被销毁,实现如下:
KeepAliveThread.h
typedef void (^KeepAliveThreadTask)(void);
@interface KeepAliveThread : NSObject
// 在子线程执行任务
- (void)executeTask:(KeepAliveThreadTask)task;
// 结束线程
- (void)stop;
@end
KeepAliveThread.m
@interface KeepAliveThread()
@property (nonatomic, strong) NSThread *thread;
@property (nonatomic, assign) BOOL shouldKeepRunning;
@end
@implementation KeepAliveThread
#pragma mark - Public methods
- (instancetype)init {
if (self = [super init]) {
self.shouldKeepRunning = YES;
__weak typeof(self) weakSelf = self;
self.thread = [[NSThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && weakSelf.shouldKeepRunning) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
[self.thread start];
}
return self;
}
- (void)executeTask:(KeepAliveThreadTask)task {
if (!self.thread || !task) return;
[self performSelector:@selector(p_executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}
- (void)stop {
if (!self.thread) return;
[self performSelector:@selector(p_stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)dealloc {
[self stop];
}
#pragma mark - Private methods
- (void)p_stop {
self.shouldKeepRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
}
- (void)p_executeTask:(KeepAliveThreadTask)task {
task();
}
@end
(2) 保证 Timer 正常运行
创建 Timer 有下面两种方式,两种实现方式是等价的:
// 方式 1
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];
// 方式 2
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
当滑动 UIScrollView
时,主线程的 RunLoop 会切换到 UITrackingRunLoopMode
这个 Mode,执行的也是 UITrackingRunLoopMode
下的任务(Mode 中的 item),而 Timer 是添加在 NSDefaultRunLoopMode
下的,所以 Timer 任务并不会执行,只有当 UITrackingRunLoopMode
的任务执行完毕,Runloop 切换到 NSDefaultRunLoopMode
后,才会继续执行 Timer。解决方法很简单,我们只需要在添加 Timer 时,将 mode 设置为 NSRunLoopCommonModes
即可:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[timer fire];
如果是在子线程中使用 Timer,由于子线程的 Runloop 并不会自动创建,所以必须在子线程中创建并启动 Runloop,否则 Timer 无法正常运行,创建并启动 Runloop 方法:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
由于子线程中不会涉及到 UI 更新,所以无需再主动将 Timer 添加到 NSRunLoopCommonModes
下。
(3) 利用 Runloop 优化 UITableView 加载图片时滑动卡顿问题
UITableView
滚动时,主线程的 Runloop 会切换到 UITrackingRunLoopMode
这个 Mode,我们可以在 NSDefaultRunLoopMode
中设置图片,避免一边滑动一边设置 image 引起的卡顿问题:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = ...
// ......
// 在 NSDefaultRunLoopMode 下设置图片
[self performSelector:@selector(p_loadImgeWithIndexPath:)
withObject:indexPath
afterDelay:0.0
inModes:@[NSDefaultRunLoopMode]];
// ......
return cell;
}
- (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
// 子线程下载图片
[ImageDownload loadImageWithUrl:@"xxxx" success:^(UIImage *image) {
// 回到主线程刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = image;
});
}];
}
(4) 利用 Runloop 监控卡顿
根据 Runloop 的执行流程可以发现,Runloop 对我们业务逻辑的处理时间在两个阶段:
-
kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
之间 -
kCFRunLoopAfterWaiting
之后
所以,如果主线程 Runloop 处在 kCFRunLoopBeforeSources
时间过长,也就是迟迟无法将任务处理完成,顺利到达 kCFRunLoopBeforeWaiting
阶段,说明发生了卡顿。
同样的,如果 Runloop 处在 kCFRunLoopAfterWaiting
时间过长,也是发生了卡顿。
所以,如果我们要利用 Runloop 来监控卡顿的话,就要关注 kCFRunLoopBeforeSources
和 kCFRunLoopAfterWaiting
两个阶段,一般卡顿时间超过 250ms 会被明显感知,所以,可以以连续 5 次卡顿时长超过 50ms 可以认为发生卡顿,或者根据需要调整统计阀值。以下是通过 Runloop 监听卡顿的一个例子:
@interface LagMonitor() {
int timeoutCount;
CFRunLoopObserverRef runLoopObserver;
@public
dispatch_semaphore_t dispatchSemaphore;
CFRunLoopActivity runLoopActivity;
}
@end
@implementation LagMonitor
#pragma mark - Interface
+ (instancetype)shareInstance {
static id instance = nil;
static dispatch_once_t dispatchOnce;
dispatch_once(&dispatchOnce, ^{
instance = [[self alloc] init];
});
return instance;
}
// 开始监控
- (void)beginMonitor {
NSLog(@"开始监控");
if (runLoopObserver) {
return;
}
// 创建信号量,注意这里信号量为 0
dispatchSemaphore = dispatch_semaphore_create(0);
// 创建 Observer
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
runLoopObserverCallBack,
&context);
// 将 Observer 添加到主线程的 RunLoop
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
// 在子线程持续监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) {
/**
信号量 P 操作,成功的话信号量会 - 1,这里超时时间为 50ms,即等待 50ms 后还没成功就返回失败
操作成功,信号量 - 1,返回值为 0;操作失败,返回值非 0
由于初始信号量为 0,这里会阻塞,直到 runLoopObserverCallBack 函数中对信号量做了 V 操作,即 RunLoop 状态发生改变的时候。
*/
long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
// 发生超时,说明 RunLoop 保持在一个状态的时间超过了 50ms
if (!self->runLoopObserver) {
self->timeoutCount = 0;
self->dispatchSemaphore = 0;
self->runLoopActivity = 0;
return;
}
// 如果是在 BeforeSources 或 AfterWaiting 这两个状态持续时间达到超时时间,就代表发生了卡顿
if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
// 出现五次出结果
if (++self->timeoutCount < 5) {
continue;
}
// 发生了卡顿,可以使用 PLCrashReporter 等收集堆栈并上报
NSLog(@"发生了卡顿,");
}
}
self->timeoutCount = 0;
}
});
}
// 结束监控
- (void)endMonitor {
NSLog(@"结束监控");
if (!runLoopObserver) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
// 释放 Observer
CFRelease(runLoopObserver);
runLoopObserver = NULL;
}
#pragma mark - Private
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
LagMonitor *lagMonitor = (__bridge LagMonitor*)info;
lagMonitor->runLoopActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
// 对信号量进行 V 操作,信号量 + 1
dispatch_semaphore_signal(semaphore);
}
@end
上面只是统计卡顿的基础版本,如果真的使用到项目中上面逻辑还有不少需要优化的地方,例如:
- 避免多次重复上报同一个卡顿堆栈
- 可以先将堆栈保存到内存中,以堆栈栈顶函数为特征,如果相同认为整个堆栈是同一个,不重复上报。
- 准确定位真正卡顿的堆栈
- 假如主线程有三个任务,只有第一个是引起卡顿的任务,当开始上报卡顿时获取到的堆栈可能是后两个不耗时的任务的堆栈。这种情况可以每 50ms 甚至更短时间获取一次堆栈,只保留最近一定数量(例如最近 20 个)堆栈信息,当发生卡顿时相同堆栈数量最多的堆栈就是真正引起卡顿的堆栈。
目前也有一些比较成熟的卡顿监控方案,例如:matrix。
🔗 原文链接:
RunLoop 实现原理 - 李峰峰博客
📢 作者声明:
转载请注明来源