RunLoop 的字面意思是运行循环, 跑圈, 一个 App 启动后能一直执行, 就是因为启动后进入了一个循环, 在这个循环中不断监听各种状态, 手势动作, 并做出相应的响应. 这个循环就是我们今天要探究的 RunLoop.
1 RunLoop 基础
1.1 RunLoop 的基本作用
保持程序的持续运行(iOS 程序为什么能一直活着不会死)
处理 App 中的各种事件(比如触摸事件, 定时器事件[NSTimer] ,selector 事件[选择器. performSelector...] )
节省 CPU 资源, 提高程序性能, 有事情就做事情, 没事情就休息
1.2 关于 RunLoop 的几点说明
如果没有 Runloop, 那么程序一启动就会退出, 什么事情都做不了.
如果有了 Runloop, 那么相当于在内部有一个死循环, 能够保证程序的持续运行
main 函数中的 Runloop
a 在 UIApplication 函数内部就启动了一个 Runloop 该函数返回一个 int 类型的值
b 这个默认启动的 Runloop 是跟主线程相关联的
1.3 RunLoop 对象
在 iOS 开发中有两套 API 来访问 Runloop
foundation 框架[NSRunloop]
core foundation 框架[CFRunloopRef]
NSRunLoop 和 CFRunLoopRef 都代表着 RunLoop 对象, 它们是等价的, 可以互相转换
NSRunLoop 是基于 CFRunLoopRef 的一层 OC 包装, 所以要了解 RunLoop 内部结构, 需要多研究 CFRunLoopRef 层面的 API(Core Foundation 层面)
1.4 RunLoop 与线程
Runloop 和线程的关系:
一个 Runloop 对应着一条唯一的线程
问题: 如何让子线程不死 回答: 给这条子线程开启一个 Runloop
Runloop 的创建: 主线程 Runloop 已经创建好了, 子线程的 runloop 需要手动创建
Runloop 的生命周期: 在第一次获取时创建, 在线程结束时销毁
1.5 获取 Runloop 对象
- /*1. 获得当前 Runloop 对象 */
- //01 NSRunloop
- NSRunLoop * runloop1 = [NSRunLoop currentRunLoop];
- //02 CFRunLoopRef
- CFRunLoopRef runloop2 = CFRunLoopGetCurrent();
- /*2. 拿到当前应用程序的主 Runloop(主线程对应的 Runloop)*/
- //01 NSRunloop
- NSRunLoop * runloop1 = [NSRunLoop mainRunLoop];
- //02 CFRunLoopRef
- CFRunLoopRef runloop2 = CFRunLoopGetMain();
- /*3. 注意点: 开一个子线程创建 runloop, 不是通过 alloc init 方法创建, 而是直接通过调用 currentRunLoop 方法来创建, 它本身是一个懒加载的.
- 4. 在子线程中, 如果不主动获取 Runloop 的话, 那么子线程内部是不会创建 Runloop 的. 可以下载 CFRunloopRef 的源码, 搜索_CFRunloopGet0, 查看代码.
- 5.Runloop 对象是利用字典来进行存储, 而且 key 是对应的线程 Value 为该线程对应的 Runloop.*/
2 RunLoop 相关类
2.1 Runloop 运行原理图
在线程中开启 RunLoop 后, 系统会进入一个死循环, 这个循环在有事件触发时 (触摸事件, 定时器事件[NSTimer] ,selector 事件[选择器. performSelector...] 等) 就工作, 没事情就休息, 提高程序性能, 节省 CPU 资源, 示意图如下.
2.2 RunLoop 相关的 5 个类
CFRunloopRef
CFRunloopModeRef[Runloop 的运行模式]
CFRunloopSourceRef[Runloop 要处理的事件源]
CFRunloopTimerRef[Timer 事件]
CFRunloopObserverRef[Runloop 的观察者(监听者)]
Runloop 要想跑起来, 它的内部必须要有一个 mode, 这个 mode 里面必须有 source\observer\timer, 至少要有其中的一个.
2.2.1 CFRunloopModeRef
CFRunloopModeRef 代表着 Runloop 的运行模式
一个 Runloop 中可以有多个 mode, 一个 mode 里面又可以有多个 source\observer\timer 等等
每次 runloop 启动的时候, 只能指定一个 mode, 这个 mode 被称为该 Runloop 的当前 mode
如果需要切换 mode, 只能先退出当前 Runloop, 再重新指定一个 mode 进入, 这样做主要是为了分割不同组的定时器等, 让他们相互之间不受影响
系统默认注册了 5 个 mode
a.kCFRunLoopDefaultMode:App 的默认 Mode, 通常主线程是在这个 Mode 下运行
b.UITrackingRunLoopMode: 界面跟踪 Mode, 用于 ScrollView 追踪触摸滑动, 保证界面滑动时不受其他 Mode 影响
c.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode, 启动完成后就不再使用
d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode, 通常用不到
e.kCFRunLoopCommonModes: 这是一个占位用的 Mode, 不是一种真正的 Mode
2.2.2 CFRunloopTimerRef: 基于时间触发一个操作. 基本上说的就是 NSTimer
NSTimer 在实际开发中会出现不准的情况, 出现这种情况的主要是 NSTimer 的初始化有两种方法如下, 然后第一种方法会自动添加到当前的 RunLoop 中, 并且 RunLoop 的运行模式 mode 设置为 kCFRunLoopDefaultMode, 这种模式在界面被拖拽时运行 mode 变为 UITrackingRunLoopMode, 这时候 defaultmode 下的定时器就会停止工作, 所以在界面拖拽时定时器不计时, 导致计时不准.
- + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
- + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
解决上述所说的定时器不准的方案是设置 RunLoop 的工作 mode 为 kCFRunLoopCommonModes, 这种模式可以在多种 mode 下都进行工作.
- /*
- 说明:
- (1)runloop 一启动就会选中一种模式, 当选中了一种模式之后其它的模式就都不鸟. 一个 mode 里面可以添加多个 NSTimer, 也就是说以后当创建 NSTimer 的时候, 可以指定它是在什么模式下运行的.
- (2)它是基于时间的触发器, 说直白点那就是时间到了我就触发一个事件, 触发一个操作. 基本上说的就是 NSTimer
- (3)相关代码
- */
- - (void)timer2 {
- //NSTimer 调用了 scheduledTimer 方法, 那么会自动添加到当前的 runloop 里面去, 而且 runloop 的运行模式 kCFRunLoopDefaultMode
- NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
- // 更改模式
- [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- }
- - (void)timer1 {
- // [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
- NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
- // 定时器添加到 UITrackingRunLoopMode 模式, 一旦 runloop 切换模式, 那么定时器就不工作
- // [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
- // 定时器添加到 NSDefaultRunLoopMode 模式, 一旦 runloop 切换模式, 那么定时器就不工作
- // [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
- // 占位模式: common modes 标记
- // 被标记为 common modes 的模式 kCFRunLoopDefaultMode UITrackingRunLoopMode
- [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- // NSLog(@"%@",[NSRunLoop currentRunLoop]);
- }
- - (void)run {
- NSLog(@"---run---%@",[NSRunLoop currentRunLoop].currentMode);
- }
- - (IBAction)btnClick {
- NSLog(@"---btnClick---");
- }
GCD 中的定时器的使用
- //0. 创建一个队列
- dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
- //1. 创建一个 GCD 的定时器
- /*
- 第一个参数: 说明这是一个定时器
- 第四个参数: GCD 的回调任务添加到那个队列中执行, 如果是主队列则在主线程执行
- */
- dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
- //2. 设置定时器的开始时间, 间隔时间以及精准度
- // 设置开始时间, 三秒钟之后调用
- dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC);
- // 设置定时器工作的间隔时间
- uint64_t intevel = 1.0 * NSEC_PER_SEC;
- /*
- 第一个参数: 要给哪个定时器设置
- 第二个参数: 定时器的开始时间 DISPATCH_TIME_NOW 表示从当前开始
- 第三个参数: 定时器调用方法的间隔时间
- 第四个参数: 定时器的精准度, 如果传 0 则表示采用最精准的方式计算, 如果传大于 0 的数值, 则表示该定时切换 i 可以接收该值范围内的误差, 通常传 0
- 该参数的意义: 可以适当的提高程序的性能
- 注意点: GCD 定时器中的时间以纳秒为单位(面试)
- */
- dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC);
- //3. 设置定时器开启后回调的方法
- /*
- 第一个参数: 要给哪个定时器设置
- 第二个参数: 回调 block
- */
- dispatch_source_set_event_handler(timer, ^{
- NSLog(@"------%@",[NSThread currentThread]);
- });
- //4. 执行定时器
- dispatch_resume(timer);
- // 注意: dispatch_source_t 本质上是 OC 类, 在这里是个局部变量, 需要强引用
- self.timer = timer;
- 2.2.3 CFRunloopSourceRef
CFRunloopSourceRef 是事件源也就是输入源, 有两种分类模式; 一种是按照苹果官方文档进行划分的, 另一种是基于函数的调用栈来进行划分的(source0 和 source1).
(1)以前的分法
- Port-Based Sources
- Custom Input Sources Cocoa
- Perform Selector Sources
(2)现在的分法 Source0: 非基于 Port 的; Source1: 基于 Port 的
可以通过打断点的方式查看一个方法的函数调用栈
2.2.4 CFRunLoopObserverRef
(1)CFRunLoopObserverRef 是观察者, 能够监听 RunLoop 的状态改变
(2)如何监听
- // 创建一个 runloop 监听者
- CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
- NSLog(@"监听 runloop 状态改变 ---%zd",activity);
- });
- // 为 runloop 添加一个监听者
- CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
- CFRelease(observer);
(3)监听的状态
- typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
- kCFRunLoopEntry = (1UL << 0), // 即将进入 Runloop
- kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 NSTimer
- kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Sources
- kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
- kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
- kCFRunLoopExit = (1UL << 7), // 即将退出 runloop
- kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有状态改变
- };
3 RunLoop 的运行逻辑
来源: https://www.cnblogs.com/mukekeheart/p/9773044.html