一、RunLoop简介
RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
事件循环:
没有消息处理时,休眠以避免资源占用。用户态切换到内核态,等待消息。
有消息需要处理时,立刻被唤醒。内核态切换到用户态,处理消息。维护事件循环可以用来不断处理消息和事件,并对他们进行管理。
runloop通过调用mach_msg()函数来转移当前线程的控制权给内核态/用户态。
基本作用
1、 保持程序的持续运行
如果没有RunLoop,main()函数一执行完,程序就会立刻退出。
而 iOS 程序能保持持续运行的原因就是在main()函数中调用了UIApplicationMain函数,这个函数内部会启动主线程的RunLoop;
2、处理 App 中的的各种事件(比如触摸事件、定时器事件等);
3、节省 CPU 资源,提高程序性能:该做事时做事,该休息时休息。
RunLoop对象
iOS 中有 2 套 API 来访问和使用RunLoop:
Foundation:NSRunLoop(是CFRunLoopRef的封装,提供了面向对象的 API)
Core Foundation:CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop不开源,而CFRunLoopRef是开源的:Core Foundation 源码
获取RunLoop对象的方式:
//Foundation
[NSRunLoop mainRunLoop]; // 获取主线程的 RunLoop 对象
[NSRunLoop currentRunLoop]; // 获取当前线程的 RunLoop 对象
// Core Foundation
CFRunLoopGetMain(); // 获取主线程的 RunLoop 对象
CFRunLoopGetCurrent(); // 获取当前线程的 RunLoop 对象
应用范畴
定时器(Timer)、PerformSelector
GCD:dispatch_async(dispatch_get_main_queue(), ^{ });
事件响应、手势识别、界面刷新
网络请求
AutoreleasePool
二、RunLoop数据结构
CFRunLoopRef
RunLoop对象的底层就是一个CFRunLoopRef结构体,它里面存储着:
_pthread:RunLoop与线程是一一对应关系
_commonModes:存储着 NSString 对象的集合(Mode 的名称)
_commonModeItems:存储着被标记为通用模式的Source0/Source1/Timer/Observer
_currentMode:RunLoop当前的运行模式
_modes:存储着RunLoop所有的 Mode(CFRunLoopModeRef)模式
// CFRunLoop.h
typedef struct __CFRunLoop * CFRunLoopRef;
// CFRunLoop.c
struct __CFRunLoop {
.......
pthread_t _pthread; //与线程一一对应
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
......
};
CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式;
一个RunLoop包含若干个 Mode,每个 Mode 又包含若干个Source0/Source1/Timer/Observer;
RunLoop启动时只能选择其中一个 Mode,作为 currentMode;
如果需要切换 Mode,只能退出当前 Loop,再重新选择一个 Mode 进入,切换模式不会导致程序退出;
不同 Mode 中的Source0/Source1/Timer/Observer能分隔开来,互不影响;
如果 Mode 里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。
// CFRunLoop.h
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
// CFRunLoop.c
struct __CFRunLoopMode {
CFStringRef _name; // mode 类型,如:NSDefaultRunLoopMode
CFMutableSetRef _sources0; // CFRunLoopSourceRef
CFMutableSetRef _sources1; // CFRunLoopSourceRef
CFMutableArrayRef _observers; // CFRunLoopObserverRef
CFMutableArrayRef _timers; // CFRunLoopTimerRef
...
};
RunLoop 的常见模式
NSDefaultRunLoopMode / KCFRunLoopDefaultMode:默认模式
UITrackingRunLoopMode:界面追踪模式,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
NSRunLoopCommonModes / KCFRunLoopCommonModes:通用模式(默认包含 KCFRunLoopDefaultMode 和 UITrackingRunLoopMode)
该模式不是实际存在的一种模式,它只是一个特殊的标记,是同步Source0/Source1/Timer/Observer到多个 Mode 中的技术方案。
被标记为通用模式的Source0/Source1/Timer/Observer都会存放到 _commonModeItems 集合中,会同步这些Source0/Source1/Timer/Observer到多个 Mode 中。
注意:
NSDefaultRunLoopMode和NSRunLoopCommonModes属于Foundation框架;
KCFRunLoopDefaultMode和KCFRunLoopCommonModes属于Core Foundation框架;
前者是对后者的封装,作用相同。
CFRunLoopModeRef 这样设计有什么好处?Runloop为什么会有多个 Mode?
Mode 做到了屏蔽的效果,当RunLoop运行在 Mode1 下面的时候,是处理不了 Mode2 的事件的,有多个 Mode 使得其在处理不同类型任务时更加灵活、高效和可控。
比如NSDefaultRunLoopMode默认模式和UITrackingRunLoopMode滚动模式,滚动屏幕的时候就会切换到滚动模式,就不用去处理默认模式下的事件了,保证了 UITableView 等的滚动顺畅。
CFRunLoopSourceRef
在RunLoop中有两个很重要的概念,一个是上面提到的模式,还有一个就是事件源。事件源分为输入源(Input Sources)和定时器源(Timer Sources)两种;
输入源(Input Sources)又分为Source0和Source1两种,以下__CFRunLoopSource中的共用体union中的version0和version1就分别对应Source0和Source1。
// CFRunLoop.h
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
// CFRunLoop.m
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
Source0 和 Source1的区别
Source0:需要手动唤醒线程:添加Source0到RunLoop并不会主动唤醒线程,需要手动唤醒)例如: 触摸事件处理、performSelector:onThread:
Source1:具备唤醒线程的能力,例如基于 Port 的线程间通信和系统事件捕捉。
系统事件捕捉是由Source1来处理,然后再交给Source0处理
CFRunLoopTimerRef
CFRunloopTimer和NSTimer是 toll-free bridged 的,可以相互转换;
performSelector:withObject:afterDelay:方法会创建timer并添加到RunLoop中。
// CFRunLoop.h
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
// CFRunLoop.c
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; // 添加该 timer 的 RunLoop
CFMutableSetRef _rlModes; // 所有包含该 timer 的 modeName
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable 理想时间间隔 */
CFTimeInterval _tolerance; /* mutable 时间偏差 */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable 回调入口 */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
CFRunLoopObserverRef
CFRunLoopObserverRef用来监听RunLoop的 6 种活动状态。
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timers
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Sources
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 表示以上所有状态
};
UI 刷新(BeforeWaiting)
Autorelease pool(BeforeWaiting)
// CFRunLoop.h
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
// CFRunLoop.c
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; // 添加该 observer 的 RunLoop
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable 监听的活动状态 */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable 回调入口 */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
CFRunLoopObserverRef中的_activities用来保存RunLoop的活动状态。
当RunLoop的状态发生改变时,通过回调_callout通知所有监听这个状态的Observer。
三、事件循环机制
主线程的 RunLoop 的启动过程
首先我们来看一下主线程的RunLoop的启动过程。
上面我们说过,iOS 程序能保持持续运行的原因就是在main()函数中调用了UIApplicationMain函数,这个函数内部会启动主线程的RunLoop。
打断点,通过 LLDB 指令bt查看函数调用栈如下

