美文网首页
06讲:RunLoop原理和实战

06讲:RunLoop原理和实战

作者: iOS寻觅者 | 来源:发表于2023-05-12 10:22 被阅读0次

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的耗时,然后再跟设定好的阀值进行比较,就可以判定主线程是否卡顿。

相关文章

网友评论

      本文标题:06讲:RunLoop原理和实战

      本文链接:https://www.haomeiwen.com/subject/jbiokdtx.html