iOS RunLoop详解

1. RunLoop简介

1.1 什么是RUnLoop

可以理解为字面的意思:Run表示运行,Loop表示循环。结合在一起就是运行的循环。通常叫做运行循环。

RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件,UI刷新事件,定时器事件,Selector事件)从而保持程序的持续运行,而且在没有事件处理的时候,会进入休眠模式,从而节省CPU资源,提高程序性能。

1.2 RunLoop和线程

RunLoop和线程是息息相关的,我们知道线程的作用是用来执行特定的一个或多个任务,但是在默认情况下,线程执行完之后就会推出,就不能在执行任务了。这时候我们就需要采用一种方法来让线程能够处理任务,并不推出。所以我们就有了RunLoop。

一条线程对应一个RunLoop对象,每条线程都有一个唯一一个与之对应的RunLoop对象。

我们只能在当前线程中操作当前线程的RunLoop,而不能去操作其它线程的RunLoop对象。

RunLoop对象在第一次获取RunLoop时创建,销毁则在线程结束的时候。

主线程的RunLoop对象,系统自动的帮我们创建好了(程序入口函数UIApplicationMain函数 自动开启一个RunLoop,维持程序的运行)。而子线程中的RunLoop对象需要我们主动创建.

1.3 默认情况下主线程的RunLoop原理

我们在启动一个iOS程序的时候,系统会调用创建项目时自动生成的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 ;
}

从上面可以看出,程序一直在do-while循环中执行,所以UIApplicationMain函数一直没有返回,我们在运行程序之后,程序不会马上退出,会保持运行的状态.

看一下苹果官方给出的RunLoop模型图

iOS RunLoop详解

从图中可以看出,RunLoop就是线程中的一个循环,RunLoop在循环中会不断检测监听,通过input Sources(输入源)和Timer Sources(定时源)两种来源等接受事件,然后对接受到的事件通知线程进行处理,并在没有事件的时候进行休息.

input Sources :port(端口事件源) Custom(自定义事件源) performSelector(Selector事件源)

Timer Sources :(定时器事件)

以上可以总结出RunLoop的作用

1.保持程序的持续运行

2.处理App中的各种事件(比如触摸事件,定时器事件,Selector事件,UI刷新事件)

3.节省CPU资源(有事做事,没事休息)提高程序性能。

2. RunLoop 相关类

下面我们来了解一下Core Foundation框架下的关于RunLoop的5个类,只有弄懂这5个类的含义,我们才能深入的了解RunLoop运行机制。

1. CFRunloopRef: 代表RunLoop的对象

2. CFRunLoopModeRef:RunLoop的运行模式

3. CFRunLoopSourceRef: 就是RunLoop模型图中提到的输入源/事件源。

4. CFRunLoopTimerRef: 就是RunLoop模型图中提到的定时源

5. CFRunLoopObserverRef: 观察者,能够监听RunLoop的状态改变。

下面详细讲解下几种类的具体含义和关系

先来看一张这5个类的关系图

iOS RunLoop详解

这5个类的关系 同志们看这里 http://blog.ibireme.com/2015/05/18/runloop/

一个RunLoop对象(CFRunLoopRef) 中包含着若干个运行模式(CFRunloopModeRef).而每一个运行模式下又包含着若干个输入源(CFRunLoopSourceRef),定时源(CFRunLoopTimerRef),观察者(CFRunLoopObserverRef).

*每次RunLoop启动时,只能指定一个运行模式(CFRunLoopModeRef),这个运行模式,被称为(currentMode)

*如果需要切换运行模式(CFRunLoopModeRef),只能退出Loop,再重新指定一个运行模式进入。

*这样做主要是为了分割开不同组的输入源(CFRunLoopSourceRef),定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响。

2.1 CFRunLoopRef

CFRunLoopRef就是Core Foundation框架下RunLoop的对象类。我们可以通过下面的方式来获取RunLoop对象

