1.RunLoop简介
- 从字面意思讲跑圈,运行循环。
RunLoop
就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束,函数返回。
- 基本作用
- 保持程序的持续运行;
- 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件);
- 节省CPU资源,提高程序性能:有事处理的时候做事,没有事件处理的时候,会进入睡眠模式。
- 没有RunLoop时模拟如下:
没有RunLoop的情况下,第3行后程序就结束了。 - 有RunLoop时模拟如下:
有RunLoop的情况下,由于main函数里面启动了个RunLoop,所以程序并不会马上退出,保持持续运行状态。 main
函数中的Runloop
:
- 在
UIApplication
函数内部就启动了一个Runloop
该函数返回一个int类型的值; - 这个默认启动的
Runloop
是跟主线程相关联的。
- 在
- iOS中有2套API来访问和使用RunLoop:
CoreFoundation
里面的CFRunLoopRef
CFRunLoopRef
是在CoreFoundation
框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。CFRunLoopRef
是开源的,其源码链接如下:
CFRunLoopRef开源代码下载Foundation
里面的NSRunLoop
NSRunLoop
是基于CFRunLoopRef
的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
RunLoop苹果官方文档NSRunLoop
和CFRunLoopRef
都代表着RunLoop
对象
2.Runloop与线程
- 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,这时就用到
RunLoop
。 - Runloop和线程的关系
- 线程和
RunLoop
之间是一一对应的,其关系是保存在一个全局的Dictionary
里。 - 线程刚创建时并没有
RunLoop
,如果你不主动获取,那它一直都不会有。RunLoop
的创建是发生在第一次获取时(懒加载),RunLoop
的销毁是发生在线程结束时。你只能在一个线程的内部获取其RunLoop
(主线程除外)。
- 线程和
3.RunLoop实现原理
下图是苹果官方文档给出的Runloop运行原理图:
从上图中可以看出,RunLoop就是线程中的一个循环,RunLoop在循环中会不断检测,通过Input sources(输入源)和Timer sources(定时源)两种源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候进行休息。
4.RunLoop 对外的接口
在 CoreFoundation
里面关于 RunLoop
有5个类:
CFRunLoopRef
RunLoop的对象
CFRunLoopModeRef
RunLoop的运行模式
CFRunLoopSourceRef
输入源/事件源
CFRunLoopTimerRef
Timer事件
CFRunLoopObserverRef
监听者,监听RunLoop的状态改变
Runloop
和相关类之间的关系图:
- 一个
RunLoop
对象(CFRunLoopRef
)中包含若干个运行模式(CFRunLoopModeRef
)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef
)、Timer
事件(CFRunLoopTimerRef
)、观察者(CFRunLoopObserverRef
)。 - 每次
RunLoop
启动时,只能指定其中一个Mode
,这个Mode
被称作CurrentMode
。如果需要切换Mode
,只能退出RunLoop
,再重新指定一个Mode
进入。这样做主要是为了分隔开不同组的Source/Timer/Observer
,让其互不影响。
4.1.CFRunLoopRef/NSRunLoop(RunLoop对象)
CFRunLoopRef
就是CoreFoundation
框架下RunLoop
对象类,不能直接创建,只能获取:
CFRunLoopGetMain();
// 获得主线程的RunLoop
对象
CFRunLoopGetCurrent();
// 获得当前线程的RunLoop
对象- 在
Foundation
框架下获取RunLoop
对象类的方法如下:
[NSRunLoop mainRunLoop];
// 获得主线程的RunLoop对象
[NSRunLoop currentRunLoop];
// 获得当前线程的RunLoop对象
4.2.CFRunLoopModeRef(运行模式)
CFRunloopModeRef
代表着Runloop
的运行模式,一个Runloop
中可以有多个Mode
,一个Mode
里面又可以有多个source\observer\timer
,每次runloop
启动的时候,只能指定一个Mode
,这个Mode
被称为该Runloop
的当前Mode
,如果需要切换Mode
,只能先退出当前Runloop
,再重新指定一个Mode
进入,分割不同组的定时器等,让他们相互之间不受影响。- 系统默认注册了5个
Mode
:
kCFRunLoopDefaultMode
:App的默认Mode,通常主线程是在这个Mode下运行。UITrackingRunLoopMode
:跟踪用户交互事件Mode(ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。)UIInitializationRunLoopMode
: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。GSEventReceiveRunLoopMode
: 接受系统事件的内部 Mode,通常用不到。kCFRunLoopCommonModes
: 这是一个占位用的Mode,不是一种真正的Mode。
4.3.CFRunLoopSourceRef
CFRunLoopSourceRef
是事件源,CFRunLoopSourceRef
有两种分类方法:
- 第一种按照官方文档来分类(就像RunLoop模型图中那样):
Port-Based Sources(基于端口)
Custom Input Sources(自定义)
Cocoa Perform Selector Sources - 第二种按照函数调用栈来分类:
Source0 :非基于Port
Source1:基于Port,通过内核和其他线程通信,接收、分发系统事件 - 可以通过打断点的方式查看一个方法的函数调用栈,如下图所示,打断点拦截点击按钮事件
结果分析:程序启动,调用17行的main
函数,main函数调用16行UIApplicationMain
函数,然后一直往上调用函数,最终调用到0行的btnClick
函数,即点击函数。同时我们可以看到11,12行中有Sources0
,也就是说我们点击事件是属于Sources0
函数的,点击事件就是在Sources0
中处理的。而至于Sources1
,则是用来接收、分发系统事件,然后再分发到Sources0
中处理的。
4.4.CFRunLoopTimerRef
CFRunLoopTimerRef
是定时源,理解为基于时间的触发器,时间到了我就触发一个事件,触发一个操作。基本上说的就是NSTimer
。下面结合CFRunLoopModeRef
举例说明:
添加一个UITextView
,在viewDidLoad
方法中创建定时器并每两面调用一次block中的方法:
- (void)viewDidLoad {
[super viewDidLoad];
__block NSInteger num = 0;
//创建定时器 ,没两秒调用一下block里面的方法
NSTimer * timer = [NSTimer timerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * `_Nonnull` timer) {
num ++;
NSLog(@"%zd次",num);
}];
//将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- 结果可见:我们行任何操作的话,定时器会稳定的每隔2秒调用block中的打印方法;但当我们滚动
UITextView
时,打印就会中断(暂停);停止操作后又会继续打印。 - 结果分析:当我们不做任何操作的时候,
RunLoop
处于NSDefaultRunLoopMode
下;当我们拖动UITextView
的时候,RunLoop就结束NSDefaultRunLoopMode
,切换到了UITrackingRunLoopMode
模式下,这个模式下没有添加NSTimer
,所以我们的NSTimer
就不工作了;当我们松开鼠标的时候,RunLoop
就结束UITrackingRunLoopMode
模式,又切换回NSDefaultRunLoopMode
模式,所以NSTimer
就又开始正常工作了。 - 将
RunLoop
的模式改为UITrackingRunLoopMode
模式,会发现只有在滚动UITextView
时,定时器的block
中的打印方法才会调用。 - 要想在两种模式下定时器都能正常工作,就需要用到伪模式(
kCFRunLoopCommonModes
,NSFoundation
框架下为NSRunLoopCommonModes
)
伪模式不是一种真实的模式,而是一种标记模式,意思就是可以在打上CommonModes
标记的模式下运行,如下修改即可:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSTimer
中的scheduledTimerWithTimeInterval
方法和RunLoop
的关系如下:
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
//等等同于下面两行代码
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
4.5.CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者,用来监听RunLoop
的状态改变、可以监听的状态改变有以下几种:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop:1
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer:2
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source:4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠:32
kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒:64
kCFRunLoopExit = (1UL << 7), // 即将从RunLoop中退出: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);
//最后添加完需要释放掉
CFRelease(observer);
}
打印结果如下:
结果可见:RunLoop
的状态在不断的改变,最终变成了状态 32,也就是即将进入睡眠状态,说明RunLoop
最后就会进入睡眠状态。
5.Runloop运行逻辑
- 根据苹果在文档里的说明,
RunLoop
运行逻辑大致如下:
- 在每次运行开启RunLoop的时候,所在线程的RunLoop会自动处理之前未处理的事件,并且通知相关的观察者。具体的顺序如下:
- 通知观察者RunLoop已经启动
- 通知观察者即将要开始的定时器
- 通知观察者任何即将启动的非基于端口的源
- 启动任何准备好的非基于端口的源
- 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
- 通知观察者线程进入休眠状态
- 将线程置于休眠知道任一下面的事件发生:
(1)某一事件到达基于端口的源
(2)定时器启动
(3)RunLoop设置的时间已经超时
(4)RunLoop被显示唤醒 - 通知观察者线程将被唤醒
- 处理未处理的事件
(1)如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
(2)如果输入源启动,传递相应的消息
(3)如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2 - 通知观察者RunLoop结束
6.RunLoop应用
- NSTimer的使用,上面已经讲过。
- ImageView推迟显示。
当界面中含有UITableView
,而且每个UITableViewCell
里边都有图片。这时候当我们滚动UITableView
的时候,如果有一堆的图片需要显示,那么可能会出现卡顿的现象。
怎么解决这个问题呢?
- 监听
UIScrollView
的滚动
因为UITableView
继承自UIScrollView
,所以我们可以通过监听UIScrollView
的滚动,实现UIScrollView
相关delegate
即可。 - 利用
PerformSelector
设置当前线程的RunLoop
的运行模式
利用performSelector
方法为UIImageView
调用setImage:
方法,并利用inModes
将其设置为RunLoop
下NSDefaultRunLoopMode
运行模式。代码如下:
- 监听
- (void)viewDidLoad {
[super viewDidLoad];
__block NSInteger num = 0;
// 创建定时器 ,没两秒调用一下block里面的方法
NSTimer * timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
num ++;
NSLog(@"%zd秒",num);
}];
//将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//延迟6秒设置图片
[self.imageV performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"we"] afterDelay:6.0 inModes:@[NSDefaultRunLoopMode]];
}
结果如下:
结果可见:当滚动UITextView时,6秒后图片并没有显示,当停止滚动时图片才显示。
- 常驻后台线程(很常用)
我们在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存。
做法
:添加一条用于常驻内存的强引用的子线程,在该线程的RunLoop
下添加一个Sources
,开启RunLoop
。
示例:
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic ,strong) NSThread * thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//初始化线程
self.thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"runThread:%@",[NSThread currentThread]);
// 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
NSRunLoop * currentRL = [NSRunLoop currentRunLoop];
[currentRL addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[currentRL run];
// 如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"NOopenRunLoop");
}];
//开启线程
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 利用performSelector,在self.thread的线程中调用run2方法执行任务
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES modes:@[NSDefaultRunLoopMode]];
}
- (void)test{
NSLog(@"testThread:%@",[NSThread currentThread]);
}
结果可见:除了之前打印的runThread
外,每当我们点击屏幕,都打印testThread
,但是没有打印NOopenRunLoop
说明开启了RunLoop
循环。
参考文档:
深入理解RunLoop
RunLoop苹果官方文档
iOS多线程相关文章:
iOS多线程简述
iOS多线程-pthread、NSThread
iOS多线程-GCD
iOS多线程-NSOperation, NSOperationQueue
iOS多线程-RunLoop
OC单例模式详解