可以看到,在UIApplicationMain函数中调用了 Core Foundation 框架下的CFRunLoopRunSpecific函数。
CFRunLoopRunSpecific 函数实现:
RunLoop 的入口。
查看源码中该函数的实现,如下:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
......
其他逻辑校验等
......
// 通知 Observers:即将进入 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// RunLoop 具体要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 Observers:即将退出 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
从函数调用栈,以及CFRunLoopRunSpecific函数的实现中可以得知,RunLoop事件循环的实现机制体现在__CFRunLoopRun函数中。

__CFRunLoopRun 函数实现:
事件循环的实现机制。
由于该函数实现较复杂,以下为删掉细节的精简版本,想探究具体的可以查看Core Foundation 源码
/**
* __CFRunLoopRun
*
* @param rl 运行的 RunLoop 对象
* @param rlm 运行的 mode
* @param seconds loop 超时时间
* @param stopAfterHandle true: RunLoop 处理完事件就退出 false:一直运行直到超时或者被手动终止
* @param previousMode 上一次运行的 mode
*
* @return 返回 4 种状态
*/
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode)
{
......
int32_t retVal = 0;
do {
// 通知 Observers:即将处理 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers:即将处理 Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 处理 Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 判断有无 Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有 Source1,就跳转到 handle_msg
goto handle_msg;
}
// 通知 Observers:即将进入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// ⚠️休眠,等待消息来唤醒线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers:刚从休眠中唤醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被 Timer 唤醒) {
// 处理 Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
} else if (被 GCD 唤醒) {
// 处理 GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被 Source1 唤醒
// 处理 Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
......
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
/* 设置返回值 */
// 进入 loop 时参数为处理完事件就返回
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
// 超出传入参数标记的超时时间
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
// 被外部调用者强制停止
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
// 自动停止
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
// mode 中没有任何的 Source0/Source1/Timer/Observer
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
......
return retVal;
}
从该函数实现中可以得知RunLoop主要就做以下几件事情:
__CFRunLoopDoObservers:通知Observers接下来要做什么
__CFRunLoopDoBlocks:处理Blocks
__CFRunLoopDoSources0:处理Sources0
__CFRunLoopDoSources1:处理Sources1
__CFRunLoopDoTimers:处理Timers
处理 GCD 相关:dispatch_async(dispatch_get_main_queue(), ^{ });
__CFRunLoopSetSleeping/__CFRunLoopUnsetSleeping:休眠等待/结束休眠
__CFRunLoopServiceMachPort -> mach-msg():转移当前线程的控制权

