本篇是探索底层Runloop,目的是能够深入理解Runloop是干什么用的?什么时候用?怎么用?
1、什么是runloop?
runloop是一个循环,它在持续不断的跑圈,iOS应用程序刚打开时,就创建了一个主线程,并默认创建了Runloop保持主线程的持续运行。
我们到官方文档搜索一下Runloop
,如图所示
发现找不到Runloop,再尝试搜索thread,发现在线程介绍里面竟然出现了Runloop字样,如图
image.png
如此可见,Runloop和线程之间有着不清不楚的关系。
再来看一下CFRunloop源码中的CFRunLoopRun
函数
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
我们可以看到,Runloop本质是一个do...while循环。
结合官方文档提供的运行循环结构看一下,Runloop是如何执行的。
image.png
从上图中我们可以看出,Runloop就是依附在线程上的循环,通过输入源(Input sources)和定时源(Timer sources)接收事件,然后交给线程去处理事件。
所以什么是Runloop?
Runloop就是一个循环,为了线程而生,它的本质是一个do...while循环
2、Runloop的作用
- 1、保持程序持续的运行
一般情况下,线程在执行完任务就会退出,如果我们不希望线程退出,还想让它执行更多的任务,就需要用到Runloop
了。 - 2、接收并处理App中的各种事件
Runloop在循环时,通过输入源(input source)和定时源(timer source)接收App事件(触摸事件、UI刷新时间、定时器、performSelector)。 - 3、提升性能
在线程不工作时休眠,节省CPU资源。
以上就是Runloop的作用,这里只是概括一下,后面会有具体到用法的Runloop应用。
3、Runloop和线程的关系
接下来我们结合Runloop源码看看它和线程之间的关系,找到_CFRunLoopGet0
这个函数,它的作用是获取runloop对象
//CFRunloop.c
//这个类是获取Runloop对象的
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (!__CFRunLoops) {//CFRunloops是存放runloop和线程对应关系的字典
//创建存放runloop和线程的字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//获取主线程runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//将主线程和主线程对应的Runloop对象mainLoop添加到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//获取子线程对应的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
//依据线程t创建Runloop对象
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//绑定线程和runloop到字典中
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
}
}
上述代码中有一段CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
,意思是以线程为key,runloop为value,将runloop存储到一个全局的字典中。
至此我们得出了第一个结论:线程和Runloop是一一对应的关系。
再看这段获取主线程runloop的代码CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
,意思是创建一个主线程的Runloop。
至此得出第二个结论:Runloop是以线程为参数创建的,并保存在全局的字典里。
再看后半段代码
//获取子线程对应的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
//依据线程t创建Runloop对象
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//绑定线程和runloop到字典中
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
}
这段代码的意思是先从__CFRunLoops
字典中获取Runloop对象,若没有,则以线程为参数创建一个,并存储到__CFRunLoops
字典里,至此我们知道了第三个结论:主线程的Runloop由系统自动创建,子线程的Runloop需要在子线程里手动获取Runloop时创建。
综上我们知道了Runloop和线程的关系:
1、线程和Runloop是一一对应的关系。
2、Runloop是以线程为参数创建的,并保存到全局的字典里
3、主线程的Runloop由系统自动创建,子线程的Runloop需要在子线程里手动获取Runloop时创建。
4、Runloop在第一次获取时创建,在线程销毁时随之销毁。
4、Runloop的五个对象
1.__CFRunLoop * CFRunLoopRef;
2.__CFRunLoopSource * CFRunLoopSourceRef;
3.__CFRunLoopObserver * CFRunLoopObserverRef;
4.__CFRunLoopTimer * CFRunLoopTimerRef;
5.CFRunloopModeRef(为什么这个这么写呢,因为Runloop并没有暴露RunloopMode这个对象)
下面逐一讲一下Runloop这几个对象的含义和它们之间的关系,如图
上图就是Runloop对象、Mode、Source、Observer、Timer之间的关系。
一个Runloop包含若干个
CFRunloopModeRef(运行模式)
,一个CFRunloopModeRef
又包含若干个CFRunLoopSourceRef(输入源)
/CFRunLoopTimerRef(定时源)
/CFRunLoopObserverRef(观察源)
,但是Runloop同一时间只能指定一个CFRunloopModeRef(运行模式)
,如果要切换CFRunloopModeRef
,需要先退出Runloop,再指定一个CFRunloopModeRef(运行模式)
进入。
为什么是这样的结构?
答:这样做主要是为了分离Source/Timer/Observer,让其互不影响。
4.1、CFRunLoopRef(Runloop对象)
CFRunLoopRef
是 Core Foundation 框架下 RunLoop 对象类,可通过如下方式获取
// 获得当前线程的 RunLoop 对象
CFRunLoopGetCurrent();
// 获得主线程的 RunLoop 对象
CFRunLoopGetMain();
也可以使用Foundation框架中的NSRunloop
获取封装过的Runloop,NSRunloop是对CFRunLoopRef
的封装
// 获得当前线程的 RunLoop 对象
[NSRunLoop currentRunLoop];
// 获得主线程的 RunLoop 对象
[NSRunLoop mainRunLoop];
4.2、CFRunLoopSourceRef(输入源)
先看一下源码
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
//对应Source0
CFRunLoopSourceContext version0; /* immutable, except invalidation */
//对应Source1
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
从源码看出Source分为个版本
- 1、Source0:这种版本的Source不能主动触发事件,得是用户或开发者手动触发,比如触摸事件和performSelector等App内部事件(UIEvent),需要先进行
CFRunLoopSourceSignal
标记,再通过CFRunLoopWakeUp
唤醒Runloop处理事件。 - 2、Source1:基于Port,用于通过内核和线程之间通信的,这种Source可以主动唤醒Runloop线程,一会儿用例子看一下。
先看一个触摸事件触发Source0的例子,创建个工程,上面放一个按钮,在点击事件的回调中打断点,如图
image.png
在左侧栏目中是触发的方法,如图
image.png
我们可以看到,用户的触摸事件,果然触发的是Source0。
下面是Source0的使用例子
要创建一个Source0输入源,需要执行六步走,
1、创建Context上下文
2、创建Source0输入源对象
3、获取Runloop
4、绑定Runloop、Source0和mode
5、标记执行信号CFRunloopSourceSignal
6、唤醒CFRunLoopWakeUp
void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"准备代发");
}
void perform(void *info){
NSLog(@"执行吧,骚年");
}
void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"取消了,终止了!!!!");
}
- (void)source0Demo{
//1、创建Context上下文
CFRunLoopSourceContext context = {
0,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
schedule,
cancel,
perform,
};
/**
2、创建CFRunLoopSourceRef
参数一:传递NULL或kCFAllocatorDefault以使用当前默认分配器。
参数二:优先级索引,指示处理运行循环源的顺序。这里我传0为了的就是自主回调
参数三:为运行输入源保存上下文信息的结构
*/
CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
//3、获取Runloop
CFRunLoopRef rlp = CFRunLoopGetCurrent();
//4、绑定Source、Runloop、Mode,此时我们的source就进入待绪状态
CFRunLoopAddSource(rlp, source0, kCFRunLoopDefaultMode);
//5、 一个执行信号
CFRunLoopSourceSignal(source0);
//6、 唤醒 run loop 防止沉睡状态
CFRunLoopWakeUp(rlp);
// 取消 移除
// CFRunLoopRemoveSource(rlp, source0, kCFRunLoopDefaultMode);
CFRelease(rlp);
}
从代码中可以看出,若要让Runloop执行Source0的事件,需要先发出一个执行信号CFRunLoopSourceSignal
,再调用CFRunLoopWakeUp
唤醒Runloop执行任务。
控制台打印效果如下
image.png
下面是Source1使用Port进行线程间通讯的例子
创建
@interface ViewController ()<NSPortDelegate>
@property (nonatomic, strong) NSPort* subThreadPort;//主线程Port
@property (nonatomic, strong) NSPort* mainThreadPort;//子线程Port
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self portCommunicateTest];
}
- (void)portCommunicateTest{
self.mainThreadPort = [NSPort port];
self.mainThreadPort.delegate = self;
// port - source1 -- runloop
//port是操作Source1的,所以同样依赖于runloop
[[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];
//创建子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//实例化子线程对应的Port
self.subThreadPort = [NSPort port];
self.subThreadPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:self.subThreadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
});
}
//NSPort的代理方法,线程间通信的回调
- (void)handlePortMessage:(id)message {
NSLog(@"当前线程是 == %@", [NSThread currentThread]); // 3 1
NSLog(@"传来的消息内容 = %@", [[NSString alloc] initWithData:[message valueForKey:@"components"][0] encoding:NSUTF8StringEncoding]);
sleep(1);
if (![[NSThread currentThread] isMainThread]) {
//像子线程的Port发送消息
NSMutableArray* components = [NSMutableArray array];
NSData* data = [@"world" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
[self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//注意,component必须以NSData的形式传递
NSMutableArray* components = [NSMutableArray array];
NSData* data = [@"Hello" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
[self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
}
点击屏幕后,打印结果如下
在回调消息处打断点,发现如图所示
image.png
看到左侧显示的输入源是
Source1
,至此使用NSPort进行线程间通信的例子执行完毕,真可爱,线程还能这么玩。
4.3、CFRunloopModeRef
Runloop有五种运行模式
-
UIInitializationRunLoopMode
:在App刚启动进入的运行模式,启动完成后会切换到kCFRunLoopDefaultMode,从此不再使用。
-
-
kCFRunLoopDefaultMode
: 默认运行模式,在主线程运行在这个模式下。
-
-
UITrackingRunLoopMode
:界面跟踪模式,当见面滚动时,会切换到这个运行模式下,保证不收其他模式的影响。
-
-
GSEventReceiveRunLoopMode
: 接受系统事件的内部运行模式。
-
-
kCFRunLoopCommonModes
: 占位模式,通常用来标记kCFRunLoopDefaultMode
和UITrackingRunLoopMode
,如果NSTimer加入这个模式,将不受运行模式切换的影响。
-
4.4、CFRunloopTimerRef
CFRunloopTimerRef
是一个时间触发器,它包含一个时间长度和一个回调(函数回调),在加入Runloop时,Runloop会注册一个时间点,经过时间长度后,Runloop会被唤醒执行回调。Timer的底层就是一个CFRunloopTimerRef,它受Mode
切换的影响。如果把Timer加入到kCFRunLoopCommonModes就不会受切换影响了,像下面这样
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"log NSTimer runloop");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
4.5、CFRunloopObserverRef
CFRunloopObserverRef
是一个观察者,用来监控Runloop的状态的,它分为以下状态
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop 1
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer 2
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source 4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 32
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 64
kCFRunLoopExit = (1UL << 7), // 即将退出Loop 128
};
举个例子
- (void)obseverDemo{
//1、创建观察者上下文
CFRunLoopObserverContext context = {
0,
((__bridge void *)self),
NULL,
NULL,
NULL
};
//2、获取当前Runloop对象
CFRunLoopRef rlp = CFRunLoopGetCurrent();
//3、创建观察者CFRunLoopObserverRef
CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, runLoopObserverCallBack, &context);
//4、将观察者observer和runloop对象、Mode关联起来
CFRunLoopAddObserver(rlp, observerRef, kCFRunLoopDefaultMode);
}
void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
}
在我们滚动视图时,控制台打印如下
image.png
由此可见,官方说的对☺,确实可以通过CFRunloopObserverRef监听Runloop的状态。
Runtime的应用有很多,之前在项目中应用的比较深的是将对CPU压力比较大的UI任务拆分成多个小任务,通过监听Runloop的Observer空闲时机,在空闲时强制其执行小任务,高效利用系统资源提升性能。
RunLoopWorkDistribution了解一下(😏)
网友评论