RunLoop深入了解及常驻线程组件开发

一、什么是RunLoop

1、概念:运行循环,在程序运行过程中,循环的做一些事,实质就是一个do while()循环。

2、应用范畴:NSTimer、perfermSelector、GCDAsyncMainQueue、事件响应、手势识别、UI界面刷新、网络请求、AutoreleasePool等。

3、如果没有RunLoop我们的App是怎么样呢?如果没有RunLoop我的app程序在main函数执行完log就即将退出程序。


RunLoop深入了解及常驻线程组件开发_第1张图片

4、如果有RunLoop,则程序不会立即退出,而是保持运行状态。


RunLoop深入了解及常驻线程组件开发_第2张图片

5、RunLoop的作用就是:

     1)保持程序的持续运行

     2)处理app中的各种事件,比如触摸事件,Timer计时器等。

     3)可以节省CPU资源,提高程序性能,有事做就做事,无事做就休眠。(休眠:mach_msg函数,切换到内核态)

二、RunLoop休眠原理简介

1、RunLoop最核心的事情:保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。通过用户态与内核态的切换来实现休眠。RunLoop的这个机制是依靠系统内核来完成的,具体来说是苹果操作系统核心组件Darwin中的Mach来完成的(Darwin是开源的:https://opensource.apple.com/tarballs/可以里面找下载了解)。

2、Mach是内核的核心,提供了进程间通信(IPC)、处理器调度等基础服务。在Mach中,进程、线程间的通信是以消息的方式来完成的,消息在两个Port之间进行传递(Source1事件就是依靠系统发送消息到指定的Port来触发的)。


RunLoop深入了解及常驻线程组件开发_第3张图片
用户态与内核态的切换流程如上图

3、使RunLoop休眠的重要函数


RunLoop深入了解及常驻线程组件开发_第4张图片
核心函数

mach_msg()会触发内核状态切换。当程序静止时,RunLoop停留在__CFRunLoopServiceMachPort(waitSet, &msg,sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而这个函数内部就是调用了mach_msg()让程序处于休眠状态。

三、RunLoop的API

1、Ios有两套API来访问使用RunLoop

--Foundation:NSRunLoop

   --CoreFoundation:CFRunLoopRef

   NSRunLoop是基于CFRunLoopRef的一层OC包装。

   CFRunLoopRef开源源码下载:https://opensource.apple.com/tarballs/CF/

四、RunLoop与线程

1、RunLoop与线程是一对一的关系。每条线程可以有一个与之对应的RunLoop对象。

2、RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。

3、线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建。


RunLoop深入了解及常驻线程组件开发_第5张图片
获取RunLoop对象的底层源码

4、RunLoop会在线程结束时销毁。

5、主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。

五、RunLoop对象的获取


RunLoop深入了解及常驻线程组件开发_第6张图片

六、RunLoop相关类

Core Foundation中关于RunLoop的5个

1、CFRunLoopRef

2、CFRunLoopModeRef

3、CFRunLoopSourceRef

4、CFRunLoopTimerRef

5、CFRunLoopObserverRef

七、RunLoop、RunLoopMode结构组成

1、RunLoop结构


RunLoop深入了解及常驻线程组件开发_第7张图片

RunLoop在CF层结构体组成,有线程指针,mode集合等。

问题:既然RunLoop与线程是一对一关系,那么此处RunLoop里面有_pthread_t线程指针,是否构成相互引用呢? 

答案:不会,因为Loop对象与线程只是value与key的对应关系,不存在相互持有。

2、RunLoopMode结构


RunLoop深入了解及常驻线程组件开发_第8张图片

1)RunLoopMode代表RunLoop的运行模式

2)一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer

3)RunLoop启动时只能选择其中一个Mode,作为currentMode,当前运行的模式。

4)如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入。

5)不同Mode下的Source0/Source1/Timer/Observer能分隔开来,互不影响。

6)如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会休眠或者立马退出。

八、RunLoop的CurrentMode如何获取

CFRunLoopCopyCurrentMode(CFRunLoopRefrl);获取当前RunLoop运行的Mode的Name


RunLoop深入了解及常驻线程组件开发_第9张图片

九、RunLoopMode的5种类

1、kCFRynLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行。

2、UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。

3、kCFRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode。带 有 common modes 标 记 的 模 式 有 UITrackingRunLoopMode和 kCFRunLoopDefaultMode。

4、UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后不再使用。

5、GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到。

说明:主要我们时常用到的就是kCFRynLoopDefaultMode与UITrackingRunLoopMode

十、RunLoop运行逻辑


RunLoop深入了解及常驻线程组件开发_第10张图片
RunLoop深入了解及常驻线程组件开发_第11张图片
RunLoop深入了解及常驻线程组件开发_第12张图片

