RunLoop 又叫运行循环,内部就是一个 do- while 循环,在这个循环内部不断处理各种任务,保证程序持续运行。RunLoop存在的目的就是当线程中有任务的时候,保证线程干活,当线程没有任务的时候,让线程睡眠,提高程序性能,节省资源,该做事的时候做事,该休息的时候休息。
RunLoop 作用:
1.保持线程持续的运行
App启动时,在函数入口main方法里的UIApplicationMain中:UIApplicationMain函数内部帮开启了主线程的RunLoop,内部拥有一个无限循环的代码(可以理解成do...while),这样UIApplicationMain函数就不会立刻返回,只要程序不退出/崩溃,就一直循环。保证线程不被销毁,主线程不销毁,程序就会持续运行。
2.应用场景:
事件响应、手势识别、界面刷新、AutoreleasePool自动释放池、NSTimer、PerformSelecter、GCD、网络请求底层等都用到了RunLoop
RunLoop 原理:
RunLoop是线程接收和分发的一种实现,线程关联的基本基础结构的一部分,一个runloop是一个事件处理循环。RunLoop与线程是一一对应关系,一个线程对应一个RunLoop,他们的映射存储在一个字典里,key为线程,value为RunLoop。
runloop的结构

由图可以看出:
RunLoop运行在线程中,接收Input Source 和 Timer Source并且进行处理。
Input Source 和 Timer Source,两个都是 RunLoop 事件的来源
一、Input Source 可以分为三类:
1.Port-Based Sources,系统底层的 Port 事件,例如 CFSocketRef
2. Custom Input Sources,用户手动创建的 Source
3. Cocoa Perform Selector Sources, Cocoa 提供的 performSelector 系列方法,也是一种事件源
二、Timer Source指定时器事件,该事件的优先级是最低的,如上图
优先级: Port > Custom > performSelector > Timer
Input Source异步投递事件到线程中,Timer Source同步投递事件到线程中。
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
RunLoop的Mode包含五种,分别是:
1. kCFRunLoopDefaultMode:默认Mode,主线程通常在这个Mode下运行。
2. UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
3. UIInitializationRunLoopMode: 启动App时第进入的第一个Mode,启动完成后不再使用,会切换到kCFRunLoopDefaultMode。
4. GSEventReceiveRunLoopMode: 接受系统事件的内部Mode,通常用不到。
5. kCFRunLoopCommonModes: 占位用Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode。
经常会用到的是NSDefaultRunLoopMode、NSEventTrackingRunLoopMode、NSRunLoopCommonModes实际开发中需要用runloop mode时,指定kCFRunLoopCommonModes就行,对于defaultMode与trackingMode来回切很麻烦。
Source有两种类型:Source0 和 Source1。
Source1 (基于port),基于mach_Port的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(iOS里进程间通信开发过程中我们一般不主动使用)。mach_port大家就理解成进程间相互发送消息的一种机制就好, 比如屏幕点击, 网络数据的传输都会触发sourse1。
Source0 (非基于port),非基于Port的 处理事件,什么叫非基于Port的呢?就是说你这个消息不是其他进程或者内核直接发送给你的。(使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。)一般是APP内部的事件, 比如hitTest:withEvent的处理, performSelectors的事件.
执行performSelectors方法,假如你在主线程performSelectors一个任务到子线程,这时候就是在代码中发送事件到子线程的runloop,这时候如果子线程开启了runloop就会执行该任务,注意该performSelector方法只有在你子线程开启runloop才能执行,如果你没有在子线程中开启runloop,那么该操作会无法执行并崩溃。
举个简单例子:一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会被IOKit先包装成Event,通过mach_Port传给正在活跃的APP , Event先告诉source1(mach_port),source1唤醒RunLoop, 然后将事件Event分发给source0,然后由source0来处理。如果没有事件,也没有timer,则runloop就会睡眠, 如果有,则runloop就会被唤醒,然后跑一圈。
RunLoop Observers
通过CFRunLoopAddObserver监控RunLoop的状态。RunLoop的状态如下:
typedefCF_OPTIONS(CFOptionFlags,CFRunLoopActivity){kCFRunLoopEntry=(1UL<<0),// 即将进入LoopkCFRunLoopBeforeTimers=(1UL<<1),// 即将处理 TimerkCFRunLoopBeforeSources=(1UL<<2),// 即将处理 SourcekCFRunLoopBeforeWaiting=(1UL<<5),// 即将进入休眠kCFRunLoopAfterWaiting=(1UL<<6),// 刚从休眠中唤醒,但是还没完全处理完事件kCFRunLoopExit=(1UL<<7),// 即将退出Loop}
我们可以通过Observer来监控主线程的卡顿。
RunLoop处理事件顺序
RunLoop内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。RunLoop t通过调用mach_msg函数进入休眠等待唤醒状态。

实战--------日常开发中我们常用的RunLoop场景有:
线程保活
Timer相关
APP卡顿检测
线程保活
首先我们应该达成的共识就是,在日常的开发过程中,线程是我们避不开的话题。比如为了不影响主线程UI的刷新,对数据的加载和解析往往会放在子线程里面去做。
有时候我们总会有一些特殊的需求,比如我们需要做一个心跳包,来保持与服务器的通讯。当然,这种数据的传输一定是放在子线程中的,那么问题就来了,子线程的任务在执行完毕之后,子线程就会被销毁。我们怎么来保证子线程不被销毁呢?
第一点,我们先来验证,子线程的销毁。
首先我们创建一个类继承自NSThread:

然后我们创建子线程并添加延时任务:

直接结果如下:

可以看到,在子线程任务结束的时候,子线程立马就被销毁了。
第二点、常驻子线程
这个时候,可能有人会想,既然要持续的在子线程中执行任务,那么直接定义一个计时器,不断的去开辟子线程不就可以了吗?
首先这种想法肯定是错误的,因为频繁的开启和销毁线程,会造成资源浪费,也会增加CUP负担。这个时候,我们就可以利用RunLoop来让线程常驻,不被销毁。
代码如下:

输出结果如下:

可以看到,我们再没有从新开启子线程的情况先,就做到了线程常驻。其实这样的需要还有很多应用场景,比如音乐播放,后台下载等等。
在使用的过程中有几点需要注意:
1、获取RunLoop只能使用[NSRunLoop CurrentRunLoop]/[NSRunLoop mainRunLoop]。
2、即使RunLoop开始运行,如果RunLoop中的modes为空,或者要执行的mode里面没有item,那么RunLoop会直接在当前Loop中返回,并进入睡眠状态。(可以参看iOS底层探索 --- RunLoop(概念))
3、自己创建的Thread的任务是在KCFRunLoopDefualtMode这个mode中执行的。
4、在子线程创建好之后,最好所有的任务都放在AutoreleasePool中执行。(这一点我们之后单独对AutoreleasePool进行探讨)。
Timer相关
我们常见的情况就是TableView与NSTimer的关系了。比如我们在当前线程中添加了一个计时器,定时打印一些信息(当然也可以是其他事情)。我们会发现,但我们去滑动TableView的时候,计时器的任务不会执行。
我们在日常开发中常用的NSTimer使用方法如下:

上面这三种方法创建的timer都是在NSDefaultRunLoopMode模式下面运行的,因为前两种没有指定mode,默认是NSDefaultRunLoopMode。
这就要引出另外一个问题,既然我们正常创建的timer默认是在NSDefaultRunLoopMode下的,那么我们上面讲到的问题,当tableView被滑动的时候,timer不再执行,那是不是说tableView的滑动,改变了当前线程的RunLoop的mode?
下面我们在tableView滑动的时候,打印一下当前的mode

打印结果如下:

通过打印结果可以看到,当刚开始创建的时候,mode为kCFRunLoopDefaultMode,但是当我们滑动tableView的时候,mode就变成了UITrackingRunLoopMode。
注意,我们上面创建的timer都是添加在kCFRunLoopDefaultMode里面。
既然找到了原因,那么修改起来也是很方便的。这里可能就有人要问,两个不同的mode,而且,要保证timer在两种mode下都能正常运行,要怎么去设置。其实我们只需要设置NSRunLoopCommonModes就可以了。(有疑问的可以阅读iOS底层探索 --- RunLoop(概念))。
其实要解决这个问题,还有另一种办法,那就会是在子线程中添加timer。因为RunLoop和线程是一一对应的。所以,当其他线程的RunLoop发成改变的时候,并不会影响timer所在线程的RunLoop。
APP卡顿检测
我们可以通过获取kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的状态,来判断是否有卡顿。
这是因为,我们运行中的主要业务处理,就是在第4步到第9步之间运行的:

这里我们设置一个监听的单例J_MonitorObject

#import "J_MonitorObject.h"
@interface J_MonitorObject ()
@property (nonatomic, strong) NSThread *monitorThread; ///监控线程
@property (nonatomic, assign) CFRunLoopObserverRef observer; ///观察者
@property (nonatomic, assign) CFRunLoopTimerRef timer; ///定时器
@property (nonatomic, strong) NSDate *startDate; ///开始执行的时间
@property (nonatomic, assign) BOOL excuting; ///执行中
@property (nonatomic, assign) NSTimeInterval interval; ///定时器间隔时间
@property (nonatomic, assign) NSTimeInterval fault; ///卡顿的阀值
@end
@implementation J_MonitorObject
static J_MonitorObject *instance = nil;
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[J_MonitorObject alloc] init];
instance.monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitorThreadEntryPoint) object:nil];
[instance.monitorThread start];
});
return instance;
}
- (void)startWithInterval:(NSTimeInterval)interval WithFault:(NSTimeInterval)fault {
_interval = interval;
_fault = fault;
///如果已经再监听,就不再创建监听器
if(_observer) {
return;
}
//1、创建Observer
///设置RunLoopObserver的运行环境
CFRunLoopObserverContext context = {0,(__bridge void*)self, NULL, NULL, NULL};
///第一个参数:用于分配Observer对象的内存
///第二个参数:用于设置Observer所要关注的事件,也就是runLoopObserverCallBack中的那些事件
///第三个参数:用于标识该Observer是在第一次进入RunLoop时执行,还是每次进入进入RunLoop处理时都执行。如果为NO,则在调用一次之后该Observer就无效了。如果为YES,则可以多次重复调用。
///第四个参数:用于设置该Observer的优先级
///第五个参数:用于设置该Observer的回调函数
///第六个参数:用于设置该Observer的运行环境
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities,YES,0, &runLoopObserverCallBack, &context);
//2、将obse添加到主线程的RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
//3、创建一个timer,并添加到子线程的RunLoop中
[selfperformSelector:@selector(addTimerToMonitorThread) onThread:_monitorThread withObject:nilwaitUntilDone:NOmodes:@[NSRunLoopCommonModes]];
}
- (void)start {
///默认定时器间隔时间是1秒,阀值是2秒
[self startWithInterval:1.0 WithFault:2.0];
}
- (void)end {
if(_observer) {
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
_observer =NULL;
}
[selfperformSelector:@selector(removeMonitorTimer) onThread:_monitorThread withObject:nilwaitUntilDone:NOmodes:@[NSRunLoopCommonModes]];
}
#pragma mark- 监控
///TODO: - 开启RunLoop
+ (void)monitorThreadEntryPoint {
@autoreleasepool {
[[NSThread currentThread] setName:@"J_Monitor"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoopaddPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLooprun];
}
}
///TODO: - observer回调函数
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
J_MonitorObject *monitor = (__bridge J_MonitorObject*)info;
switch(activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
monitor.startDate = [NSDatedate];
monitor.excuting =YES;
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
monitor.excuting =NO;
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
}
///TODO: - 在监控线程里面添加计时器
- (void)addTimerToMonitorThread {
if(_timer) {
return;
}
//1、创建timer
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, (__bridge void*)self, NULL, NULL, NULL};
_timer = CFRunLoopTimerCreate(kCFAllocatorDefault,0.1, _interval,0,0, &runLoopTimerCallBack, &context);
//2、添加子线程到RunLoop中
CFRunLoopAddTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);
}
///TODO: - 计时器回到函数
static void runLoopTimerCallBack(CFRunLoopTimerRef timer, void *info) {
J_MonitorObject *monitor = (__bridge J_MonitorObject*)info;
//如果为NO,说明进入休眠状态,此时不会有卡顿
if(!monitor.excuting) {
return;
}
//每个一段时间,检查一下是RunLoop是否在执行,然后检测一下当前循环执行的时长
//如果主线程正在执行任务,并且这一次 loop 执行到现在还没有执行完,那就需要计算时间差
NSTimeInterval excuteTime = [[NSDate date] timeIntervalSinceDate:monitor.startDate];
NSLog(@"定时器 --- %@", [NSThread currentThread]);
NSLog(@"主线程执行了 --- %f秒", excuteTime);
//跟阀值做比较,看一下是否卡顿
if(excuteTime >= monitor.fault) {
NSLog(@"线程卡顿了 --- %f秒 ---", excuteTime);
[monitorhandleStackInfo];
}
}
///TODO: - 将卡顿信息上传给服务器
- (void)handleStackInfo {
NSLog(@"将卡顿信息上传给服务器");
}
///TODO: - 移除计时器
- (void)removeMonitorTimer {
if(_timer) {
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
CFRunLoopRemoveTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);
CFRelease(_timer);
_timer =NULL;
}
}
监听卡顿的一个主要流程是:
i:创建一个子线程,并在当前线程中创建RunLoop
ii:在子线程的RunLoop中添加计时器(时间间隔自定)
iii:创建Observer,监听主线程RunLoop的状态
iiii:根据上面的分析,我们只需要在计算kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting的耗时,然后再跟设定好的阀值进行比较,就可以判定主线程是否卡顿。

网友评论