RunLoop
文章目录
- RunLoop 简介
1.1 什么是 RunLoop?
1.2 RunLoop 和线程
1.3 默认情况下主线程的 RunLoop 原理- RunLoop 相关类
2.1 CFRunLoopRef
2.2 CFRunLoopModeRef
2.3 CFRunLoopTimerRef
2.4 CFRunLoopSourceRef
2.5 CFRunLoopObserverRef- RunLoop 原理
- RunLoop 实战应用
4.1 NSTimer 的使用
4.2 ImageView 推迟显示
4.3 后台常驻线程(很常用)
本文项目连接地址
1. RunLoop 简介
1.1 什么是 RunLoop?
根据字面意思:Run 表示运行,Loop 表示循环,结合在一起就是运行的循环。
RunLoop 实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如触摸事件,UI 刷新事件,定时器事件,selector 事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省 CPU 资源,提高程序性能。
1.2 RunLoop 和线程
RunLoop 和线程息息相关,我们知道线程的作用是用来执行特定的一个或多个任务,但是在默认情况下,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够处理任务,并不退出。所以就有了 RunLoop。
- 一条线程对应一个 RunLoop 对象,每条线程都有唯一一个与之对应的 RunLoop 对象。
- 我们只能在当前线程中操作当前线程的 RunLoop,而不能去操作其他线程的 RunLoop。
- RunLoop 对象在第一次获取 RunLoop 时创建,销毁则是在线程结束的时候。
- 主线程的 RunLoop 对象,系统自动帮我们创建好了(原理如下)。而子线程的 RunLoop 对象需要我们主动创建.
1.3 默认情况下主线程的 RunLoop 原理
我们再开启一个 iOS 程序的时候,系统会调用创建项目时自动生成的 main.m 文件。main.m 文件如下所示:
int main(int argc, char * argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
其中UIApplicationMain
函数内部帮我们开启了主线程RunLoop,UIApplicationMain
内部拥有一个无线循环的代码。上边的代码中开启RunLoop 的过程可以简单的理解为如下代码:
int main(int argc, char * argv[]) {
BOOL running = YES;
do {
// 执行各种任务,处理各种事件
// ......
} while (running);
return 0;
}
从上边可以看出,程序一直在do-while 循环中执行,所以UIApplicationMain 函数一直没有返回,我们在运行程序之后,程序不会马上退出,会保持持续运行状态。
下图是苹果官方给出的 RunLoop 模型图
从上图可以看出,RunLoop 就是线程中的一个循环,RunLoop 在循环中会不断检测,通过input sources(输入源)和Timer sources(定时源)两种来源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候进行休息。
2. RunLoop 相关类
下面我们来了解一下 Core Founction 框架下关于 RunLoop 的5个类,只有弄懂这几个类的含义,我们才能深入了解 RunLoop 运行机制。
-
CFRunLoopRef
:代表 RunLoop 的对象 -
CFRunLoopModeRef
:RunLoop 的运行模式 -
CFRunLoopSourceRef
:就是上图提到的输入源、事件源 -
CFRunLoopTimerRef
:就是上图提到的定时源 -
CFRunLoopObserverRef
:观察者,可以监听 RunLoop 的状态改变
接下来详细讲解这几个类的具体含义和关系
接着介绍这几个类的相互关系
一个 RunLoop 对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef),定时源(CFRunLoopTimerRef),观察者(CFRunLoopObserverRef)。
- 每次 RunLoop 启动时,只能指定其中一个运行模式
CFRunLoopModeRef
,这个运行模式CFRunLoopModeRef
被称为 CurrentMode。 - 如果需要切换运行模式
CFRunLoopModeRef
,只能退出 Loop,再重新指定一个运行模式CFRunLoopModeRef
进入。 - 这样做主要是为了分隔开不同组的输入源
CFRunLoopSourceRef
,定时源CFRunLoopTimerRef
,观察者CFRunLoopObserverRef
,让其互不影响。
下面我们来详细讲解这五个类:
2.1 CFRunLoopRef
CFRunLoopRef就是 Core Foundation 框架下 RunLoop 对象类,我们可以通过以下方式来获取 RunLoop 对象:
- Core Foundation
-
CFRunLoopGetCurrent()
获得当前线程的 RunLoop 对象 -
CFRunLoopGetMain()
获得主线程的 RunLoop 对象
在 Foundation 框架下获取 RunLoop 对象类的方法如下
-
- Foundation
-
[NSRunLoop currentRunLoop]
获得当前线程的RunLoop对象 -
[NSRunLoop mainRunLoop]
获得主线程的RunLoop对象
-
2.2 CFRunLoopModeRef
系统默认定义了多种运行模式(CFRunLoopModeRef),如下所示
-
kCFRunLoopDefaultMode
App 的默认运行模式,通常主线程是在这个运行模式下运行的 -
UITrackingRunLoopMode
跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响) -
UIInitializationRunLoopMode
在刚启动 App 时首次进入的第一个 Mode,启动完成后就不再使用 -
GSEventReceiveRunLoopMode
接受系统内部事件,通常用不到 -
kCFRunLoopCommonModes
伪模式,不是一种真正的运行模式
其中kCFRunLoopDefaultMode
,UITrackingRunLoopMode
,kCFRunLoopCommonModes
是我们开发中需要用到的模式。
2.3 CFRunLoopTimerRef
CFRunLoopTimerRef是定时源,理解即为基于时间的触发器,可以将其理解为定时器。
下面我们来演示CFRunLoopModeRef
和CFRunLoopTimerRef
结合的使用用法,从而加深理解。
- 往视图中添加一个 Text View。
- 添加一个定时器,每隔2秒控制台输出打印。
- (void)setupTimer {
// 定义一个定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 添加到 runloop
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- 然后运行,这个时候如果我们没有任何操作的话,定时器会稳定的每隔2秒调用 run 方法打印。
- 但是当我们拖动 Text View 滚动时,发现 run 方法不打印了,即 NSTimer 不工作了。当松开鼠标后,NSTimer 又开始正常工作了。
这是因为:
- 当我们不做任何操作的时候,RunLoop 处于
NSDefaultRunLoopMode
下。 - 当我们拖动 Text View 的时候,RunLoop 就会结束
NSDefaultRunLoopMode
,切换到了UITrackingRunLoopMode
模式下,这个模式没有添加 NSTimer,所以我们的 NSTimer 就不工作了。 - 当我们松开鼠标的时候,RunLoop 就结束
UITrackingRunLoopMode
,又切换回```NSDefaultRunLoopMode模式,所以 NSTimer 就又开始正常工作了。
你可以将 NSTimer 添加到UITrackingRunLoopMode下,会发现定时器只会在拖动 Text View 的模式下工作,而不做操作的时候定时器不工作。
- (void)setupTimer {
// 定义一个定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 添加到 runloop
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
}
然道我们就不能在这两种模式都让NSTimer 都能正常工作吗?
当然可以,这就用到了我们之前说的伪模式 (kCFRunLoopCommonModes
),这其实不是一种真实的模式,而是一种标记模式,意思就是可以在打上Common Modes 标记的模式下运行。
那么哪些模式被标记上了 Common Modes 呢?
NSDefaultRunLoopMode
和 UITrackingRunLoopMode
。
所以我们只要将 NSTimer 添加到当前 RunLoop 的kCFRunLoopCommonModes
(Foundation 框架下为NSRunLoopCommonModes
)下,就可以让 NSTimer 在不做操作和拖动 Text View 两种情况下正常工作了.
- (void)setupTimer {
// 定义一个定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 添加到 runloop
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
既然说到 NSTimer,我们再讲下NSTimer 中的scheduledTimerWithTimeInterval
方法和 RunLoop 的关系。代码如下
NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
这句代码调用了scheduledTimer 返回的定时器,NSTimer 会自动被加入到RunLoop 的NSDefaultRunLoopMode模式下。这句代码相当于下面两句代码。
// 定义一个定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 添加到 runloop
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
2.4 CFRunLoopSourceRef
CFRunLoopSourceRef是事件源,(上图中有提到过),CFRunLoopSourceRef有两种分类方法。
-
第一种按照官方文档来分类(就像 RunLoop 模型图中的那样)
- Port-Based Sources(基于端口)
- Custom Input Source(自定义)
- Cocoa Perform Selector Sources
-
第二种按照函数调用栈来分类:
- Source0:非基于 Port
- Source1:基于 Port,通过内核和其他线程通信,接收,分发系统事件
这两种分类方式其实没有什么区别,只不过第一种是通过官方理论来分类,第二种是在实际应用中通过调用函数来分类。
下面举个例子来了解一下函数调用栈和 Source
- 在视图中添加一个按钮,并且在调用方法中输出语句,在执行过程中打上断点,然后查看对应堆栈信息。
查看堆栈信息
所以点击事件时这样来的:
1.首先程序启动,调用16行的 main 函数,main 函数调用15行的UIApplicationMain函数,然后一直往上调用函数,最终调用到0行的BtnClick 函数,即点函数。
- 同时可以看到11行中有 Source0,即说点击事件属于 Sources0函数,点击事件时在 Sources0中处理的。
- 至于 Sources1,则是用来接收,分发系统事件,然后再分发到 Sources0中处理的.
2.5 CFRunLoopObserverRef
CFRunLoopObserverRef是观察者,用来监听 RunLoop 的状态改变
CFRunLoopObserverRef可以监听的状态改变有以下几种:
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 // 监听全部状态改变
};
下面通过代码来监听 RunLoop中的状态改变
// 添加 RunLoop 监听
- (void)addObserver {
// 创建观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop发生改变---%zd",activity);
});
// 添加观察者到当前的 RunLoop 中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放 observer, 最后添加完需要释放掉
CFRelease(observer);
}
运行结果
可以看到RunLoop 的状态不断的改变,最终变成了状态32,也就是即将进入睡眠状态,说明 RunLoop 之后就会进入睡眠状态
3. RunLoop 原理
了解完5个分类,我们就可以来理解 RunLoop 的运行逻辑了
接下来说下官方文档给我们的 RunLoop 逻辑
在每次运行开启 RunLoop 的时候,所在线程的 RunLoop 会自动处理之前未处理的事件,并且通知相关的观察者。
具体顺序如下:
- 通知观察者 RunLoop 已经启动
- 通知观察者即将要开始的定时器
- 通知观察者任何即将启动的非基于端口的源
- 启动任何准备好的非基于端口的源
- 如果基于端口的源准备好并处于等待状态,立即启动,并进入步骤9
- 通知观察者线程进入休眠状态
- 将线程置于休眠,知道任一下面的事件发生:
- 某一事件到达基于端口的源
- 定时器启动
- RunLoop 设置的时间已经超时
- RunLoop 被显示唤醒
- 通知观察者线程将被唤醒
- 处理未处理的事件
- 如果用户定义的定时器启动,处理定时器事件并重启 RunLoop,进入步骤2
- 如果输入源启动,传递相应的消息
- 如果 RunLoop 被显示唤醒而且时间还没超时,重启 RunLoop,进入步骤2
- 通知观察者 RunLoop 结束
4. RunLoop 实战应用
光懂原理没啥用,能够实战应用才是王道,下面讲讲 RunLoop 的几种应用
4.1 NSTimer 的使用(详细见2.3CFRunLoopTimerRef)
4.2ImageView 推迟显示
利用performSelector
方法为 UIImageView 调用setImage:
方法,并利用inModes
将其设置为 RunLoop 下NSDefaultRunLoopMode运行模式,代码如下
1.添加图片视图和 text view 视图
- (void)drawImageView {
_imgView = [[UIImageView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
[self.view addSubview:_imgView];
}
- 在
touchsBegan
方法中调用赋值图片方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[_imgView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"scenery"] afterDelay:4.0 inModes:@[NSDefaultRunLoopMode]];
}
4.运行程序,然后拖动 UITextView,拖动4秒以上,发现过来4秒后,UIImageView 还没有显示图片,当我们松开的时候,图片显示了。
这样我们就实现了在拖动完之后,再延时显示 UIImaeView 了。
4.3 后台常驻线程(很常用)
我们再开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件,后台播放音乐等),我们最好能让这条线程永远常驻内存。
做法如下:
添加一条用于常驻内存的强引用的子线程,在该线程的 RunLoop 下添加一个 Source,开启 RunLoop。
具体操作如下:
1.添加一条强引用的 thread 线程属性。
/** 线程 */
@property(nonatomic, strong)NSThread *thread;
2.创建线程平启动方法
- (void)run1 {
// 执行任务
NSLog(@"---run---");
// 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"未开启RunLoop");
}
- (void)createThread {
// 创建线程
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
// 开启线程
[self.thread start];
}
运行结果
这样我们就开启了一条常驻线程,下面我们可以添加其他任务,除了之前创建的时候调用了 run1,还可以在点击的时候调用 run2方法。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 利用performSelector,在self.thread的线程中调用run2方法执行任务
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)run2 {
NSLog(@"---run2---");
}
运行结果
每当我们点击屏幕的时候,都可以调用---run2---。这样就实现了常驻线程的需求。
本文参考大部分参考iOS多线程:『RunLoop』详尽总结,文章中涉及到的代码也都有实践,经实践完全正确,非常感谢该作者。
iOS多线程详细总结系列文章
iOS GCD之dispatch_semaphore(信号量)
iOS 多线程-GCD 详细总结
iOS 多线程: [NSOperation NSOperationQueue] 详解
iOS 多线程:[pthread,NSThread]详细总结
相关资料参考
深入理解 RunLoop