RunLoop 是什么
runloop
就是一个运行循环,目的是让程序运行起来不会直接结束,能在有任务的时候处理任务,没有任务的时候等待处理任务。
iOS 中有两套API 可以访问 runloop
- Foundation : NSRunLoop
- Core Foundation : CFRunLoopRef
参考
- Runloop 源码 Objective-C 版本:OC_CFRunLoop
- Runloop 源码最新的 Swift 版本:swift_CFRunLoop.c
- 官方源码下载地址:https://opensource.apple.com/tarballs/CF/
- 官方文档 Run Loops
RunLoop 与线程的关系
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
获取 RunLoop
对象
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
可以从 CFRunLoopGetCurrent() 源码中看到相关获取逻辑,代码简化如下
CFRunLoopRef CFRunLoopGetCurrent(void) {
// 返回根据当前线程取到的runloop
return _CFRunLoopGet0(pthread_self());
}
// 核心逻辑如下,已简化代码
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 1.判断如果没有runloop字典
if (!__CFRunLoops) {
// 2.创建一个字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 3.根据主线程创建runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(_CFMainPThread);
// 4.在dict中将主线程指针和当前创建的runloop以 key:value 形式保存
CFDictionarySetValue(dict, pthreadPointer(_CFMainPThread), mainLoop);
// 5.dict 赋值给 __CFRunLoops(代码经简化,源码并非如此简单)
__CFRunLoops = dict;
}
// 6. 从 __CFRunLoops 中根据参数t指针获取runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
// 7. 如果没有就创建一个新loop对象
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
// 8. __CFRunLoops 中添加参数t的指针和新loop对象保存
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
// 9. 新runloop 对象就是要返回的loop对象
loop = newLoop;
}
return loop;
}
runloop 内部数据结构
runloop 内部核心数据
struct __CFRunLoop {
pthread_t _pthread; // 对应的线程
CFMutableSetRef _commonModes; // 通用Mode
CFMutableSetRef _commonModeItems; // 通用mode的items
CFRunLoopModeRef _currentMode; // 当前mode
CFMutableSetRef _modes; // 所有的mode
};
从数据结构中可以看到,实际就是 runloop 中包含自己的 thread 和 mode, CFRunLoopModeRef 类型结构如下
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
可以看到 runloop 的内部结构基本如图
runloop 的运行模式 CFRunLoopModeRef
- CFRunLoopModeRef代表RunLoop的运行模式
- 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
- RunLoop启动时只能选择其中一个Mode,作为currentMode
- 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
- 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
- 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
- 常见的2种Mode
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
Runloop 的运行逻辑
说白了 runloop 就是在整循环中一直检查当前运行的 mode 内部有没有需要处理的事件,有就处理,没有就休眠
Runloop 的 mode 内部主要有下面4类事件要处理
-
Source0
- 触摸事件处理
- performSelector:onThread:
-
Source1
- 基于Port的线程间通信
- 系统事件捕捉
-
Timers
- NSTimer
- performSelector:withObject:afterDelay:
-
Observers
- 用于监听RunLoop的状态
- UI刷新(BeforeWaiting)
- Autorelease pool(BeforeWaiting)
一次循环处理事件的逻辑如下
一个简单的线程保活
@interface PermenantThread()
@property (strong, nonatomic) NSThread *innerThread;
@end
@implementation PermenantThread
#pragma mark - public methods
- (instancetype)init
{
if (self = [super init]) {
self.innerThread = [[NSThread alloc] initWithBlock:^{
// 创建上下文(要初始化一下结构体)
CFRunLoopSourceContext context = {0};
// 创建source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// 往Runloop中添加source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 销毁source
CFRelease(source);
// 启动
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
// while (weakSelf && !weakSelf.isStopped) {
// // 第3个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop
// CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
// }
NSLog(@"end----");
}];
[self.innerThread start];
}
return self;
}
- (void)executeTask:(PermenantThreadTask)task
{
if (!self.innerThread || !task) return;
[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}
- (void)stop
{
if (!self.innerThread) return;
[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
#pragma mark - private methods
- (void)__stop
{
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}
- (void)__executeTask:(PermenantThreadTask)task
{
task();
}
@end
面试题
- 讲讲 Runloop,项目中有用到吗?
1. 用到过
2. 线程保活,对于频繁需要在子线程做的操作,使用 runloo 进行线程保活 -- 适用于一个串行线程,非并发的情况。如需要一个后台线程持续做否些计算,网络请求。
3. 如 AFNetworking 2.0版本的时候就是后台有一个常驻线程
1. 实例
1. 控制线程声明周期(线程保活)
2. 解决NSTimer在滑动时停止工作的问题
3. 监控应用卡顿
4. 性能优化
- runloop 内部实现逻辑?
1. runloop 的创建逻辑,从内部的 dict 中获取,没有就新建,并保存,目的是一条线程只对应一个runloop
2. runloop 的运行逻辑,本质上就是一个 while() 循环。每次进入循环的时候处理如上图事件。没有事件的时候进入休眠(通过调用内核接口实现)。
- runloop 和线程的关系?
是一一对应的关系。
其实保存在 __CFRunLoops 全局 dict 中,以线程指针为key,线程对应的runloop地址为value
- timer 和 线程的关系?
- 程序中添加每3秒响应一次的NSTimer,当拖动 tableView 时候 timer 可能无法响应怎么解决?
将 timer 添加到 commonMode中,这样就可以处理普通模式和tracking 模式下的事件了
- runloop 是怎么影响用户操作的,具体流程是什么样的?
由 source1 捕捉用户的触摸事件, 由 source0 处理用户的触摸事件。 V158T13
- 说说 runloop 的几种状态?
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
- runloop 的 mode 作用是什么?
不同 mode 将各自的 source、timer、observer 隔离开,这样同一时间只能执行一个 mode 下的 source、timer、observer,这样在某种模式下就会比较流畅。