基本概念
定义
通常来讲,一个线程在执行完任务后,会直接退出。但在我们日常的App使用中,即使什么都不做,App也不会退出。这其中的原理,便是运行时在线程中构建一个消息循环,使得这个线程中一直有任务执行。在iOS和OSX中,这个消息循环的机制便被称为RunLoop。
所以Runloop就是通过内部维护的事件循环来对事件/消息进行管理的对象。
Swift中的RunLoop对应OC中的NSRunLoop,本文以Swift的RunLoop作为主要讲解名词。
作用
-
保持程序的正常运行
-
处理APP中的各种事件
1. 事件响应、手势识别、界面刷新
2. 网络请求
3. GCD Async Main Queue
4. Timer定时器、performSelector
5. AutoReleasePool
6. ···
-
节省cpu资源、提供程序的性能:该做事就做事,该休息就休息
CFRunloop
-
CFRunLoop对象监视任务的输入源,并在它们准备好进行处理时分派控制。 -
运行循环可以监视三种类型的对象:
CFRunLoopSource、CFRunLoopTimer和CFRunLoopObserver。 -
添加到运行循环中的每个源、计时器和观察者必须与一个或多个运行循环模式相关联。
-
Core Foundation定义了一种特殊的伪模式,称为common modes,它允许您将多个模式与给定的source、timer或observer关联起来。 -
每个线程只有一个运行循环。你既不创建也不销毁线程的运行循环。
Core Foundation会根据需要自动为您创建它。 -
运行循环可以递归地运行。您可以在任何运行循环调用中调用
CFRunLoopRun或CFRunLoopRunInMode,并在当前线程的调用堆栈上创建嵌套的运行循环激活。 -
Cocoa应用程序构建在CFRunLoop之上,实现它们自己的高级事件循环。在编写应用程序时,可以将源代码、计时器和观察者添加到它们的运行循环对象和模式中。然后,您的对象将作为常规应用程序事件循环的一部分被监视。使用RunLoop的GetCFRunLoop方法可以得到对应的CFRunLoopRef类型。
RunLoop
-
RunLoop是对Core Fundation中的CFRunloop的封装 -
RunLoop对象处理:
- 来自窗口系统的鼠标和键盘事件
- NSPort对象和NSConnection对象等源的输入。
- NSTimer事件。
-
RunLoop类通常被认为是线程不安全的,它的方法应该只在当前线程的上下文中被调用。永远不要尝试调用运行在不同线程中的RunLoop对象的方法,因为这样做可能会导致意想不到的结果。
CFRunLoop结构分析
struct __CFRunLoop {
···
_CFThreadRef _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
···
};
// Mode
struct __CFRunLoopMode {
···
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
···
};
// Source
struct __CFRunLoopSource {
···
union {
// 用以区分Source0和Source1
CFRunLoopSourceContext version0;
CFRunLoopSourceContext1 version1;
} _context;
};
// Observer
struct __CFRunLoopObserver {
···
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
···
};
// Timer
struct __CFRunLoopTimer {
···
uint16_t _bits;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
···
};
如图所示
__CFRunLoopModeRef
每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作CurrentMode,如果需要切换 Mode,只能退出Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
代表RunLoop的运行模式
-
一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
-
如果Mode里面没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
-
切换mode是在循环里面切换的,所以不会导致程序退出
-
常见mode
1. KCFRunLoopDefaultMode (对应NSDefaultRunLoopMode):App的默认Mode,通常是主线程是在这个Mode下运行
2. UITrackingRunLoopMode : 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
__CFRunLoopSource
事件产生的地方
-
Source0:**一般处理触摸事件和performSelector:onThread:。 只包含一个回调函数指针 (代表不能主动触发) **,需要给一个待处理的signal,再调用wakeup唤醒runLoop处理事件。 -
Source1:**基于Port的线程间通信,系统事件的捕捉(包括触摸事件,捕捉到后传递给Source0)。 **基于包含mach_port(端口通讯)和回调函数指针。被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。
__CFRunLoopObserver
是观察者,每个 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
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
__CFRunLoopTimer
基于时间的触发器,它和 **NSTimer** 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
RunLoop实现逻辑
下图来自苹果官方文档,帮助理解RunLoop的实现逻辑。

