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

一、什么是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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,348评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,122评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,936评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,427评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,467评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,785评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,931评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,696评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,141评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,483评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,625评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,291评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,892评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,492评论 2 348

推荐阅读更多精彩内容