逻辑顺序:

(1)doSource0

---触摸事件处理

---pperformSelector:/onThread:

(2)doSource1

--基于Port的线程间通信

--系统事件捕捉

(3)doTimers

--NSTimer

--performSelector:withObject:afterDelay:

(4)doObservers

--用于监听RunLoop的状态

--UI刷新(BeforeWaiting)

--Autoreleasepool(BeforeWaiting)

1、RunLoop只能运行在其中一个mode模式下,如果要运行另外的mode,需要退出当前mode然后再次进入需要运行的mode。

2、每个mode里面有自己独立的source0/source1/observers/timers。运行在某个mode下只能处理mode里面对应的事件源。

十一、CFRunLoopObserverRef与RunLoopActivity状态


RunLoop深入了解及常驻线程组件开发_第13张图片

1、CFRunLoopObserver创建的2种方式

•CFRunLoopObserverCreate

•CFRunLoopObserverCreateWithHandler

2、CFRunLoopObserverCreate注意点

  函数:CFRunLoopObserverCreate(CFAllocatorRefallocator, CFOptionFlagsactivities, Boolean repeats, CFIndexorder, CFRunLoopObserverCallBackcallout, CFRunLoopObserverContext*context) ;

其中的context最好不要为NULL,如果为空,可能出现莫名的报错,最好做个上下文初始化。

CFRunLoopObserverContext context={0};//结构体初始化

因为函数局部变量如果没有初始化,可能存放的是一些乱七八糟的初始值,有可能当前的函数调用栈的空间是上个函数遗留的。例如:


3、CFRunLoopObserverCreateWithHandler使用


RunLoop深入了解及常驻线程组件开发_第14张图片

4、CFRunLoopRunResult结构


RunLoop深入了解及常驻线程组件开发_第15张图片

十二、RunLoop在实际开中的应用

1、控制线程生命周期(线程保活)

2、解决NSTimer在滑动时停止工作的问题

3、监控应用卡顿

4、性能优化

十三、利用RunLoop技术创建一个可控生命周期常驻线程。

1、大体创建常驻线程及停止的代码核心如下:

NSRunLoop* loop = [NSRunLoopcurrentRunLoop];

[loopaddPort:[NSPortport] forMode:NSDefaultRunLoopMode];

[loop run];//开启runLoop

CFRunLoopStop(loop);//停止Loop,没有NS开头相关的Foundation下的API,只有CF层的API,CFRunLoopStop函数使得loop停止。

上面代码存在的问题:

1、为什么不加[loop addPort:[NSPortport] forMode:NSDefaultRunLoopMode];直接[loop run]不会启动loop而是直接退出?


RunLoop深入了解及常驻线程组件开发_第16张图片

   [loop run]的底层核心代码就是执行上面的CFRunLoopRunSpecific,如果没有加入port端口等事件源,那么__CFRunLoopFindMode找到的currentMode=NULL,所有直接就return退出了,不会启动runLoop。因此如果要构建一个常住线程启动runLoop,必须加入事件源才能run起来。


2、[runLoop run];跑起的RunLoop为什么CFRunLoopStop()停止不了?


RunLoop深入了解及常驻线程组件开发_第17张图片

  上面运行截图看出,loopStop只是停止了一次loop,然后里面loop又run起来了,说明[runLoop run]根本没停掉。那么[runLoop run]到底怎么回事呢?看下嘛文档截图说明:


RunLoop深入了解及常驻线程组件开发_第18张图片

从文档可看出开启的是一个无限循环。不停的调用runMode:beforeDate方法开启loop。所以当我们调用__CFRunloopStop()的时候只是停止了其中一次loop。然后由于开启的是无限调用runMode:beforeDate方法,从而又开启了loop。相当于一个do{}whlie(YES)死循环。

从GNUStep里面下载Foundation源码佐证:

源码下载地址:http://wwwmain.gnustep.org/resources/downloads.php?site=ftp%3A%2F%2Fftp.gnustep.org%2Fpub%2Fgnustep%2F

run方法其实调用的是runUntilDate:theFuture,theFuture是[NSDate distantFuture]


RunLoop深入了解及常驻线程组件开发_第19张图片


RunLoop深入了解及常驻线程组件开发_第20张图片

总结:要开启一个可控的loop生命周期,就不能使用run()来开启loop,而是要用runMode:beforeDate方法。我做了个Demo(https://github.com/harrywater/ThreadKeepAlive.git)如果利用CF层的CFRunLoopRun()函数来启动,则不会出现这种情况,

可以达到目的。这也是两则的区别,也就是说[NSRunLooprun]内部实现是跟CFRunLoopRun没有关系,相互独立的,虽然很多NSRunLoop的方法底层核心就是对应的CF层相应函数。也可以使用CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);启动一个可控生命周期的常住线程。

