面试题引发的思考:
Q: RunLoop内部实现逻辑?

Q: RunLoop和线程的关系?
- 每条线程都有唯一的一个与之对应的RunLoop对象;
- RunLoop保存在一个全局的
Dictionary里,线程作为key,RunLoop作为value; - 主线程的RunLoop自动创建,子线程的RunLoop需要手动创建;
- RunLoop在第一次获取线程时创建,在线程结束时销毁。
Q: timer与RunLoop的关系?
timer运行在RunLoop里,相当于RunLoop来控制timer何时执行。
Q: RunLoop 是怎么响应用户操作的, 具体流程是什么样的?
- 先由
Source1捕捉触摸事件,然后由Source0去处理触摸事件。
Q: RunLoop有哪几种状态?

Q: RunLoop的mode作用是什么?
- 把不同模式下的
Source0/Source1/Timer/Observer隔离开来,互不影响,会使程序在当前模式下流畅运行。
1. RunLoop简介
(1) RunLoop概念
RunLoop顾名思义,运行循环,在程序运行过程中循环做一些事情:
- 如果没有RunLoop程序执行完毕就会立即退出;
- 如果有RunLoop程序并不会马上退出,而是保持运行状态,等待处理程序的各种事件;
- RunLoop可以保持程序的持续运行,在没有事件处理的时候使程序进入休眠模式,从而节省CPU资源,提高程序性能。

代码如上:没有RunLoop,执行完第14行代码后,会立即退出程序。

代码如上:有RunLoop,程序并不会马上退出,而是保持运行状态。
其伪代码如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
int result = 0;
do {
// 睡眠中等待消息
int message = sleep_and_wait();
// 处理消息
result = process_message(message);
} while (result == 0);
return 0;
}
}
RunLoop确实是do while通过判断result的值实现的。因此,我们可以把RunLoop看成一个死循环。
(2) RunLoop应用
RunLoop应用范畴:
- 定时器(
Timer)、PerformSelectorGCD Async Main Queue- 事件响应、手势识别、界面刷新
- 网络请求
AutoreleasePool
2. RunLoop相关概念
(1) RunLoop对象
- iOS中有2套API来访问和使用RunLoop
-
Foundation:NSRunLoop -
Core Foundation:CFRunLoopRef -
NSRunLoop和CFRunLoopRef都代表着RunLoop对象 -
NSRunLoop是基于CFRunLoopRef的一层OC包装 - CFRunLoopRef 源码是开源的
- (void)viewDidLoad {
[super viewDidLoad];
// Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
// Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
}
(2) RunLoop与线程
- 每条线程都有唯一的一个与之对应的RunLoop对象;
- RunLoop保存在一个全局的
Dictionary里,线程作为key,RunLoop作为value;- 主线程的RunLoop自动创建,子线程的Runloop需要手动创建;
- RunLoop在第一次获取线程时创建,在线程结束时销毁。
以上结论都可以由以下相关源码总结得出:

CFRunLoopGetCurrent调用_CFRunLoopGet0:

(3) RunLoop相关的类
Core Foundation中关于RunLoop的5个类:
CFRunLoopRef:获得当前RunLoop和主RunLoopCFRunLoopModeRef:运行模式CFRunLoopSourceRef:事件源,输入源CFRunLoopTimerRef:定时器时间CFRunLoopObserverRef:观察者
由相关源码可知:


由以上代码可以得出RunLoop五个类直接的对应关系:

- 一个RunLoop包含若干个
Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer;- RunLoop启动时只能选择其中一个
Mode,作为_currentMode- 如果需要切换
Mode,只能退出当前Loop,再重新选择一个Mode进入;
不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响;- 如果
Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。
Source0: 触摸事件处理、performSelector:onThread:Source1: 基于Port的线程间通信、系统事件捕捉Timers:NSTimer、performSelector:withObject:afterDelay:Observers: 用于监听RunLoop的状态、UI刷新(BeforeWaiting)、Autorelease pool(BeforeWaiting)
1> CFRunLoopModeRef
常见的2种
Mode:
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):
App的默认Mode,通常主线程是在这个Mode下运行;UITrackingRunLoopMode:
界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
2> CFRunLoopObserverRef
RunLoop的Observer状态类型如下:

监听RunLoop状态:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry - %@", mode);
CFRelease(mode);
break;
}
case kCFRunLoopExit: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit - %@", mode);
CFRelease(mode);
break;
}
default:
break;
}
});
// 添加observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
}
在ViewController放一个UITextView控件,运行程序之后,滑动控件,查看打印结果:

由打印结果可知:
- 控件开始滚动时,先退出
kCFRunLoopDefaultMode,然后进入UITrackingRunLoopMode; - 控件停止滚动时,先退出
UITrackingRunLoopMode,然后进入kCFRunLoopDefaultMode。
如此便产生了模式的切换。
3. RunLoop运行逻辑
(1) 官方文档解读
由苹果官方文档可知RunLoop的运行逻辑如下:

由以上图例可知:
RunLoop在运行循环过程中,接收到Input sources或者Timer sources时会通过对应处理方式处理;没有事件消息传入的时候,就会使程序处于休眠状态。
(2) 源码解读
下面通过源码解读RunLoop底层实现。

首先通过运行程序断点可以知道RunLoop入口函数为CFRunLoopRunSpecific函数。
对相关源码进行简化,以便解读RunLoop底层实现:

RunLoop底层实现代码流程图如下:

(3) 特殊要点 GCD相关
一般GCD是有自己的处理逻辑,不依赖RunLoop实现;
GCD有一种特殊情况,需要交给RunLoop进行处理:

(4) RunLoop休眠的实现原理
由RunLoop底层实现可知:

RunLoop是通过__CFRunLoopServiceMachPort函数来休眠的:

__CFRunLoopServiceMachPort函数主要方法为mach_msg函数,mach_msg是内核层面的API,这是程序真正达到休眠的方式。
那么RunLoop休眠的实现原理如下:

RunLoop在用户态调用mach_msg()时,会自动转到内核态,调用内核态的mach_msg(),达到真正休眠的目的:
没有消息就让线程休眠;有消息就唤醒线程,回到用户态处理消息。