前言
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
image
代码从上到下执行,到第三行就结束了
有了Runloop以后
image
由于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对象
Foundation下的获取方法
[NSRunLoop mainRunLoop];
[NSRunLoop currentRunLoop];
获得当前线程的RunLoop对象,在子线程调用该方法相当于新创建一个runloop
获得主线程的RunLoop对象
Core Foundation下的获取方法
CFRunLoopGetMain();
CFRunLoopGetCurrent();
获得当前线程的RunLoop对象,在子线程调用该方法相当于新创建一个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,让其互不影响。
image
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 };
图解
image
image
从图上看就是一直在循环,然后响应对应的事件,有就处理,没有就休息。
在即将进入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,需要手动添加指定modeNSTimer *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来观察
// 创建observerCFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { NSLog(@"----监听到RunLoop状态发生改变---%zd", activity); });// 添加观察者:监听RunLoop的状态CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);// 释放ObserverCFRelease(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,网络请求,数据填充,搞点炫酷的动画,和产品经理撕逼。即便你会各种黑魔法,各种超能力,能够用到的机会其实并不多。所以越是基础的东西越需要打牢,有了基础才能举一反三,才能一步一步的去解决更多刁钻的需求。
作者:RocKwok
链接:https://www.jianshu.com/p/f930c8bf75ec