RunLoop的本质
RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象
没有消息需要处理时,休眠以避免资源占用,状态切换是从用户态通过系统调用切换到内核态
有消息处理时,立刻被唤醒,状态切换是从内核态通过系统调用切换到用户态
这里有一个问题,我们应用程序中的main函数为什么可以保持无退出呢
实际上呢,在我们的main函数中会调用UIApplicationMain函数,在这个函数中会启动一个运行循环(也就是我们所说的RunLoop),在这个运行循环中可以处理很多事件,例如屏幕的点击,滑动列表,或者网络请求的返回等等,在处理完事件之后,会进入等待,在这个循环中,并不是一个单纯的for循环或者while循环,而是从用户态到内核态的切换,以及再从内核态到用户态的切换,这里面的等待也不等于死循环,这里面最重要的是状态的切换
RunLoop的数据结构
在OC中,系统为我们提供了两个RunLoop,一个是CFRunLoop,另一个是NSRunLoop,而NSRunLoop是对CFRunLoop的一个封装,提供了面向对象的API,并且它们也分别属于不同的框架,NSRunLoop是属于Foundation框架,而CFRunLoop是属于Core Foundation框架
关于RunLoop的数据结构主要有三种:
CFRunLoop
CFRunLoopMode
Source/Timer/Observer
pthread:代表的是线程,RunLoop与线程的关系是一一对应的
currentMode:是一个CFRunLoopMode这样一个数据结构
modes:是一个包含CFRunLoopMode类型的集合(NSMutableSet<CFRunLoopMode*>)
commonModes:是一个包含NSString类型的集合(NSMutableSet<NSString*>)
commonModeItems:也是一个集合,在这个集合中包含多个元素,其中包括多个Observer,多个Timer,多个Source
name:名称,例如NSDefaultRunLoopMode,所以说是通过这样一个名称来切换对应的模式,例如在上面的commonModes里面都是名称字符串,也就是说通过这些名称来支持多种模式
source0:集合类型的数据结构
source1:集合类型的数据结构
obsevers:数组类型的数据结构
timers:数组类型的数据结构
CFRunLoopSource
source0:需要手动唤醒线程
source1:具备唤醒线程的能力
CFRunLoopTimer
和NSTimer是toll-free bridge的(免费桥转换)
CFRunLoopObserver
我们可以通过注册一些Observer来实现对RunLoop相关时间点的观测
可以观测的时间点包括:
kCFRunLoopEntry:RunLoop的入口时机,RunLoop将要启动的时候的回调通知
kCFRunLoopBeforeTimers:RunLoop将要处理Timer事件的时候
kCFRunLoopBeforeSources:RunLoop将要处理Source事件的时候
kCFRunLoopBeforeWaiting:RunLoop将要进入休眠的时候,将要进行用户态到内核态的切换
kCFRunLoopAfterWaiting:RunLoop将要进入唤醒的时候,内核态到用户态的切换后不久
kCFRunLoopExit:RunLoop退出的时候
RunLoop的mode
在RunLoop中,假如在mode1中运行,那么在mode2中事件的回调就会接收不到,RunLoop只接受在当前mode中的回调,那么这里有一个经典问题,当我们在滑动列表时,为什么会出现cell上的定时器停止的情况以及如何解决
因为在列表滑动的时候当前RunLoop的mode从Default切换到了Tracking,所以导致原来mode中的事件回调接收不到,想要解决便可将其加入commonModes中,下面我们来看一下commonMode
CommonMode的特殊性
CommonMode并不是一个实际存在的模式
是同步Source/Timer/Observer到多个Mode中的一中技术方案
事件循环的实现机制
在RunLoop启动之后会发送一个通知,来告知观察者
将要处理Timer/Source0事件这样一个通知的发送
处理Source0事件
如果有Source1要处理,这时会通过一个go to语句的实现来进行代码逻辑的跳转,处理唤醒是收到的消息
如果没有Source1要处理,线程就将要休眠,同时发送一个通知,告诉观察者
然后线程进入一个用户态到内核态的切换,休眠,然后等待唤醒,唤醒的条件大约包括三种:
1、Source1
2、Timer事件
3、外部手动唤醒线程刚被唤醒之后也要发送一个通知告诉观察者,然后处理唤醒时收到的消息
回到将要处理Timer/Source0事件这样一个通知的发送
然后再次进行上面步骤,这就是一个RunLoop的事件循环机制
这里有一个这样的问题:当我们点击一个app,从我们点击到程序启动、程序运行再到程序杀死这个过程,系统都发生了什么呢
实际上当我们调用了main函数之后,会调用UIApplicationMain函数,在这个函数内部会启动主线程的RunLoop,然后经过一系列的处理,最终主线程的RunLoop会处于一个休眠状态,然后我们此时如果点击一下屏幕,会转化成一个Source1来讲我们的主线程唤醒,然后当我们杀死程序时,会调用RunLoop的退出,同时发送通知告诉观察者
RunLoop与多线程
线程与RunLoop是一一对应的
自己创建的线程默认没有RunLoop
实现一个常驻线程
为当前线程开启一个RunLoop
向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环
启动该RunLoop
请看下面的一个代码逻辑
#import "WXObject.h"
static NSThread *thread = nil;
/** 是否继续事件循环*/
static BOOL runAlways = YES;
@implementation WXObject
+ (NSThread *)threadForDispatch {
if (thread == nil) {
@synchronized (self) {
if (thread == nil) {
thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
[thread setName:@"alwaysThread"];
//启动线程
[thread start];
}
}
}
return thread;
}
+ (void)runRequest {
//创建一个Source
CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
//创建RunLoop,同时向RunLoop的defaultMode下面添加Source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
//如果可以运行
while (runAlways) {
@autoreleasepool {
//令当前RunLoop运行在defaultMode下
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
}
}
//某一时机,静态变量runAlways变为NO时,保证跳出RunLoop,线程推出
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}
@end
首先我们在这里定义两个全局静态变量,一个是我们自定义的线程thread,还有一个是用来控制是否事件循环
然后我们创建线程,用@synchronized来保证线程安全,创建的时候添加入口方法,然后启动线程,当线程调用start方法时,会调用下面入口方法
在这个方法中首先创建source,传入一个上下文,然后创建RunLoop,同时向RunLoop的defaultMode下面添加Source,CFRunLoopGetCurrent()这个方法如果获取不到就会创建一个RunLoop,然后添加到defaultMode中
通过我们前面定义的静态变量来进行判断,如果可以运行,就令当前RunLoop运行在defaultMode下,这里用了一个自动释放池,减小内存峰值消耗,这里需要注意的是,如果我们上面添加到的是defaultMode,这里也需要运行在defaultMode中,否则会出现死循环
某一时机,静态变量runAlways变为NO时,保证跳出RunLoop,线程推出,释放source
以上就是实现一个常驻线程的代码逻辑