面试题引发的思考:
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
)、PerformSelector
GCD 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()
,达到真正休眠的目的:
没有消息就让线程休眠;有消息就唤醒线程,回到用户态处理消息。