前言
RunLoop是iOS和OSX开发中非常基础的一个概念,学习Runloop能够帮助我们更清楚的了解APP为何能够持续运行。虽然在平时的工作场景中使用Runloop的机会很少,但是理解RunLoop可以帮助开发者更好的利用多线程编程。网上关于Runloop的文章千篇一律,但"一千个读者,就有一千个哈姆雷特",每个人都有自己不同的理解。
Runloop
通俗概念
- 可以从字面上理解成“运行循环”、“跑圈”,通俗一点,它就是一个死循环,相当于一个do..while;
- 正是因为这个死循环的存在,才能保持程序的持续运行,不会像一块代码,执行完了就完了;
- 有事情的时候做事情,没事情的时候就休息,充分节省了CPU的性能;
- 所谓的“事情”,实际上就是App中的各种事件(比如触摸事件、定时器事件、Selector事件);
官方解释
- Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
- Runloop是与多线程相关的、基础框架中非常重要的一部分。 Runloop是你用来调度、协调当前的到来事件的一个循环。Runloop的目的就是当有任务到来的时候保持当前线程处于繁忙状态, 当没有任务需要处理的时候让当前线程处于休眠状态。
如果没有Runloop
- 代码从上到下执行,到第三行就结束了
有了Runloop以后
- 由于main函数里面启动了RunLoop,所以程序并不会马上退出,保持持续运行状态
- 在代码main.m里都默认在主线程中启动了runloop
- 所以UIApplicationMain函数一直没有返回,保持了程序的持续运行
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
RunLoop对象
- iOS中为我们提供了两套API访问和操作runloop,一套是面向OC的,一套是基于C语言的
- Foundation下的NSRunLoop
- Core Foundation下的CFRunLoopRef
- NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)
- 更多细节我们可以查阅苹果官方文档深入学习
- 苹果官方文档
- RunLoop 官方编程手册翻译
Runloop与线程的关系
- 每条线程都有唯一的一个与之对应的RunLoop对象
- 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建(子线程的RunLoop默认是关闭的,因为有时候开了个线程但却没有必要开一个RunLoop,不然反而浪费了资源)。
- RunLoop在第一次获取时创建,在线程结束时销毁(内部实际上是一个懒加载)
获得RunLoop对象
- Foundation下的获取方法
- 获得当前线程的RunLoop对象,在子线程调用该方法相当于新创建一个runloop
- [NSRunLoop currentRunLoop];
- 获得主线程的RunLoop对象
- [NSRunLoop mainRunLoop];
- 获得当前线程的RunLoop对象,在子线程调用该方法相当于新创建一个runloop
- Core Foundation下的获取方法
- 获得当前线程的RunLoop对象,在子线程调用该方法相当于新创建一个runloop
- CFRunLoopGetCurrent();
- 获得主线程的RunLoop对象
- CFRunLoopGetMain();
- 获得当前线程的RunLoop对象,在子线程调用该方法相当于新创建一个runloop
Runloop的相关类
- 着重了解Core Foundation下的类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
- 它们的关系如图所示
- 一个runloop内部包含若干个Mode,而每个Mode下又包含了若干个timer,observer,source.
- 调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
- 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
CFRunLoopRef
- 即代表runloop本身
CFRunLoopModeRef
- 代表RunLoop的运行模式
- 系统默认注册了5个mode,前两个常用,后三个基本用不到
- kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
CFRunLoopSourceRef
- 代表事件源(输入源)
- 以port进行区分,而port可以为系统
- 分为系统方法和自定义方法
- sourse0:非基于port的,自定义方法,响应
- sourse1:基于port的,系统提供的方法
CFRunLoopTimerRef
- 基于时间的触发器
- 基本上就等效于NSTimer
CFRunLoopObserverRef
- 观察者,用于监听RunLoop的状态改变
- 监听以下几个状态
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
图解
- 从图上看就是一直在循环,然后响应对应的事件,有就处理,没有就休息。
- 在即将进入loop的时候会有一个判空操作,如果内部没有任何的source、timer、observer等待着处理,那么runloop会直接退出,所以当我们在子线程开启runloop的时候需要注意两点:
// 1.要给runloop添加一个事件,让它先跑起来再说
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 2.需要手动开启它
[[NSRunLoop currentRunLoop] run];
Runloop的应用
1.处理NSTimer滑动暂停的问题。
// 通过timerWithTimeInterval创建出来的timer,默认不会被添加到runloop,需要手动添加指定mode
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 通过timer的scheduledTimerWithTimeInterval创建出来的timer,默认被添加到runloop的NSDefaultRunLoopMode下
// 当滑动scrollView时,runloop会切换到UITrackingRunLoopMode
// 也就导致之前NSDefaultRunLoopMode下的timer暂停
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 解决方法,手动将timer添加到NSDefaultRunLoopMode下
// NSDefaultRunLoopMode表示timer既能响应UITrackingRunLoopMode,
// 也能响应NSDefaultRunLoopMode
// 相当于将timer拷贝了一份放在这两个mode下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
2.在某个mode下调用方法。
// 只在NSDefaultRunLoopMode模式下显示图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
3.更好的理解自动释放池(@autoreleasepool)
- @autoreleasepool会在runloop进入休眠前统一释放,在下一次即将进入runloop时重新创建
- 具体验证可以通过创建observer来观察
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);
4. 创建一个常驻线程
- 比如我们需要创建一个子线程,让这个线程不死,一直循环做一些事情,比如说后台不停的监控用户的网络状态,扫描文件等。
- 这时就可以为子线程创建一个runloop,让它跑起来,有事情的时候做事情,没事情的时候休息
#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
{
// 让线程不死的一种取巧做法,
// 不停的开启runloop,
// 每次runloop发现如果没有任何的port就直接退出了,
// 当我们调用touchesBegan为runloop添加了一个source时,runloop才正在跑起来了
while (1) {
[[NSRunLoop currentRunLoop] run];
}
}
- (void)run1
{
// 推荐开启常驻线程的办法
// 手动添加一个port,让它跑起来
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
- (void)test
{
NSLog(@"----------test----%@", [NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
关于runloop的面试题
1.什么是Runloop?
- 从字面意思上理解为:运行循环、跑圈;
- 其实它内部是do-while循环,在这个循环内部不断的处理各种任务(比如Timer、Sources、Observer)
- 一个线程对应一个runloop,主线程的runloop默认已经启动,子线程的runloop需要手动开启(通过调用run方法)
- runloop只能选择一个mode启动,如果当前mode中没有任何Timer、Sources、Observer。那么则直接退出runloop.
2.自动释放池什么时候释放?
- 在runloop睡眠之前释放(kCFRunLoopBeforeWaiting),在下一次跑圈的时候重新创建.
3.在开发中如何使用runloop?
- 开启一个常驻线程(即让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)
- 在子线程开启定时器
- 在子线程进行一些长期监控(比如用户的网络状态,扫描用户的文件等)
- 可以控制定时器在特定mode下运行
- 可以让某些事件(行为、任务)在特定mode下执行
- 可以添加observe监听runloop的一些状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
4.ARC下是否还需要进行内存管理?
- 需要。即便项目是ARC的情况下,针对Core Foundation下创建的对象也需要进行内存管理,因为ARC是针对OC而言的,而Core Foundation针对的是C语言.
凡是带有Create、Copy、Retain等字眼的函数,最后都要记得调用CFRelease(对象)
5.NSTimer受runloop的影响,精度上存在误差,如何解决?
- 使用GCD创建timer,创建出来的timer不受runloop的影响,不会被添加到任何mode下,精度更高.
写在最后
很久很久没有更新博客,一直忙于日常的工作,有时候学了新东西想写一写,可能发现网上早已经有了很多关于这方面的文章,于是便放弃了写下去的念头。其实,人都是有惰性的,总觉得看现成的比自己去写一写要来得快一些,但是整理知识点的过程,我们实际上也在加深一遍理解,而学习是一个不断重复的过程。对于已经掌握了相关知识的人,这种总结性文章可能毫无意义,但是对于想入门学习的人,文章能够做到浅显易懂,它就是有价值的。做了很久的开发,发现实际编码中我们真的很渺小,我们总是在搭建UI,创建model,网络请求,数据填充,搞点炫酷的动画,和产品经理撕逼。即便你会各种黑魔法,各种超能力,能够用到的机会其实并不多。所以越是基础的东西越需要打牢,有了基础才能举一反三,才能一步一步的去解决更多刁钻的需求。