下面我们从CFRunLoopRun的源码着手来梳理一下流程。源码有重点删减,篇幅较长,可以跳过,直接看结论
void CFRunLoopRun(void) {
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
···
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
可以看到主要调用的是CFRunLoopRunSpecific方法,当返回的Mode不为kCFRunLoopRunStopped/kCFRunLoopRunFinished时,就一直执行
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl,
CFStringRef modeName,
CFTimeInterval seconds,
Boolean returnAfterSourceHandled) {
// 基本状态判断
···
// 根据modeName找到本次运行的mode
CFRunLoopModeRef currentMode = __CFRunLoopCopyMode(rl, modeName, false);
// 如果没找到,或mode中没有注册任何事件,则就此停止
···
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
// 取上一次运行的mode
CFRunLoopModeRef previousMode = rl->_currentMode;
// 将本次运行的mode赋值给runloop中存储
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
// 【mode判断】若当前为即将进入entry,通知对应观察者
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_START, rl, currentMode, seconds, previousMode);
// 执行runloop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_END, rl, currentMode, seconds, previousMode);
// 【mode判断】若当前即将退出runloop,通知对应观察者
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
···
return result;
}
// __CFRunLoopRun执行runloop
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
//获取系统启动后的CPU运行时间,用于控制超时时间
uint64_t startTSR = mach_absolute_time();
// 基本mode状态判断
···
// mach端口,在内核中,消息在端口之间传递。 初始为0
__CFPort dispatchPort = CFPORT_NULL;
// 判断是否为主线程
Boolean libdispatchQSafe = (pthread_main_np() == 1) && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
// 如果在主线程 ,并且 runloop是主线程的runloop ,并且 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
uint64_t termTSR = 0ULL;
dispatch_source_t timeout_timer = NULL;
// 基本超时时间判断
···
// 超时时间设置永不超时
seconds = 9999999999.0;
termTSR = UINT64_MAX;
Boolean didDispatchPortLastTime = true;
int32_t retVal = 0;
do {
voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;
voucher_t voucherCopy = NULL;
//初始化一个存放内核消息的缓冲池
uint8_t msg_buffer[3 * 1024];
mach_msg_header_t *msg = NULL;
mach_port_t livePort = MACH_PORT_NULL;
// 取所有需要监听的port
__CFPortSet waitSet = rlm->_portSet;
// 设置RunLoop为可以被唤醒状态
__CFRunLoopUnsetIgnoreWakeUps(rl);
// 【mode判断】若即将触发 Timer 回调,通知对应观察者
if (rlm->_observerMask & kCFRunLoopBeforeTimers) {
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
}
// 【mode判断】若即将触发 Source0(非port) 回调,通知对应观察者
if (rlm->_observerMask & kCFRunLoopBeforeSources) {
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
}
// 执行被加入的block
__CFRunLoopDoBlocks(rl, rlm);
// 触发 Source0 (非port) 回调。
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
//如果没有Sources0事件处理 并且 没有超时,poll为false
//如果有Sources0事件处理 或者 超时,poll都为true
Boolean poll = sourceHandledThisLoop || (0ULL == termTSR);
//第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true
if (CFPORT_NULL != dispatchPort && !didDispatchPortLastTime) {
//从缓冲区读取消息
msg = (mach_msg_header_t *)msg_buffer;
// 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL, rl, rlm)) {
//如果接收到了消息的话,处理msg
goto handle_msg;
}
}
didDispatchPortLastTime = false;
// 【mode判断】若runloop即将休眠,通知对应观察者
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 使runloop休眠
__CFRunLoopSetSleeping(rl);
···
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
msg = (mach_msg_header_t *)msg_buffer;
/// 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy, rl, rlm);
// 设置当前runloop的sleepTime为当前时间减去开始时间
rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
// 移除端口
__CFPortSetRemove(dispatchPort, waitSet);
// 设置RunLoop为不可以被唤醒状态
__CFRunLoopSetIgnoreWakeUps(rl);
// 若刚从休眠中唤醒,通知对应观察者
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 若当前不为主线程端口
if (rlm->_timerPort != CFPORT_NULL && livePort == rlm->_timerPort) {
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// 重新设置下一个计时器
rlm->_timerSoftDeadline = UINT64_MAX;
rlm->_timerHardDeadline = UINT64_MAX;
__CFArmNextTimerInMode(rlm, rl);
}
}
/* --- DISPATCHES --- */
else if (livePort == dispatchPort) {
// 如果有dispatch到main_queue的block,执行block
···
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
}
/* --- SOURCE1S --- */
else {
/// 如果一个 Source1 (基于port) 发出事件了,处理这个事件
CFRUNLOOP_WAKEUP_FOR_SOURCE();
cf_trace(KDEBUG_EVENT_CFRL_DID_WAKEUP_FOR_SOURCE, rl, rlm, 0, 0);
// Despite the name, this works for windows handles as well
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
mach_msg_header_t *reply = NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
} else {
os_log_error(_CFOSLog(), "__CFRunLoopModeFindSourceForMachPort returned NULL for mode '%@' livePort: %u", rlm->_name, livePort);
}
}
/* --- BLOCKS --- */
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (termTSR < mach_absolute_time()) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
/// 被外部调用者强制停止了
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
/// 自动停止了
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
}
return retVal;
}
总结:
RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
RunLoop & 线程
-
Cocoa和Core Foundation都提供了运行循环对象(
RunLoop/NSRunLoop和CFRunloop)来帮助配置和管理线程的运行循环。 -
iOS 开发中能遇到两个线程对象:
pthread_t和NSThread。两者是一一对应的。CFRunLoop是基于pthread来管理的。线程和RunLoop在Core Foundation中通过key-value的形式一一对应 -
作为应用程序启动过程的一部分,应用程序框架自动在主线程上设置并运行运行循环。而子线程需要显式地运行它们的运行循环。
-
线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。下述源码可以验证这一点
源码探索
-
我们从
CFRunLoopGetMain和CFRunLoopGetCurrent入手,可以看到其实现中都是调用了_CFRunLoopGet0函数,分别传入的参数是pthread_main_thread_np()(主RunLoop线程)和pthread_self()(当前线程)
CFRunLoopRef CFRunLoopGetMain(void) {
···
static CFRunLoopRef __main = NULL;
// 调用_CFRunLoopGet0,传入参数为0
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np());
return __main;
}
CFRunLoopRef CFRunLoopGetCurrent(void) {
···
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
// 调用_CFRunLoopGet0,传入参数为当前线程的线程ID
return _CFRunLoopGet0(pthread_self());
}
// CFRunLoopGetMain调用_CFRunLoopGet0的参数解析
CF_EXPORT _CFThreadRef _CFMainPThread;
_CFThreadRef _CFMainPThread = kNilPthreadT;
#define kNilPthreadT (_CFThreadRef)0
-
_CFRunLoopGet0源码分析如下
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(_CFThreadRef t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
// static CFMutableDictionaryRef __CFRunLoops = NULL;
// 如上所示,__CFRunLoops为一个全局的字典
if (!__CFRunLoops) {
// 若字典为空,则创建一个空字典并将主Runloop存入,key为线程指针
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
···
CFRelease(dict);
}
CFRelease(mainLoop);
}
CFRunLoopRef newLoop = NULL;
// 根据传入的线程ID从全局字典中获取runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
// 若runloop为空,则创建并存入全局字典
newLoop = __CFRunLoopCreate(t);
···
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
···
// 注:注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
return loop;
}
总结:
-
先有线程,再有runLoop
-
子线程刚创建时并没有RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 会在第一次获取时创建,在线程结束时销毁。
-
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。
RunLoop & AutoReleasePool
App启动后,苹果在主线程创建了其关联的RunLoop,并在该RunLoop中注册两个Observer
-
第一个
Observer(优先级最高)监控的事件:Entry(即将进入Loop),回调内会调用_objc_autoreleasePoolPush()创建自动释放池 -
第二个
Observer(优先级最低)监控两个事件:
- 第一个事件:BeforeWaiting(准备进入休眠),回调内会调用_objc_autoreleasePoolPop()释放旧的池并调用_objc_autoreleasePoolPush()创建新的池
- 第二个事件:Exit(即将退出Loop),回调内会调用_objc_autoreleasePoolPop()销毁自动释放池
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,**每当一个运行循环结束的时候,它都会释放一次autorelease pool,同时pool中的所有自动释放类型变量都会被释放掉。 **所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
常见问题
Timer
-
【主线程问题】以
scheduledTimerWithTimeInterval的方式开启的Timer,是以kCFRunLoopDefaultMode的mode添加到当前Runloop的,如下所示
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer._timer!, kCFRunLoopDefaultMode)
所以在主线程开启的Timer会被手势事件影响。解决方案:将Timer添加到kCFRunLoopCommonModes 中,如下所示
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSRunLoopCommonModes];
-
【子线程问题】子线程默认不开启Runloop,所以如果在子线程添加timer会无法执行
解决方案一:
NSThread 思路:在子线程中将NSTimer以默认方式加到该线程的runloop中,启动子线程。
NSThread *t = [[NSThread alloc] initWithTarget:self
selector:@selector(privatePrint)
object:nil];
[t start];
- (void)privatePrint {
[NSTimer scheduledTimerWithTimeInterval:3
repeats:YES
block:^(NSTimer * _Nonnull timer) {
NSLog(@"--%@",[[NSRunLoop currentRunLoop] currentMode]);
NSLog(@"--%@",[[NSRunLoop mainRunLoop] isEqual:[NSRunLoop currentRunLoop]] ? @"main" : @"not main");
}];
[[NSRunLoop currentRunLoop] run];
}
解决方案二:GCD
// 声明属性
@property(nonatomic, strong) dispatch_source_t timers;
- (void)runloopTimer {
uint64_t interval = 0.01 * NSEC_PER_SEC;
dispatch_queue_t queue = dispatch_queue_create("my queue", 0);
_timers = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_timers,
dispatch_time(DISPATCH_TIME_NOW, 0),
interval,
0);
__weak ViewController *blockSelf = self;
dispatch_source_set_event_handler(_timers, ^() {
[blockSelf addTime];
});
dispatch_resume(_timers);
}
- (void)addTime {
_timeCount += 100;
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主线程执行UI操作
});
}
参考文档: