一. 简介
-
RunLoop
:通常指的是NSRunLoop
和CFRunLoopRef
。 -
CFRunLoopRef
:是CoreFoundation
框架下RunLoop
的对象类,是纯C的代码函数。 -
NSRunLoop
:是Foundation
框架下RunLoop
的对象类,是CFRunloopRef
的OC封装。
二. 什么是RunLoop?
-
RunLoop
:运行环,处理在程序的运行过程中出现的各种事件,保证了程序的持续运行。 -
RunLoop
与线程
息息相关,每条线程都有一个与之对应的RunLoop
对象。 - 主线程的
RunLoop
对象系统自动创建好的,子线程的RunLoop
对象则需要手动获取。 - 当
RunLoop
所对应的线程没有事件需要处理的时候,RunLoop
会使线程
进入睡眠模式
,从而节省CPU
资源,提高程序的性能。
三. RunLoop与线程的关系
RunLoop
,就是一个对象,每条线程都有一个对应的RunLoop
对象。一般来说,一个线程只能执行一个任务,执行完之后,线程就会退出。这个RunLoop
对象可以在线程没有任何事件需要处理的时候,使线程
进入睡眠模式
,节省CPU
资源,提高程序的性能。在线程上有消息需要处理的时候,立刻唤醒线程
,执行操作任务。
四. RunLoop的原理
RunLoop
它就是线程中的一个循环,创建启动之后,它会在循环中不断的检测有没有等待接受的事件。如果有,RunLoop
会通知线程进行处理,如果没有,会让线程进入睡眠模式
。其底层实现类似于一个do{}while()
的循环函数,该函数一直处于等待-处理
的循环之中,直到超时或者手动让这个循环结束。主线程的RunLoop
只要程序不退出、崩溃
会一直循环。
五. 相关类
- CFRunLoopRef:RunLoop 的对象
- CFRunLoopModeRef:RunLoop 的运行模式
- CFRunLoopSourceRef:RunLoop 的输入源 / 事件源
- CFRunLoopTimerRef:RunLoop 的定时源
- CFRunLoopObserverRef:RunLoop 的观察者,监听 RunLoop 的状态改变
注:每一个RunLoop 的对象
包含若干个RunLoop 的运行模式
,而每一个RunLoop 的运行模式
又包含若干个RunLoop 的输入源 / 事件源
、RunLoop 的定时源
以及RunLoop 的观察者
。
1. CFRunLoopRef
- 获取当前线程的
RunLoop
对象:
CFRunLoopGetCurrent();
- 获得主线程的
RunLoop
对象:
CFRunLoopGetMain();
另外,如果使用NSRunLoop
- 获取当前线程的
RunLoop
对象:
[NSRunLoop currentRunLoop];
- 获得主线程的
RunLoop
对象:
CFRunLoopGetMain();
注:苹果不允许直接创建RunLoop
对象,只能通过以上的方法获取RunLoop
,其实整个程序的每一个RunLoop
对象都放在一个全局的Dictionary
里面。线程刚创建时并没有RunLoop
对象,如果你不主动获取,那它一直都不会有。RunLoop
对象的创建是发生在第一次获取的时候,并将它保存在Dictionary
里面。RunLoop
对象的销毁是发生在线程结束时。
- 非主线程的
RunLoop
对象,只能在线程的内部获取。
2. CFRunLoopModeRef
- kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行。
- UITrackingRunLoopMode:跟踪用户交互事件模式。
-
kCFRunLoopCommonModes:伪模式,不是一种真实的模式,而是一种模式组合,即
kCFRunLoopDefaultMode
模式和UITrackingRunLoopMode
模式的结合。(下面有例子说明)
注:每次调用RunLoop
的主函数时,只能指定其中一种Mode
(模式),这个Mode被称CurrentMode
(当前模式)。如果需要切换Mode
,会先退出Loop
(其实就是退出当前的Mode
),再重新指定一个Mode
进入。这样做主要是为了分隔开不同Mode
下的 Source、Timer、Observer
,让他们互不影响。
3. CFRunLoopTimerRef
定时源,理解为基于时间的触发器,其实就是NSTimer
,NSTimer
的触发是基于RunLoop
运行的。
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic,strong)UIScrollView *scrollView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
[self.view addSubview:_scrollView];
[_scrollView setContentSize:CGSizeMake(self.view.frame.size.width, self.view.frame.size.height * 2)];
// 定义一个定时器,约定两秒之后调用self的run方法
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)run{
NSLog(@"---");
}
@end
- 如果是
NSDefaultRunLoopMode
模式,则在scrollView
不滑动的情况,每隔两秒,就会打印一次---
。 - 如果是
UITrackingRunLoopMode
模式,则在scrollView
滑动的情况,每隔两秒,就会打印一次---
。 - 如果是
kCFRunLoopCommonModes
模式,则在scrollView
不管滑不滑动,每隔两秒,都会打印一次---
。
总结:RunLoop
默认是NSDefaultRunLoopMode
模式,当scrollView
滑动的时候,RunLoop
就结束NSDefaultRunLoopMode
模式,切换成UITrackingRunLoopMode
模式。当scrollView
滑动结束的时候,RunLoop
又结束UITrackingRunLoopMode
模式,重新切换成NSDefaultRunLoopMode
模式。NSTimer
添加在RunLoop
的哪一种模式下,- (void)run
就在哪一种模式下执行。
4. CFRunLoopSourceRef
事件源,事件产生的地方。有两个版本:Source0
和Source1
。
-
Source0
,只包含了一个回调,不能主动触发事件。需要先调用CFRunLoopSourceSignal(source)
,将这个Source
标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop
对象,让其处理这个事件。(外部手动添加的事件。) -
Source1
,包含了一个mach_port
和一个回调,被用于通过内核和其他线程相互发送消息。这种Source
能主动唤醒RunLoop
的线程。(系统事件)
5. CFRunLoopObserverRef
观察者,用来监听RunLoop
的状态改变。
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop:1
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer:2
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source:4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠:32
kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒:64
kCFRunLoopExit = (1UL << 7), // 即将从Loop中退出:128
kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听全部状态改变
};
- (void)viewDidLoad {
[super viewDidLoad];
// 创建观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop发生改变---%zd",activity);
});
// 添加观察者到当前RunLoop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放observer,最后添加完需要释放掉
CFRelease(observer);
}
六. RunLoop的运行逻辑
RunLoop
创建并启动。
- 通知观察者,即将进入
RunLoop
。 - 通知观察者,将要处理
NSTimer
。 - 通知观察者,将要处理事件
Source
。 - 通知观察者,正在处理事件
Source
。 - 通知观察者,线程进入休眠状态。(没有
Source
需要处理) - 通知观察者,
RunLoop
结束。
RunLoop
销毁。(主线程的RunLoop
不会销毁)
七. RunLoop的应用
1. NSTimer的使用
参考上面CFRunLoopTimerRef
类的例子。
2. ImageView延迟加载显示
需求:列表上面每一个cell都网络图片要显示,正常操作的话,可能会出现卡顿现象。可以通过优化,当列表在滑动的时候,不加载图片,等列表停止滑动之后,再加载图片。
[_itemImg performSelector:@selector(sd_setImageWithURL:) withObject:[NSURL URLWithString:model.pictUrl] afterDelay:1.0 inModes:@[NSDefaultRunLoopMode]];
解释:当调用NSObject
的performSelecter:afterDelay:
后,实际上其内部会创建一个NSTimer
并添加到当前线程的RunLoop
中。
3. 常驻线程
需求:如果经常需要一些耗时操作在子线程上面执行,那么可以让一条子线程永远常驻内存。(省去不断创建线程的麻烦)
#import "ViewController.h"
@interface ViewController ()
// 常驻内存的线程
@property (nonatomic,strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run{
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
@end
开启runLoop
有三种方法:
// 第一种
- (void)run;
// 第二种
- (void)runUntilDate:(NSDate *)limitDate;
// 第三种
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
注:这三种方式无论通过哪一种方式启动runLoop
,如果没有一个数据源或者timer
附加于runLoop
上,runloop
就会立刻退出。
- 第一种方式,
runLoop
会一直运行下去,在此期间会处理来自数据源Source
的数据,并且会在NSDefaultRunLoopMode
模式下重复调用runMode:beforeDate:
方法。 - 第二种方式,可以设置超时时间,在超时时间到达之前,
runLoop
会一直运行,在此期间runLoop
会处理来自数据源Source
的数据,并且也会在NSDefaultRunLoopMode
模式下重复调用runMode:beforeDate:
方法; - 第三种方式,
runLoop
会运行一次,超时时间到达或者第一个数据源Source
被处理,则runLoop
就会退出。
退出runLoop
- 第一种启动方式想退出
runLoop
,不应该使用第一种启动方式来启动runLoop
。虽然runLoop
在没有数据源Source
或者附加的timer
,runLoop
就会退出。但是系统内部有可能会在当前线程的runLoop
中添加一些输入源,导致无法退出。 - 第二种启动方式可以通过设置超时时间来退出
runLoop
。 - 第三种启动方式,
runLoop
会运行一次,当超时时间到达或者第一个数据源Source
被处理,runLoop
就会退出。