__CFRunLoopServiceMachPort 函数实现:
RunLoop 休眠的实现原理。
在__CFRunLoopRun函数中,会调用__CFRunLoopServiceMachPort函数,该函数中调用了mach_msg()函数来转移当前线程的控制权给内核态/用户态。
没有消息需要处理时,休眠线程以避免资源占用。调用mach_msg()从用户态切换到内核态,等待消息;
有消息需要处理时,立刻唤醒线程,调用mach_msg()回到用户态处理消息。
这就是RunLoop休眠的实现原理,也是RunLoop与简单的do...while循环区别:
RunLoop:休眠的时候,当前线程不会做任何事,CPU 不会再分配资源;
简单的do...while循环:当前线程并没有休息,一直占用 CPU 资源。
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
Boolean originalBuffer = true;
kern_return_t ret = KERN_SUCCESS;
for (;;) { /* In that sleep of death what nightmares may come ... */
mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
msg->msgh_bits = 0;
msg->msgh_local_port = port;
msg->msgh_remote_port = MACH_PORT_NULL;
msg->msgh_size = buffer_size;
msg->msgh_id = 0;
if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
// ⚠️⚠️⚠️
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
// Take care of all voucher-related work right after mach_msg.
// If we don't release the previous voucher we're going to leak it.
voucher_mach_msg_revert(*voucherState);
// Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
*voucherState = voucher_mach_msg_adopt(msg);
if (voucherCopy) {
if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) {
// Caller requested a copy of the voucher at this point. By doing this right next to mach_msg we make sure that no voucher has been set in between the return of mach_msg and the use of the voucher copy.
// CFMachPortBoost uses the voucher to drop importance explicitly. However, we want to make sure we only drop importance for a new voucher (not unchanged), so we only set the TSD when the voucher is not state_unchanged.
*voucherCopy = voucher_copy();
} else {
*voucherCopy = NULL;
}
}
CFRUNLOOP_WAKEUP(ret);
if (MACH_MSG_SUCCESS == ret) {
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
if (MACH_RCV_TIMED_OUT == ret) {
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
if (MACH_RCV_TOO_LARGE != ret) break;
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer, buffer_size);
}
HALT;
return false;
}
四、RunLoop与线程
RunLoop官方文档以及RunLoop的数据结构中可知,Runloop和线程的关系:
RunLoop对象和线程是一一对应关系;
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value;
如果没有RunLoop,线程执行完任务就会退出;如果没有RunLoop,主线程执行完main()函数就会退出,程序就不能处于运行状态;
RunLoop创建时机:线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建;
RunLoop销毁时机:RunLoop会在线程结束时销毁;
主线程的RunLoop自动获取(创建),子线程默认没有开启RunLoop;
主线程的RunLoop对象是在UIApplicationMain中通过[NSRunLoop currentRunLoop]获取,一旦发现它不存在,就会创建RunLoop对象。
未启动 RunLoop 的子线程
创建一个NSThread的子类HTThread并重写了dealloc方法来观察线程的状态。
执行以下代码,发现子线程执行完一次test任务就退出销毁了,没有再执行test任务,原因就是没有启动该线程的RunLoop。
@implementation HFThread
- (void)dealloc {
NSLog(@"%s ",__func__);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
HFThread *thread = [[HFThread alloc]initWithTarget:self selector:@selector(threadAction) object:nil];
[thread start];
[self performSelector:@selector(threadAction) onThread:thread withObject:nil waitUntilDone:NO];
}
- (void)threadAction {
NSLog(@"test on %@", [NSThread currentThread]);
}
打印输出:
test on <HFThread: 0x600001a517c0>{number = 7, name = (null)}
[HFThread dealloc]
开启子线程的 RunLoop 的过程
获取 RunLoop 对象
上面提到过可以使用了获取runloop对象的方法:
//Foundation
[NSRunLoop mainRunLoop]; // 获取主线程的 RunLoop 对象
[NSRunLoop currentRunLoop]; // 获取当前线程的 RunLoop 对象
// Core Foundation
CFRunLoopGetMain(); // 获取主线程的 RunLoop 对象
CFRunLoopGetCurrent(); // 获取当前线程的 RunLoop 对象
再来看一下CFRunLoopGetCurrent()函数是怎么获取RunLoop对象的:
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self()); // 调用 _CFRunLoopGet0 函数并传入当前线程
}
_CFRunLoopGet0内部实现:
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// ️当前线程作为 Key,从 __CFRunLoops 字典中获取 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) { // ️如果字典中不存在
CFRunLoopRef newLoop = __CFRunLoopCreate(t); // 创建当前线程的 RunLoop
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); // 保存到字典中
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
启动子线程的RunLoop
可以通过以下方式来启动子线程的RunLoop:
// Foundation
[[NSRunLoop currentRunLoop] run];
[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// Core Foundation
CFRunLoopRun();
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
// 第3个参数:设置为 true,代表执行完 Source/Port 后就会退出当前 loop
- (void)threadAction {
NSLog(@"test on %@", [NSThread currentThread]);
[[NSRunLoop currentRunLoop] run];
}
打印输出
test on <HFThread: 0x6000006d43c0>{number = 7, name = (null)}
test on <HFThread: 0x6000006d43c0>{number = 7, name = (null)}
CFRunLoopRun()/CFRunLoopRunInMode()函数是怎么启动RunLoop的:
void CFRunLoopRun(void) {
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) {
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
CFRunLoopRunSpecific在上面描述事件循环机制的时候详细讲解过。
实现一个常驻线程
好处:
可以长期执行任务,适用于需要持续执行的后台务,例如网络请求,定时任务等。
减少资源消耗,常驻线程不需要频繁的创建销毁线程,可以减少资源消耗和系统开销。
提高响应速度,常驻线程中的光任务可以减少线程切换的开销,提高响应速度,特别适合需要频繁执行任务的场景。
设计条件:该任务需是串行的,而非并发;
设计步骤:
1、获取/创建当前线程的RunLoop;
2、向该RunLoop中添加一个Source/Port等来维持RunLoop的事件循环(如果 Mode 里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出);
3、启动该RunLoop。
#import "HFNextViewViewController.h"
#import "HFThread.h"
@interface HFNextViewViewController ()
@property (nonatomic, assign, getter=isStoped) BOOL stopped;
@property (nonatomic, strong) HFThread *thread;
@end
@implementation HFNextViewViewController
- (void)dealloc
{
NSLog(@"%s", __func__);
if (!self.thread) return;
// 在子线程调用(waitUntilDone设置为YES,代表子线程的代码执行完毕后,当前方法才会继续往下执行)
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
[self hf_addButton];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[HFThread alloc] initWithBlock:^{
NSLog(@"begin-----%@", [NSThread currentThread]);
// ① 获取/创建当前线程的 RunLoop
// ② 向该 RunLoop 中添加一个 Source/Port 等来维持 RunLoop 的事件循环
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStoped) {
// ③ 启动该 RunLoop
/*
[[NSRunLoop currentRunLoop] run]
如果调用 RunLoop 的 run 方法,则会开启一个永不销毁的线程
因为 run 方法会通过反复调用 runMode:beforeDate: 方法,以运行在 NSDefaultRunLoopMode 模式下
换句话说,该方法有效地开启了一个无限的循环,处理来自 RunLoop 的输入源 Sources 和 Timers 的数据
*/
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"end-----%@", [NSThread currentThread]);
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (!self.thread) return;
[self performSelector:@selector(threadAction) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)threadAction {
NSLog(@"%s- - - - %@",__func__, [NSThread currentThread]);
}
// 停止子线程的 RunLoop
- (void)stopThread
{
// 设置标记为 YES
self.stopped = YES;
// 停止 RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
// 清空线程
self.thread = nil;
}
- (void)hf_addButton {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(50, 140, 200, 200);
button.backgroundColor = [UIColor redColor];
[self.view addSubview:button];
[button addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)buttonAction:(UIButton *)button
{
NSLog(@"back= %@",button);
[self dismissViewControllerAnimated:YES completion:nil];
}
@end
#import "HFThread.h"
@implementation HFThread
- (void)dealloc {
NSLog(@"%s ",__func__);
}
@end
多次点击HFNextViewViewController的view,然后退出此页面,输出日志
begin-----<HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController threadAction]- - - - <HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController threadAction]- - - - <HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController threadAction]- - - - <HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController dealloc]
-[HFNextViewViewController stopThread]-----<HFThread: 0x600000d03340>{number = 8, name = (null)}
end-----<HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFThread dealloc]
五、RunLoop与NSTimer
NSTimer是由RunLoop来管理的,NSTimer其实就是CFRunLoopTimerRef,他们之间是 toll-free bridged 的,可以相互转换;
如果在子线程上使用NSTimer,就必须开启子线程的RunLoop,否则定时器无法生效。
解决 tableview 滑动时 NSTimer 失效的问题
问题:
由上面的分析我们知道,RunLoop同一时间只能运行在一种模式下,当我们滑动tableview/scrollview的时候RunLoop会切换到UITrackingRunLoopMode界面追踪模式下。如果我们的NSTimer是添加到RunLoop的KCFRunLoopDefaultMode/NSDefaultRunLoopMode默认模式下的话,此时是会失效的。
解决方案:
可以将NSTimer添加到RunLoop的KCFRunLoopCommonModes/NSRunLoopCommonModes通用模式下,来保证无论在默认模式还是界面追踪模式下NSTimer都可以执行。
NSTimer的创建方式
// 方式一、这种方式创建的NSTimer是自动添加到RunLoop模式下的
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
// 方式二、通过timerxxx开头方法创建的NSTimer是不会自动添加到RunLoop中的,
// 所以一定要记得手动添加,否则NSTimer不生效。
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
CFRunLoopAddTimer 函数实现
CFRunLoopAddTimer()函数中会判断传入的modeName模式名称是不是kCFRunLoopCommonModes通用模式。
是的话就会将timer添加到RunLoop的 _commonModeItems 集合中,并同步该timer到 _commonModes 里的所有模式中,
这样无论在默认模式还是界面追踪模式下NSTimer都可以执行。
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
__CFRunLoopLock(rl);
if (modeName == kCFRunLoopCommonModes) { // 判断 modeName 是不是 kCFRunLoopCommonModes
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) { // 懒加载,判断 _commonModeItems 是否为空,是的话创建
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rlt); // 将 timer 添加到 _commonModeItems 中
if (NULL != set) {
CFTypeRef context[2] = {rl, rlt}; // 将 timer 和 RunLoop 封装到 context 中
/* add new item to all common-modes */
// 遍历 commonModes,将 timer 添加到 commonModes 的所有模式下
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
......
}
}
static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
CFStringRef modeName = (CFStringRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
}
}
NSTimer 和 CADisplayLink 存在的问题
问题:
不准时:NSTime和CADisplayLink底层都是基于RunLoop的CFRunLoopTimerRef的实现的,也就是说它们都依赖于RunLoop。如果RunLoop的任务过于繁重,会导致它们不准时。
比如NSTimer每1.0秒就会执行一次任务,Runloop每进行一次循环,就会看一下NSTimer的时间是否达到1.0秒,是的话就执行任务。但是由于Runloop每一次循环的任务不一样,所花费的时间就不固定。假设第一次循环所花时间为 0.2s,第二次 0.3s,第三次 0.3s,则再过 0.2s 就会执行NSTimer的任务,这时候可能Runloop的任务过于繁重,第四次花了0.5s,那加起来时间就是 1.3s,导致NSTimer不准时。
解决方案:
使用 GCD 的定时器。GCD 的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLoop,所以它非常的准时。
转载参考:
深入浅出RoonLoop