美文网首页
RunLoop深入了解及常驻线程组件开发

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

作者: 王的for | 来源:发表于2018-08-02 19:18 被阅读0次

    一、什么是RunLoop

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

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

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

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

    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来触发的)。

    用户态与内核态的切换流程如上图

    3、使RunLoop休眠的重要函数

    核心函数

    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对象的底层源码

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

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

    五、RunLoop对象的获取

    六、RunLoop相关类

    Core Foundation中关于RunLoop的5个

    1、CFRunLoopRef

    2、CFRunLoopModeRef

    3、CFRunLoopSourceRef

    4、CFRunLoopTimerRef

    5、CFRunLoopObserverRef

    七、RunLoop、RunLoopMode结构组成

    1、RunLoop结构

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

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

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

    2、RunLoopMode结构

    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

    九、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运行逻辑

    逻辑顺序:

    (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状态

    1、CFRunLoopObserver创建的2种方式

    •CFRunLoopObserverCreate

    •CFRunLoopObserverCreateWithHandler

    2、CFRunLoopObserverCreate注意点

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

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

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

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

    3、CFRunLoopObserverCreateWithHandler使用

    4、CFRunLoopRunResult结构

    十二、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而是直接退出?

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

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

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

    从文档可看出开启的是一个无限循环。不停的调用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]

    总结:要开启一个可控的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数是多少?

    利用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深入了解及常驻线程组件开发

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