一、什么是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会在第一次获取它时创建。
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