CFRunLoopGetCurrent();//获取当前线程的RunLoop对象
CFRunLoopGetMain();//获取主线程的RunLoop对象

与其对应的在Foundation框架下获取RunLoop对象类的方法如下:

[NSRunLoop currentRunLoop]; //获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; //获取主线程的RunLoop对象

2.2 CFRunLoopModeRef

系统默认定义了多种运行模式(CFRunLoopModeRef),如下

1. KCFRunLoopDefauleMode: App的默认运行模式,通常主线程就是在这种模式下运行。

2. UITrackingRunLoopMode: 跟踪用户交互事件(用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode的影响)

3. UIInitializationRunLoopMode: 在刚启动App时进入的第一个Mode,启动完成后 就不再使用

4. GSEventReceiveRunLoopMode: 接受系统内部事件,通常用不到

5. KCFRunLoopCommonModes: 伪模式,不是一种真正的运行模式。

其中 KCFRunLoopDefaultMode UITrackingRunLoopMode, KCFRunLoopCommonModes 是我们开发中常用的模式。后面会有用法讲解.

2.3 CFRunLoopTimerRef

CFRunLoopTimerRef是定时源(RunLoop模型图中提到过),理解为基于时间的触发器,基本上就是NSTimer

下面我们来演示一下CFRunLoopModeRef 和 CFRunLoopTimerRef结合的使用方法,从而加深理解。

1.首先我们新建一个iOS项目,在Main.storyboard中拖入一个Text View。

2.在ViewController.m文件中加入以下代码

- (void)viewDidLoad {
[super viewDidLoad];
[self runLoopTimerRef];
} - (void)runLoopTimerRef {
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(@"-----run");
}

运行后 我们发现如果我们不对模拟器进行任何操作,定时器会稳定的每隔两秒调用run方法打印。

但是当我们拖动UItextView的时候,我们会发现run方法不打印了。也就是说定时器不工作了,而当我们松开鼠标的时候,定时器又开始正常工作了。

这是因为:

当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode中。

当我们拖动TextView的时候,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode的模式。这个模式下没有添加NSTimer.所以我们的定时器就不工作了。

当我们松开鼠标的时候,RunLoop就结束UITrackingRunLoopMode进入NSDefaultRunLoopMode,所以定时器又可以工作了。

你可以试着将上述代码中的[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];语句换为[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];,也就是将定时器添加到当前RunLoop的UITrackingRunLoopMode下,你就会发现定时器只会在拖动Text View的模式下工作,而不做操作的时候定时器就不工作。

正常情况下,我们需要在任何时刻,定时器都要工作的,这个时候就用到了我们上面所讲的伪模式KCFRunLoopComonModes了。这中模式其实不是一种真正的模式,而是一种标记模式,意思就是可以在打上Common Modes标记的模式下运行。

那么 哪些模式被打上了Common Modes 呢

NSDefaultRunLoopMode 和 UITrackingRunLoopMode

所以 我们只要将NSTi 么热添加到当前RunLoop的KCFRunLoopComonModes(NSRunLoopCOmmonModes)中就可以了。这样我们就可以让NSTimer在不做任何操作时 和 拖拽TextView时 都工作了。

具体做法如下

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

既然讲到了既然讲到了NSTimer,这里顺便讲下NSTimer中的scheduledTimerWithTimeInterval方法和RunLoop的关系。添加下面的代码:

[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];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

2.4 CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(就像RUnLoop模型图中的那样)

port-based Sources(基于端口)

Custom Input sources (自定义)

Cocoa perform Selector Sources

第二种按照函数调用栈来分类

Source0:非基于端口

Source1:基于Port 通过内核和其他线程通信 接收 分发系统事件。

这两种分类方式其实没有什么区别,只不过第一种是通过官方理论来分类,第二种是在实际应用中通过调用函数来分类。

下面我们举个例子大致来了解一下函数的调用栈和Source。

1. 在我们的项目中的Main.storyboard中添加一个Button按钮,并添加点击动作。

