一. Runloop简介
- RunLoop字面意思就是一个跑起来的循环,我们的程序之所以能一直运行不会无端退出就是因为RunLoop的存在。
- RunLoop用来处理程序运行过程中出现的各种事件,从而保持程序的持续运行。而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。
二. Runloop的作用
1. 保持程序的一直运行
每个iOS程序都有一个main函数的入口:1
2
3
4
5int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
程序之所以能一直运行是因为UIApplicationMain函数创建了一个Runloop维持着程序运行。
如果我们将main函数改成下面这样,那么程序一旦启动就会结束。1
2
3
4
5
6int main(int argc, char * argv[]) {
@autoreleasepool {
}
return 0;
}
2. 处理程序运行中的各种事件:
触摸事件
定时器事件
手势识别
界面刷新
网络请求
Selector
…
三. 获取Runloop对象
iOS中有两套API可以访问和使用Runloop:
Foundation:NSRunloop
1
2
3
4//获取当前线程的RunLoop对象
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
//获取主线程的RunLoop对象
NSRunLoop *runloop2 = [NSRunLoop mainRunLoop];Core Foundation:CFRunloopRef
1
2
3
4//获取当前线程的RunLoop对象
CFRunLoopRef runloop3 = CFRunLoopGetCurrent();
//获取主线程的RunLoop对象
CFRunLoopRef runloop4 = CFRunLoopGetMain();
NSRunloop是基于CFRunloopRef的OC包装。
四. Runloop与线程
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
五. Runloop相关的类
CFRunLoopRef:Runloop类
CFRunLoopModeRef:CFRunLoopMode类
CFRunLoopSourceRef:是事件产生的地方。Source有两个版本:Source0 和 Source1。
CFRunLoopTimerRef:基于时间的触发器,它是NSTimer的C语言版本,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef: 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
CFRunLoop的源码结构(__CFRunLoop结构体中包含的元素不单单是这几个,其他不重要的就没有往里面写了,CFRunLoopMode的源码结构同理)1
2
3
4
5
6
7struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};
pthread_t _pthread:Runloop对象当前所处的线程。
CFMutableSetRef _commonModes:_commonModes是一个集合,里面装的是通用的模式,即Runloop对象可以运行的模式。
CFMutableSetRef _commonModeItems:_commonModeItems也是一个集合,里面装的是通用模式中的source0,source1,obersvers,timers等对象。
CFRunLoopModeRef _currentMode:Runloop对象当前所处的模式。
CFMutableSetRef _modes:Runloop对象可以使用的模式集合。
CFRunLoopMode的源码结构1
2
3
4
5
6
7struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
CFStringRef _name:字符串名称
CFMutableSetRef _sources0:包含了一个回调(函数指针),它并不能主动触发事件。使用时,需要将这个 Source 标记为待处理,然后唤醒 RunLoop,让其处理这个事件。
CFMutableSetRef _sources1:包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。 能主动唤醒 RunLoop 的线程。
CFMutableArrayRef _observers:_observers集合
CFMutableArrayRef _timers:_timers集合
六. 常见的几种CFRunLoopMode
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- kCFRunLoopCommonModes:并不是真正的CFRunLoopMode,而是代表了所有标有common属性的CFRunLoopMode,是一个模式集合。当绑定一个事件源到这个模式集合的时候就相当于绑定到了集合内的每一个模式。上面两个Mode就标有common属性。
七. NSTimer在UITrackingRunLoopMode模式下的Runloop中失效的问题
NSTimer的创建方式有两种:
scheduledTimerWithTimeInterval:1
2
3
4__block NSInteger count = 0;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%ld",(long)count++);
}];
timerWithTimeInterval:1
2
3
4__block NSInteger count = 0;\
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%ld",(long)count++);
}];
运行上面两段代码,你会发现第一种方式会一直正常的执行打印信息,而第二种创建方式不会有任何打印信息。因为通过scheduledTimerWithTimeInterval创建的timer会被自动加入到kCFRunLoopDefaultMode下的Runloop中执行(就像方法名一样该Timer被安排好执行了)。而通过timerWithTimeInterval:创建的timer需要手动添加到Runloop中执行。
所以通过scheduledTimerWithTimeInterval创建的timer在界面滚动的时候会停止打印,因为界面一旦滚动,当前Runloop会自动切换到UITrackingRunLoopMode模式,而此时timer只能在kCFRunLoopDefaultMode中执行,所以才会失效。
我们可以将创建出来的timer添加到通用模式中执行,这样不管当前Runloop处于哪种模式,timer的执行都不会受到影响:1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
这里需要注意的是,如果Timer是添加到特定模式下的Runloop中,那么timer对象会存储在CFRunLoopMode的CFMutableArrayRef _timers中。如果Timer是添加到通用模式下的Runloop中,timer对象会储存在CFRunLoop的CFMutableSetRef _commonModeItems中。
CFRunloopRef的源码是完全公开的,可以通过以下链接访问下载源码: https://opensource.apple.com/tarballs/CF/