今天我们来学习下iOS中一个较为重要的核心--RunLoop。其实我们对RunLoop既熟悉又陌生。熟悉是因为我们在开发中时不时的都会用到它,陌生是因为它较为底层,我们对它的了解不是多。今天我们就一起来揭开RunLoop神秘的面纱,对他进行一个较为简单的介绍。通过我的简单介绍,如果大家能对它有一定的了解和认识,那无论是在平时的工作中,还是在以后的面试中,它都是我们能拿上台面的一个利器。好,废话不多说,进入正题吧。
1.RunLoop的简单介绍
1.1 RunLoop的基本概念
那么到底是什么RunLoop呢,从字面意思上可以得知,他就是“跑圈”,翻译的雅一点就是“运行的循环”。在iOS SDK中,RunLoop实际上也是一个对象,这个对象在循环的处理程序在运行过程中发出各种事件,例如,用户点击了屏幕(TouchEvent
),UI界面的刷新事件,定时器事件(Timer
),Selector
事件等等。当我们将我们的程序退出到后台,注意只是退出到后台,并没有terminate,这时RunLoop不会处理任何事件,此时为了节省CPU的资源,RunLoop会自动进入休眠模式。
1.2 RunLoop和线程的关系
当我们谈论到RunLoop,其实我们就会提到与之息息相关的东西,线程。我们知道,线程的作用是用来执行一个或者多个特定任务的,一般情况下,某个线程的任务执行完毕,他就return掉。那么,我们为了让线程能够不断执行我们指派的,或者系统分配任务,让其任务执行完后并不return,这时候我们就用到了RunLoop。
一个线程对应唯一一个RunLoop对象,这点很重要。但是往往,RunLoop并不能保证我们线程安全,因为我们只能在当前线程中操作与之对应的RunLoop对象,而不能在当前线程中去操作其他线程的RunLoop对象。子线程的RunLoop对象,需要我们手动去创建和维护,当线程结束时,它也就销毁了。这里我们就会想到,那么主线程mainThread
的RunLoop,是谁创建的呢?其实这个问题有点白痴 : ],当然是系统自动创建的。
1.3 主线程的RunLoop
当我们创建一个新工程的时候,我们找他的main.m
,我们可以看到他的代码很简单,就一个main方法,代码如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
这个方法就是我们程序的入口,在这个方法中,首先标注了一个自动释放池autoreleasepool
,然后再返回一个系统函数UIApplicationMain
执行结果,其实这个UIApplicationMain
函数就是为我们的程序创建了一个主线程的RunLoop,只要我们的程序不crash掉,那他就会一直运行循环,直到我们将程序退出到后台,或者terminate掉,它才会被挂起或是被销毁掉。
我们再来看看,官方文档上是如何图解RunLoop的,如下图所示:
从上图中我们可以看出,RunLoop实际就是一个循环,它会一直监听输入源
InputSources
和定时源TimerSources
,一旦接收到事件,它便会对其进行处理,如果长时间没有检测到事件,那么它会自动进入休眠状态。
2.RunLoop相关类介绍
我们要想对RunLoop有跟进一步的理解,我则需要对CoreFoundation
框架下的与RunLoop相关的5个类,他们分别是:
-
CFRunLoopRef
:RunLoop对象 -
CFRunLoopMode
:RunLoop的运行模式 -
CFRunLoopSourceRef
:RunLoop监听的输入源(事件源) -
CFRunLoopTimerRef
:RunLoop监听的定时源(Timer源) -
CFRunLoopObserverRef
:观察者,监听RunLoop状态的变化
接下来我们就来详细的说下上面的这5个类的具体内容和他们之间的关系。
首先我们来看下5个类的关系,如下图所示,
我们来简单解释下他们之间的关系,一个
CFRunLoopRef
对象包含若干个CFRunLoopMode
运行模式,每个运行模式又包含若干个CFRunLoopSourceRef
输入源、CFRunLoopTimerRef
定时源、CFRunLoopObserverRef
观察者。这里需要注意的是,虽然一个RunLoopRef
对象可以包含若干个CFRunLoopMode
运行模式,但是该RunLoopRef
对象当前运行的模式只能是指定的它包含的若干个CFRunLoopMode
运行模式中的一个,那么这个被指定的运行模式就叫做“当前运行模式”CurrentMode
。但是CurrentMode
是可以进行切换的,如果需要切换运行模式,则需要退出当前的Loop,然后重新指定CFRunLoopMode
。这样做的目的其实也很容明白,因为每一个CFRunLoopMode
包含若干个CFRunLoopSourceRef
、CFRunLoopTimerRef
、CFRunLoopObserverRef
,为了能有效的隔离不同CFRunLoopMode
的CFRunLoopSourceRef
、CFRunLoopTimerRef
、CFRunLoopObserverRef
,使其不同CFRunLoopSourceRef
互不影响,所以一个CFRunLoopRef
只能指定一个CurrentMode
。接下来,我们将逐一详细的来介绍与RunLoop密切相关的这5个类。
2.1 CFRunLoopRef
类
CFRunLoopRef是CoreFoundation内库中RunLoop对象类。我们可以以下方式来获取CFRunLoopRef对象。
// 获取当前线程的CFRunLoopRef
CFRunLoopRef runLoopRef = CFRunLoopGetCurrent();
// 获取主线程的CFRunLoopRef
CFRunLoopRef mainRunLoopRef = CFRunLoopGetMain();
NSRunLoop是Foundation内库中RunLoop的对象。我们同样可以使用以下方法来回去NSRunLoop对象
// 获取当前RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 获取主线程的RunLoop
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
2.2 CFRunLoopMode
类
iOS系统第一了很多种运行模式,下面我们一一简单的介绍:
-
kCFRunLoopDefaultMode
:App默认运行模式,通常我们的主线程的当前模式就是这个模式。 -
UITrackingRunLoopMode
:跟踪用户交互事件(用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode的影响)。 -
UIInitializationRunLoopMode
:当App刚启动的时候,系统将进入这个模式,启动完成后将切换到另外的运行模式。 -
GSEventReceiveRunLoopMode
:接受系统内部事件,通常我们再开发的时候很少用这个运行模式。 -
kCFRunLoopCommonModes
:伪模式,不是一种真正的运行模式,我们在后面的内容中会介绍到这个运行模式。
以上提及到的5种运行模式中,我们在开发过中常用到的是kCFRunLoopDefaultMode
、UITrackingRunLoopMode
、kCFRunLoopCommonModes
,那么下面就来讲讲这三种运行模式的具体用法。
2.3 CFRunLoopTimerRef (Timer事件源)
CFRunLoopTimerRef
是我们上文提及到的定时源,他是RunLoop监听的事件源之一,在RunLoop相关类的关系图中我们提过它。我们可以简单的将其理解为基于时间的触发器。我们也可以将其理解我们开发时常用的NSTimer。
接下来我们举一个简单的例子来演示一下CFRunLoopMode
和CFRunLoopTimerRef
相结合的简单用法。
1.首先我们再UIViewController.view上添加一个scrollView, 在scrollView上我们再添加一个Label, 然后我们再懒加载一个定时器, 然后我们启动定时器,定时器将每隔1秒修改Label.text,代码如下:
- (void)viewDidLoad {
[self.view addSubview:self.scrollView];
[self.scrollView addSubview: self.label];
[self.timer fire];
}
- (void)on_timer {
_num += 1;
self.label.text = [NSString stringWithFormat:@"%zd", _num];
}
// 懒加载一个定时器
- (NSTimer *)timer {
if (!_timer) {
_timer = ({
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(on_timer) userInfo:nil repeats:YES];
timer;
});
}
return _timer;
}
我们运行我们的程序,我们不做任何操作,我们可以看到我们的label.text会每隔一秒钟就变化一次。如果此时我们滑动我们的scrollView,我们就会发现label.text将不会再发生改变,当我们停止滑动,手指离开屏幕的时候,label.text又开始继续发生改变。那么到底是什么原因到导致这样的结果呢?
当我们再初始化Timer的时候,没有手动将Timer注入到某种运行模式下,此时系统会默认将其注入到NSDefaultRunLoopMode
,所以我们运行程序,且不做任何操作的时候,此时RunLoop是当currentMode是NSDefaultRunLoopMode
,Timer正常工作。当我们滑动scrollView的时候,此时系统会将currentMode切换到UITrackingRunLoopMode
,我们的Timer并没有注入到这个模式下,所以Timer失效。当我们的滑动结束,手指离开屏幕的时候,currentMode又切换到NSDefaultRunLoopMode
,此时Timer又正常工作。那么我么在初始化Timer的时候,可以将其注入到指定的运行模式下吗?是可以的,代码如下:
// 手动的将Timer注入到UITrackingRunLoopMode模式下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
此时我们手动将Timer注入到UITrackingRunLoopMode
,然后我们运行程序,其实显而易见,此时我们不做任何操作,Timer是不会正常工作的,当我们滑动scrollView的时候,Timer开始正常工作,原因与上同理。这里我来问个S13问题:), 难倒我们就不能在这两种模式下让Timer都能正常工作吗?(这个问题真的S13)。
答案是当然可以的,其实我是为了引出伪模式(kCFRunLoopCommonModes
)。kCFRunLoopCommonModes
其实都不能算是一种运行模式,它只是一种标记,它可以对其他运行模型进行标记。说道这里,大家可能都明白了系统的NSDefaultRunLoopMode
和UITrackingRunLoopMode
都是被标记上Common modes。所以我们只需要将我们的timer手动注入到kCFRunLoopCommonModes
下,那么此时无论系统的currentMode是default还是UITracking,timer将都能正常工作。修改代码如下:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
这里我们说道了Timer,那么我就顺便就说说这个timer
NSTimer有两个便利构造函数入下:
// 构造函数1
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
// 构造函数2
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
构造函数1,返回的timer对象会自动注入到NSDefaultRunLoopMode,其实相当于构造函数1,再手动注入到运行模式中,例如:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(...) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
2.4 CFRunLoopSourceRef (Input事件源)
所有的事件源有两种分类方法,我们来具体看看他哪两种分类:
按照官方文档来分类(和前面我贴出的官方RunLoop模型图里一样):
Port-Based Sources :基于端口事件
Custom Input Sources:用户自定义事件按照函数调用栈来分类:
Source0:非基于端口事件
Source1:基于端口,通过内核和其他线程通信、接收、分发系统事件
其实根本一点讲,这两种分类是没有区别的,只不过呢,第一种是根据官方给出的理论来进行分类的。第二是我们在实际应用中通过函数的调用来进行分类的。
2.5 CFRunLoopObserverRef (I观察者类)
RunLoop的状态会根据用户的操作以及程序状态的变化而发生改变,通常我们会去监听RunLoop的实时变化,此时我们就会用到一个观察者类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 // 监听全部状态改变
};
下面我们用个小小的例子来观察下,RunLoop的状态到底是怎么变化的,以及我们如何使用CFRunLoopObserverRef
来监听RunLoop的状态变化。
首先我们在viewDidLoad中添加如下代码:
CFRunLoopObserverRef observerRef =
CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),
kCFRunLoopAllActivities,
YES,
0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop发生改变---%zd",activity);
});
// 2.append Observer to RunLoop
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observerRef, kCFRunLoopDefaultMode);
// 3.release observer
CFRelease(observerRef);
然后我们运行我们的程序,然后控制台会打印出一大串信息,这里我只贴出最后部分打印信息:
2018-12-25 10:36:03.293175+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---2
2018-12-25 10:36:03.293363+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---4
2018-12-25 10:36:03.293517+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---32
2018-12-25 10:37:00.032407+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---64
2018-12-25 10:37:00.036248+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---2
2018-12-25 10:37:00.036335+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---4
2018-12-25 10:37:00.036392+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---32
我们可以看到,RunLoop的状态是在不断的变化的,他最后的状态是32,也就是我们上面的枚举中的kCFRunLoopBeforeWaiting
,RunLoop即将进入休眠。
3.RunLoop的原理介绍
OK,我们上面讲解了RunLoop的基本概念,以及与RunLoop相关的几个类的介绍,接下来我们将具体的来说一说RunLoop的原理。它到底是怎么运作,希望通过下面讲解,大家都可以了解RunLoop的内部运行逻辑。
这里我贴一张我自己画的图,简单的描述了一下RunLoop的运行逻辑,如下图所示:
上图是我自己按照自己的理解画的RunLoop运行逻辑图,可能对大家在理解RunLoop的运行逻辑的时候有一点帮助,下面我们来看看官方文档对RunLoop的运行逻辑是如何阐述的。
当我们运行我们的程序的时候,所有线程的RunLoop会同时被唤醒,并且开始自动处理之前未处理完的事件,通知也发出通知,通知其相应的Observer。详细的顺序如下:
- 通知观察者RunLoop已经启动
- 通知观察者即将要开始的定时器
- 通知观察者任何即将启动的非基于端口的源
- 启动任何准备好的非基于端口的源
- 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
- 通知观察者线程进入休眠状态
- 将线程置于休眠知道任一下面的事件发生:
某一事件到达基于端口的源
定时器启动
RunLoop设置的时间已经超时
RunLoop被显示唤醒 - 通知观察者线程将被唤醒
- 处理未处理的事件
如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
如果输入源启动,传递相应的消息
如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2 - 通知观察者RunLoop结束。
以上就是官方对RunLoop的运行逻辑给出的说明,我是直接用有道翻译出来的,没有去做校验,大家可以对照我上面贴出的图,大致看下RunLoop的运行逻辑。
4.RunLoop开发应用
上面说一大堆,很多都是概念性东西,要想真正的掌握RunLoop我们还是得到实际运用中。通过在实际开发过程中的实际应用,我们对RunLoop的认识才能更加深刻。
4.1 NSTimer的应用
NSTimer,我们上面的2.3CFRunLoopTimerRef (Timer事件源)中已经提到过有关NSTimer和RunLoop的关系,这里呢,我们就不再重复了。
4.2 开启常驻线程
在我们的实际开发过程中,有时候可能会遇到在后台频繁操作的的一些需求,例如,文件现在,音乐播放,实时定位等等,这些操作经常会在子线程做一些耗时的操作,此时,我们就可以应用RunLoop将这些耗时的子线程放到我们的常驻线程中去。
具体的操作室,代码如下:
1.首先我们开启一条子线程:
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化线程,执行任务run1
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
[self.thread start];
}
- (void) run1
{
NSLog(@"----run1-----");
// 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 如果开启RunLoop,则不会执行下面这句打印,因为RunLoop开启了循环。
NSLog(@"未开启RunLoop");
}
我们运行我们的程序,控制并没有打印 "未开启RunLoop", 说明我们开启了一条常驻线程,此时我们再往线程中添加一些子任务代码如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 在self.thread的线程中执行任务run2
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void) run2
{
NSLog(@"----run2------");
}
此时我们运行我们的程序,每当我们点击我们的屏幕的时候,控制台都会打印“----run2------”,这样我们就实现了常驻线程的需求。当然这里我只是简单的给大家演示了一下常驻线程的例子,那么大家可以根据自己不同是实际开发需求,来进行编码,并将RunLoop特性发挥起来。
以上就是对RunLoop的基本概念,相关类,运行逻辑,以及实际应用的简单介绍,当然我个人是水平也是比较次,有不对的,或者不准确的地方,希望大家留言指正。