2. 然后在点击动作的代码中加入一句输出语句,并打上断点,如下图所示

iOS RunLoop详解

3. 运行程序 点击按钮

4. 然后在项目中点击下图红色部分

iOS RunLoop详解

可以看到如下图所示就是点击事件产生的函数调用栈

iOS RunLoop详解

所以点击事件是这样来的:

1.首先程序启动,调用16行的main函数,main函数调用15行UIApplicationMain函数,然后一直往上调用函数,最终调用到0行的BtnClick函数,即点击函数。

2.同时我们可以看到11行中有Sources0,也就是说我们点击事件是属于Sources0函数的,点击事件就是在Sources0中处理的。

3.至于sources1 是用来接收 分发系统事件 然后再分发到sources0中处理的。

2.5 CFRunLoopObserverRef

CFRunloopObserverRef是观察者,用来监听RunLoop的状态变化。

CFRunLoopObserverRef可以监听状态的改变有以下几种:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << ), // 即将进入Loop:1
kCFRunLoopBeforeTimers = (1UL << ), // 即将处理Timer:2
kCFRunLoopBeforeSources = (1UL << ), // 即将处理Source:4
kCFRunLoopBeforeWaiting = (1UL << ), // 即将进入休眠:32
kCFRunLoopAfterWaiting = (1UL << ), // 即将从休眠中唤醒:64
kCFRunLoopExit = (1UL << ), // 即将从Loop中退出:128
kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听全部状态改变
};

下边我们通过代码来监听RunLoop中的状态改变.

