本文主要内容
一.RunLoop的概念
二.RunLoop的数据结构
三.事件循环的实现机制
四.RunLoop与NSTimer
五.RunLoop与多线程
一.RunLoop的概念
RunLoop是通过内部维护的事件循环
来对事件/消息进行管理
的一个对象。
问题1:什么是事件循环?
- 没有消息需要处理时,休眠以避免资源占用;
- 有消息需要处理时,立刻被唤醒。
`扩展`
内核态:在一个进程中,如果有系统调用,此时进程处于内核态,执行文件操作、网络数据发送等任务,此时特权级别比较高,为0级。
用户态:当一个进程执行用户自己的代码时,处于用户态,此时特权级别比较低,为3级。
所有用户程序都是运行在用户态的,但是有时候程序确实需要做一些内核态的事情,例如执行文件操作、网络数据发送等操作;而唯一可以做这些事情的就是操作系统,所以此时程序就需要操作系统请求以程序的名义来执行这些操作。
以下是操作系统内存空间分布图:
问题2:为什么程序中的main函数能够保证不退出?
- 在main函数中,调用了UIApplicationMain函数,此函数中会启动主线程的runloop 。runloop是对事件循环的维护机制,可以做到"有事做的时候去做事,没事做的时候从用户态切换到内核态,避免资源的占用,使当前线程处于一个休眠状态"。
二.RunLoop的数据结构
NSRunLoop是CFRunLoop的封装,提供了面向对象的API。
NSRunLoop位于Foundation中,位于Core Foundation。CFRunLoop的源码是开源的!
2.1 RunLoop的数据结构
- CFRunLoop
- CFRunLoopMode
- Source/Timer/Observer
CFRunLoop
- pthread:一一对应,RunLoop和线程的关系;
- currentMode:CFRunLoopMode数据结构;
- modes:NSMutableSet集合<CFRunLoopMode*>;
commonModes
:NSMutableSet集合<NSString*>;- cmomonModeltems:多个Observer、多个Timer、多个Source组成。
问题3:RunLoop和线程之间的关系
1.线程和RunLoop之间的关系是一一对应的;
2.主线程的RunLoop是默认开启的,子线程的RunLoop默认是不开启的;
3.子线程的RunLoop在主动获取的情况下,才会创建;
4.子线程的RunLoop在线程结束时,才会销毁。
问题4:commonModes的作用
1.在iOS上对应的是NSRunLoopCommonModes;
2.commonMode不是实际存在的一种Mode;
3.是同步Source/Timer/Observer到多个Model中的一种技术方案;
CFRunLoopMode
- name:NSDefaultRunLoopMode
- sources0:MutableSet
- source1:MutableSet
- observers:MutableArray
- timers:MutableArray
CFRunLoopSource
- source0:需要手动唤醒线程;
- source1:具备唤醒线程的能力。
CFRunLoopTimer
基于事件的定时器,和NSTimer是toll-free birdged(免费桥转换)的。
CFRunLoopObserver
观测时间点:
- kCFRunLoopEntry:入口时机;
- kCFRunLoopBeforeTimers:通知观察者RunLoop将对timer相关事件进行处理;
- kCFRunLoopBeforeSources:将要处理source事件;
- kCFRunLoopBeforeWaiting:将要进入休眠状态,从用户态切换到内核态;
- kCFRunLoopAfterWaiting:从用户态切换到内核态不久后
- kCFRunLoopExit:RunLoop退出通知
2.2 各个数据结构之间的关系
问题5:RunLoop、Model、Source/Timer/Observer关系
- RunLoop和Model是一对多的关系(从CFRunLoop的数据结构modes)
- Model和Source/Timer/Observer是一对多的关系。
问题6:RunLoop为什么有多个Model
这样设计的原因是为了起到事件屏蔽的效果。
当RunLoop运行在Mode1时,只能接收处理Mode1上的source1、observer、timers事件回调,不能接收其他Mode上source/observer/timer事件回调,起到了事件屏蔽的效果。
三.事件循环的实现机制
无论是NSRunLoop还是CFRunLoop最终都会调用到CFRunLoopRun()。
3.1 事件循环机制的实现流程
- 1.在RunLoop启动后,先发送通知告知观察者当前RunLoop即将启动;
- 2.之后RunLoop将要处理Timer/Source0事件的通知发送;
- 3.接着进入Source0事件的处理;
- 4.此时如果有Source1要处理,会通过goto语句实现来进行代码逻辑的调转,处理唤醒时收到的消息(第8步);
- 5.如果没有Source1需要处理,线程将要休眠(第6步),同时发送通知到Observer;
- 6.从用户态到内核态的切换,线程休眠,等待唤醒;
- 唤醒条件:Source1、Timer事件、外部手动唤醒
- 7.线程刚被唤醒,发送通知给观察者;
- 8.处理唤醒时收到的消息;
- 9.再次回到第2步。
问题7:一个处于休眠状态的RunLoop通过哪些事件能唤醒它?
- Source1回调;
- Timer事件
- 外部手动唤醒
问题8:点击APP图标,从程序启动、运行、退出这个过程当中,系统都发生了什么?
- 程序启动后,调用main函数后,会调用UIApplicationmain函数,此函数内部会启动主线程的RunLoop,经过一系列处理,最终主线程RunLoop处于休眠状态;
- 如果此时点击了屏幕,会产生一个
mach_port
,基于mach_port最终转成Source1,唤醒主线程,运行处理; - 当把程序杀死后,RunLoop退出,并且发送通知给观察者。RunLoop退出后线程即刻销毁。
3.2 RunLoop的核心
- 在main函数中,经过一些列处理,会调用系统函数mach_msg(),就发生了系统调用,此时会从用户态转为核心态;
- 在内核态下,一定条件时(Source1/Timer事件/外部手动唤醒),mach_msg()会返回给调用方,此时程序会从核心态转为用户态。
四.RunLoop与NSTimer
问题9:滑动TableView的时候定时器还会生效吗?
不会生效
- 1.TableView正常情况是运行在kCFRunLoopDefaultMode模式下,当滑动时会发生Mode的切换,切换到UITrackingRunLoopMode模式。
- 2.当把Timer/Source/Observer添加到某一个Mode(kCFRunLoopDefaultMode)上面后,如果当前RunLoop运行在另一个Mode(UITrackingRunLoopMode)上,对应的Timer/Source/Observer无法进行处理和回调。
解决方案:使用如下函数将Timer/Source/Observer同步到多个Mode中
void CFRunLoopAddTimer(runLoop, timer, commonMode)
五.RunLoop与多线程
- 线程和RunLopp一一对应
- 自己创建的线程默认没有RunLoop
问题10:怎样实现一个常驻线程?
- 为当前线程开启一个RunLoop;
- 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环;
- 启动该RunLoop。
关键代码逻辑
HGObject.h
#import "HGObject.h"
#implementation HGObject
// 自定义线程
static NSThread *thread = nil;
// 标记是否要继续事件循环
static BOOL runAlways = YES;
+ (NSThread *)threadForDispatch {
if (thread == nil) {
// 创建线程:线程安全
@synchronized(self) {
if (thread == nil) {
thread = [[NSThread alloc] initWithTarget: self selector: @selector(runRequest) object: nil];
[thread setName: @"com.tonlyele.xxx"];
// 启动
[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);
CGRelease(source);
}
@end
实现常驻线程代码
// 创建线程
NSThread *subThread = [[NSThread alloc] initWithTarget: self selector: @selector(subThreadEntryPoint) object: nil];
[subThread setName: @"HGThread"];
[subThread start];
// 开启RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 如果注释如下行,子线程中的任务无法执行
[runLoop addPort: [NSMachPort port] forMode: NSRunLoopCommonModes];
[runLoop run];
本文总结
问题1:什么是RunLoop,它是怎样做到有事做事,没事休息的?
- RunLoop是一个通过内部循环对事件或消息进行管理的对象;
- 程序运行会调用main函数,在main函数中调用UIApplicationMain,在此函数中会启动主线程的RunLoop;
- RunLoop运行后,会调用系统mac_msg()方法,发生系统调用,会使程序从用户态转为核心态,此时线程处于休眠状态;
- 当外界条件(source回调/Timer事件/Observer)变化时,mach_msg()函数会使得程序从核心态转为用户态,此时线程处于唤醒状态。
问题2:RunLoop与线程的关系
- RunLoop与线程是一一对应的;
- 一个线程默认是没有RunLoop的,需要手动创建(主线程除外)。
问题3:如何实现一个常驻线程?
- 1.自定义线程,并为线程开启一个RunLoop;
- 2.向RunLoop中添加一个Port/Source等维持RunLoop的事件循环;
- 3.启动该RunLoop
问题4:怎样保证子线程数据回来更新UI的时候,不打断用户的滑动操作?
- 1.用户滑动时,当前的RunLoop运行在UITrackingRunLoopMode模式下;
- 2.而网络请求一般放在子线程进行,子线程返回给主线程的数据要抛给主线程用来更新UI。可以把子线程请求数据抛回给主线程进行UI更新的逻辑,包装起来提交到主线程的NSDefaultRunLoopMode模式下;
- 3.当当前用户正在滑动操作时处于UITrackingRunLoopMode模式下,分派到NSDefaultRunLoopMode模式下的任务不会执行;
- 4.当停止滑动后,当前线程会切换到NSDefaultRunLoopMode模式下,处理子线程上抛给主线程的UI更新的任务,这样就不会打断用户的滑动操作。
有任何问题,欢迎👏各位评论指出!觉得博主写的还不错的麻烦点个赞喽👍