RunLoop的二三事
Runloop的简介
RunLoop 是与线程相关的事件处理循环,我们可以在这里安排操作和协调对从外面传进来的事件的接收。RunLoop 的目的是让我们的线程在有活干的时候工作,在没活干的时候进入睡眠。所以 RunLoop 能让程序保持活着,在没有消息可处理时休眠来节省 CPU 资源。
Runloop的详情
Runloop的作用:
此时的“结束”将不会被打印,因为当程序启动从main函数进来之后,UIApplicationMain(argc, argv, nil, appDelegateClassName);
就创建了一个和主线程绑定的Runloop。Runloop 的存在,保住App的生命,让App 可以随时待命,处理用户的操作以及其他事件。否则的话,App刚启动就结束了!对于主线程的Runloop来说,这个描述很恰当,但是Runloop是和线程绑定的,其实这么说有点狭隘了。Runloop应该是用来保证当前线程的生命周期不被终结的一种方式。
Runloop的构成:
我们平时用NSRunloop是对CFRunLoopRef的封装,提供的是面向对象的 API。从开发层面来说,我们还是应该走进CFRunLoopRef来研究Runloop。
在 CoreFoundation 中关于 RunLoop 有 5 个类:CFRunloopRef、CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef。执行NSLog(@"%@", [NSRunLoop currentRunLoop]);
的打印缩略:
一个Runloop对应多个mode,一个mode对应多个source、timer、observer。同一个时刻,RunLoop只能是在一个mode上面的运行。如果需要切换mode,只能是退出currentMode ,切换到指定的 mode 。对应关系如下图:
CFRunLoopModeRef:
一个 RunloopMode 是若干个 source、timer 和 observer 的集合,mode过滤掉一些不想要的事件。
一个 RunLoop 在某个 mode 下运行时,不会接收和处理其他 mode 的事件 。要保持一个 mode 活着,就必须往里面添加至少一个 source、timer 或 observer 。
苹果公开的 mode 有两个:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。前者是默认的模式,程序运行的大多时候都处于该 mode 下,后者是滚动视图scrollerView及其子类滚动时为了界面流畅而用的 mode,这两个mode在执行的时候,相互独立,互不干扰。让不同的mode各司其职,对于程序运行的解耦很有好处。
CFRunLoop里面有一个伪mode叫做 kCFRunLoopCommonModes,它不是一个真正的 mode,而是若干个 mode 的集合。只要把事件加入到了 CommonModes 里面,就相当于添加到了它里面所有的 mode 中。
CFRunLoopSourceRef:
source就是事件输入源,分为三类:Port-Based Sources,Custom Input Sources,Cocoa Perform Selector Sources。现实情况下只有两种事件来源:source0 和 source1。
source0 是app内部的消息机制,使用时需要调用 CFRunLoopSourceSignal()来把这个 source 标记为待处理,然后掉用 CFRunLoopWakeUp() 来唤醒 RunLoop,让其处理这个事件。source1 是基于 mach_ports 的,用于通过内核和其他线程互相发送消息。
CFRunLoopTimerRef:
timer就是我们熟悉的OC里面的计时器NSTimer。我们用NSTimer的时候会有几个注意的点,一般情况下用scheduledTimerWithTimeInterval
初始化的timer不用手动添加到Runloop,而用timerWithTimeInterval
创建的timer要手动添加到Runloop里面。同时有一个经典的面试题目:
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开始");
[self performSelector:@selector(testFunc) withObject:nil afterDelay:0];
NSLog(@"结束");
});}
- (void)testFunc {
NSLog(@"调用");
}
此时是不会打印“调用”的,[self performSelector:@selector(testFunc) withObject:nil afterDelay:0];
虽然添加进了当前子线程的Runloop,但是子线程的Runloop是默认不开启的。所以在[self performSelector:@selector(testFunc) withObject:nil afterDelay:0];
之后要添加[[NSRunLoop currentRunLoop] run];
来开启子线程的Runloop。
CFRunLoopObserverRef:
observer既观察者,这个观察者可以观察Runloop的7种状态:
我从Demo的测试总得出的结论是:
主线程的Runloop在处理了启动后的timer、source、observer输入的事件之后,
进入休眠kCFRunLoopBeforeWaiting状态。
直到我自己写的定时器唤起Runloop到kCFRunLoopAfterWaiting状态,
每次处理完定时器testFunc的回调,就进入kCFRunLoopBeforeWaiting状态,
直至被下次计时器唤醒进去kCFRunLoopAfterWaiting状态。
往复不停的循环,直到定时器结束,Runloop进入休眠。
Runloop与线程的关系:
主线程有与之对应的mainRunLoop,可以通过[NSRunLoop mainRunLoop]
或者在主线程里[NSRunLoop currentRunLoop]
获取。子线程如果不主动或者通过某些方法隐式获取当前线程的Runloop的话,那么这个线程就没有Runloop。苹果没有创建runloop的方法,但是可以通过currentRunloop在当前线程创建Runloop。RunLoop和线程是一一对应的,可以有线程没有runloop,但是有runloop一定会有线程与之对应。
Runloop是以线程为key值放在一个全局Map里的
。
获取的方法大致如下:
// 全局的 dictionary, key 是 pthread_t, value 是 CFRunLoopRefstatic CFMutableDictionaryRef __CFRunLoops = NULL;
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 第一次进入时,创建全局 dictionary
if (!__CFRunLoops) {
// 创建可变字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable();
// 先创建主线程的 RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 主线程的 RunLoop 存进字典中
CFDictionarySetValue(dict, pthread_main_thread_np(), mainLoop);
}
// 用 传进来的线程 作 key,获取对应的 RunLoop
CFRunLoopRef loop = CFDictionaryGetValue(__CFRunLoops, t);
// 如果获取不到,则新建一个,并存入字典
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
}
return loop;
}
// 获取主线程的 RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np());
return __main;
}
Runloop运行流程:
转载自:https://juejin.cn/post/6981821257507913742