3、[runLooprun]与CFRunLoopRun()有区别吗?

[NSRunLooprun]内部实现是跟CFRunLoopRun没有关系,相互独立的,虽然很多NSRunLoop的方法底层核心就是对应的CF层相应函数。也可以使用CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);启动一个可控生命周期的常住线程。

4、__CFRunLoop结构体里面的_modes表示的是loop运行的mode集合,那么这个set里面的元素个数会随着addSource:forMode:的方法加入事件源,相应增加吗?这个集合的count数是多少?


RunLoop深入了解及常驻线程组件开发_第21张图片

利用CFRunLoopCopyAllModes()函数得到主线程所有的Modes打印。在app内操作,不管我们如何滑动scrolView或者其他事件加入处理,主线程的RunLoopMode打印结果,可以看出modes里面的Mode并不是随着Mode切换及加入新的事件源而增加多个mode。而是在固定的几个mode下面运行。事件源也是加入在这固定的几个mode里面。

十四、构建一个常驻可控生命周期的线程组件

核心代码就下面这个:

HPAliveThread.h文件

/**可控生命的线程 **/

#import

@interface HPAliveThread : NSObject

//处理线程任务

- (void)doTask:(void(^)(void))task;

//停止

- (void)stop;

@end

HPAliveThread.m文件

#import "HPAliveThread.h"

@interface HPAliveThread()

@property(nonatomic,strong)NSThread* thread;

//@property(nonatomic,assign)BOOL isStoped;

@end

@implementation HPAliveThread

//观察runLoop状态

void abserverRunLoopActivityFun(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)

{

switch (activity) {

case kCFRunLoopEntry:

NSLog(@"kCFRunLoopEntry");

break;

case kCFRunLoopExit:

NSLog(@"kCFRunLoopExit");

break;

default:

break;

}

}

- (instancetype)init

{

self = [super init];

if (self) {

//        self.isStoped = NO;

//        __weak typeof(self)weakSelf = self;

_thread = [[NSThread alloc]initWithBlock:^{

//创建一个观察者

CFRunLoopObserverContext observerContext = {0};

CFRunLoopObserverRef abserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopEntry|kCFRunLoopExit, YES, 0, abserverRunLoopActivityFun, &observerContext);

//添加观察者

CFRunLoopAddObserver(CFRunLoopGetCurrent(), abserver, kCFRunLoopDefaultMode);

CFRelease(abserver);

//开启runLoop  这种方式需要配合一个外部isStop及do while(...)来做停止跟开启RunLoop

//            NSRunLoop* loop = [NSRunLoop currentRunLoop];

//            [loop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

//

//            while (weakSelf && !weakSelf.isStoped) {

//                [loop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

//            }

//===================推荐使用===============

//简易代码 开启runLoop

//不能为NULL,需要初始化,如果不初始化可能由于函数调用栈空间是之前其他函数留下,可能会存一些糟数据,如果为NULL也可能出现报错

CFRunLoopSourceContext sourceContext  ={0};

CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &sourceContext);

CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

CFRunLoopRun();

//CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);

//========================================

}];

}

return self;

}

#pragma mark --public method

- (void)doTask:(void(^)(void))task

{

if (!self.thread.executing && task) {

[self.thread start];

}

[self performSelector:@selector(__innerThreadTask:) onThread:self.thread withObject:task waitUntilDone:NO];

}

- (void)stop

{

if (!self.thread) return;

[self performSelector:@selector(__stop) onThread:self.thread withObject:nil waitUntilDone:YES];

}

- (void)dealloc

{

NSLog(@"%s",__func__);

[self stop];//销毁线程

}

#pragma mark --private method

- (void)__stop

{

//    self.isStoped = YES;

//停止runLoop 线程保活结束

CFRunLoopStop(CFRunLoopGetCurrent());

self.thread = nil;

}

- (void)__innerThreadTask:(void(^)(void))task

{

//执行task

task();

}

@end

构造一个HPAliveThread类,继承NSObject,然后里面包含一个NSThread对象,在这里说下为什么HPAliveThread不继承NSThread呢?因为如果继承NSThread,很多父类的公有方法都会可以使用,这个增加了很多HPAliveThread操作的不确定性,所以让其继承NSObject,只给外界提供我想提供的方法即可。

常住可控生命周期线程组件下载地址:https://github.com/harrywater/HPAliveThread.git

你可能感兴趣的:(RunLoop深入了解及常驻线程组件开发)