1. 在ViewController.m中添加如下代码。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self runLoopObserverShow];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(taskDemo) userInfo:nil repeats:NO];
} - (void)taskDemo {
NSLog(@"%s",__func__);
} - (void)runLoopObserverShow {
//创建观察者
/*
第一个参数:怎么分配存储空间
第二个参数:要监听的状态
第三个参数:是否持续监听
第四个参数:优先级 总是传0
第五个参数:block块,当状态改变的时候,会调用Block
*/ CFRunLoopObserverRef obserRef = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, , ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"即将进入RUnLoop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即将处理timer事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即将处理source事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即将进入休眠状态");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"即将进入唤醒状态");
break;
case kCFRunLoopExit:
NSLog(@"即将退出RunLoop");
break;
default:
NSLog(@"全部的状态");
break;
}
});
/*
第一个参数:要监听的RunLoop
第二个参数:监听者,观察者
第三个参数:运行模式
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), obserRef, kCFRunLoopDefaultMode);
//释放监听
CFRelease(obserRef);
}

看下打印情况

-- ::30.881072+ RunLoopObserver[:] 即将处理timer事件
-- ::30.881290+ RunLoopObserver[:] 即将处理source事件
-- ::30.881627+ RunLoopObserver[:] 即将处理timer事件
-- ::30.881786+ RunLoopObserver[:] 即将处理source事件
-- ::30.882054+ RunLoopObserver[:] 即将处理timer事件
-- ::30.882191+ RunLoopObserver[:] 即将处理source事件
-- ::30.882477+ RunLoopObserver[:] 即将进入休眠状态
-- ::31.061359+ RunLoopObserver[:] 即将进入唤醒状态
-- ::31.061517+ RunLoopObserver[:] 即将处理timer事件
-- ::31.061625+ RunLoopObserver[:] 即将处理source事件
-- ::31.061956+ RunLoopObserver[:] 即将处理timer事件
-- ::31.062073+ RunLoopObserver[:] 即将处理source事件
-- ::31.062193+ RunLoopObserver[:] 即将进入休眠状态
-- ::32.881640+ RunLoopObserver[:] 即将进入唤醒状态
-- ::32.881867+ RunLoopObserver[:] -[ViewController taskDemo]
-- ::32.882027+ RunLoopObserver[:] 即将处理timer事件
-- ::32.882155+ RunLoopObserver[:] 即将处理source事件
-- ::32.882306+ RunLoopObserver[:] 即将进入休眠状态

可以看到RunLoop在被唤醒后会处理 timer事件 source事件 处理完毕后,进入休眠状态,程序运行期间,每个线程的RunLoop不断的在这些状态之间切换,处理事件。保证程序运行。

3. RunLoop 原理

现在5个类我们都已经大致了解了,那么我们就可以看一下RUnLoop的实现逻辑了。

iOS RunLoop详解

这张图就是RunLoop的运行逻辑了。在每次运行开启RunLoop的时候,所在线程的RunLoop会自动处理之前未处理的事情。并通知相关的观察者。

具体顺序如下:

1. 通知观察者RunLoop已经启动

2. 通知观察者即将要开启的定时器(处理timer事件)

3. 通知观察者任何即将开启的非基于端口的源(sources0) | 将要处理sources0事件

4. 启动任何准备好的非基于端口的源(sources0) 处理sources0 事件

5. 如果基于端口的源(sources1)准备好并且处于等待状态,立即启动;并进入步骤9(sources1 分发timer事件和sources0事件)

6. 如果处理完timer和sources0 没有发现sources1事件,就通知线程进入休眠状态。

7. 将线程置于休眠直到任一下面的事件发生

(1) 某一事件到达基于端口的源(sources1分发timer和sources0)

  (2) 定时器启动

(3) RunLoop设置的时间已经超时

(4) RunLoop被显示唤醒

8. 通知观察者线程将被唤醒

9. 处理未处理的事件

(1) 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2

(2) 如果输入源启动,传递相应的消息

(3) 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2

10. 通知观察者RunLoop结束。

4. RunLoop实战应用

RunLoop在实战中的应用,我们可以看一下。

4.1 NSTimer的使用

上面已经讲过,可查看2.3

4.2 ImageView 推迟显示

有时候,我们会遇到这种情况:
当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这时候当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能会出现卡顿的现象。

怎么解决这个问题呢?

这时候,我们应该推迟图片的显示,也就是ImageView推迟显示图片。有两种方法:

1. 监听UIScrollView的滚动

因为UITableView继承自UIScrollView,所以我们可以通过监听UIScrollView的滚动,实现UIScrollView相关delegate即可。

2. 利用PerformSelector设置当前线程的RunLoop的运行模式

利用performSelector方法为UIImageView调用setImage:方法,并利用inModes将其设置为RunLoop下NSDefaultRunLoopMode运行模式。代码如下:

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:NSDefaultRunLoopMode];

下边利用Demo演示一下该方法。

1. 在项目中的Main.storyboard中添加一个UIImageView,并添加属性,并简单添加一下约束(不然无法显示)如下图所示。

iOS RunLoop详解

2.随便找一张图片 然后我们在touchesBegan方法中添加下面的代码

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:@[NSDefaultRunLoopMode]];
}

这样我们就实现了在拖动完之后,在延迟显示UIImageView。

4.3 后台常驻线程(很常用)

我们在开发应用程序的时候,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存

添加一条用于常驻内存的强引用的子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop

具体实现过程如下

- (void)viewDidLoad {
[super viewDidLoad];
[self addBackgroundThread];
} - (void)addBackgroundThread {
//创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
[thread start];
self.thread = thread;
} - (void)run1 {
//这里写任务
NSLog(@"----run1----");
// 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"未开启RunLoop");
}

运行之后发现打印了----run1---- ,而未开启RUnLoop则没有打印。

我们开启了一条常驻线程,下面我们来试着添加其他任务,除了之前创建的时候调用了run1方法,我们另外在点击的时候调用run2方法。

那么,我们在touchesBegan中调用PerformSelector,从而实现在点击屏幕的时候调用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------");
}

经过运行测试,除了之前打印的----run1-----,每当我们点击屏幕,都能调用----run2------
这样我们就实现了常驻线程的需求。

上一篇:python爬虫解析库学习


下一篇:IOS AFNetworking简介及使用