RunLoop - 知识点总结
RunLoop 初窥:
- 从字面意思看:运行循环, 跑圈
- 其实它内部就是一个 do-while 循环, 在这个循环内部不断的处理各种任务(比如 source, timer, observer)
基本作用
- 保持程序的持续运行
- 处理 APP 中的各种事件(触摸事件, 定时器事件, Selector 事件等)
- 节省 CPU 资源, 提高程序性能, 该做事时做事, 该休息时休息
在程序中的体现
- 在有 RunLoop 的情况下,由于 main 函数里面启动了 RunLoop, 所以程序不会马上退出, 保持持续运行状态
- 在
main.m
里的UIApplicationMain
函数内部会启动一个 RunLoop - 所以
UIApplicationMain
函数一直没有返回,保持了程序的持续运行 - 这个默认启动的 RunLoop 是跟主线程相关联的
RunLoop 对象
iOS 中有两套 API 来访问和使用 RunLoop
-
Foundation -- 用 NSRunLoop 访问
-
Core Foundation -- 用 CFRunLoopRef 访问
NSRunLoop 和 CFRunLoopRef 都代表 RunLoop 对象, NSRunLoop是在基于CFRunLoopRef的一层 OC 封装
RunLoop 与线程
- 每条线程都有唯一的一个与之对应的 RunLoop 对象
- 主线程的 RunLoop 已经自动创建好了, 子线程的 RunLoop 需要手动去创建
- RunLoop 在第一次获取时创建,在线程结束的时候销毁
获得 RunLoop 对象
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的 RunLoop 对象
[NSRunLoop mainRunLoop]; // 获得主线程的 RunLoop 对象
Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的 RunLoop 对象
CFRunLoopGetMain(); // 获得主线程的 RunLoop 对象
在代码中是这样的
// 1. 获得主线程对应的 RunLoop
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
// 2. 获得当前线程对应的 RunLoop
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
// 两者内存地址是一样的
NSLog(@"%p - %p", mainRunLoop, currentRunLoop);
//NSLog(@"%@", mainRunLoop);
// 3. core
NSLog(@"%p - %p", CFRunLoopGetMain(), CFRunLoopGetCurrent());
通过打印结果我们可以发现, 当前线程的 RunLoop 和主线程的 RunLoop 内存地址是一样的
RunLoop 和线程的关系
两者是一一对应的. 只是主线程的 RunLoop 已经创建, 子线程的 RunLoop 需要手动创建.
[[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];
实现run 方法
- (void)run {
// 创建子线程对应的 RunLoop. currentRunLoop是懒加载方法,当第一次调用时先判断对应的 RunLoop 是否存在,如果不存在,自己创建并返回,如果存在就直接返回
NSLog(@"%@", [NSRunLoop currentRunLoop]);
NSLog(@"run - %@", [NSThread currentThread]);
}
RunLoop 相关类
Core Foundation 中关于 RunLoop 的5个类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimeRef
- CFRunLoopObserverRef
CFRunLoopModeRef
系统默认注册了5个 Mode:
- kCFRunLoopDefaultMode: App 的默认 Mode, 通常主线程是在这个 Mode 下运行
- UITrackingRunLoopMode: 界面跟踪 Mode, 用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode: 在刚启动 APP 时进入的第一个 Mode, 启动文成后就不再使用
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode, 通常用不到
- kCFRunLoopCommonModes: 这是一个占位用的 Mode, 不是一种真正的 Mode
我们平时主要用到的是前两种
每个 RunLoop 中可能有两个或者多个 Mode,
mode 元素:
- Source - 事件源
- Observer - 观察者
- Timer - 定时器事件
注意:
- 虽然 RunLoop 中有多个运行模式, 但是启动之后只能选择一种模式运行 ,这种 Mode 被称作 CurrentMode
- mode 里至少要有一个 Timer 或者 Source
- 如果需要切换 Mode, 只能先退出 Loop, 再重新指定一个 Mode 进入. 这样做的好处主要是为了分隔开不同组的 Source/Timer/Observer, 让其互不影响
当 RunLoop 中添加定时器时, 选择不同的 mode 会有不同的效果
NSDefaultRunLoopMode
// 1. 创建定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run2) userInfo:nil repeats:YES];
// 2. 添加到 RunLoop 中, 指定 RunLoop 的运行模式为默认模式
/*
第一个参数: 定时器
第二个参数: RunLoop 的运行模式
*/
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
但是这时候有个问题, 当拖拽界面其他控件(例如 scrollView 和 textView)的时候,定时器不工作. 松开点击后才工作
UITrackingRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
这个模式下只有拖拽界面其他控件的时候,定时器才工作,但停止拖拽, 依然不工作
想要实现无论在什么情况下, 定时器都能正常工作,可以这样
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
确实能达到效果,但看起来并不是那么完美. 这是时候可以用另一种 mode
NSRunLoopCommonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
我们先来看下NSRunLoopCommonModes
里默认保存是什么 mode
NSLog(@"%@", [NSRunLoop currentRunLoop]);
打印结果:
由此可知,
NSRunLoopCommonModes
里面就有前面前面需要的两个 mode,这个时候在运行工程, 无论是拖拽还是不拖拽, 定时器都能正常工作了.
CFRunLoopTimerRef
- CFRunLoopTimerRef 是基于时间的触发器, 基本说的都是 NSTimer
- GCD 也有定时器功能, 和 NSTimer 定时器有一些区别, 这个在之前专门学习GCD使用的博客里也有提到(iOS - GCD 编程). 今天采用传统方法创建 GCD 来继续学习下
// 1. 创建 GCD 中的定时器
/*
第一个参数:source 的类型,DISPATCH_SOURCE_TYPE_TIMER 表示定时器
第二个参数:描述信息,线程 ID
第三个参数:更详细的描述信息
第四个参数:队列, 决定 GCD 定时器中的任务在哪个线程中执行
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); // 程序并发队列
// 2. 设置定时器-- 起始时间和间隔时间以及精准度
/*
第一个参数:timer, 定时器对象
第二个参数:起始时间,DISPATCH_TIME_NOW 从现在开始几时
第三个参数:间隔时间 GCD 中的时间单位是纳秒
第四个参数:精准度 绝对精准 0
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
// 3. 设置定时器要执行的任务
dispatch_source_set_event_handler(timer, ^{
NSLog(@"GCD - %@", [NSThread currentThread]);
});
// 4. 启动执行
dispatch_resume(timer);
这个时候调用该方法,发现定时器并没有工作,需要先生命一个 定时器属性
@property (nonatomic, strong) dispatch_source_t timer;
然后在上面代码的最后给属性赋值
self.timer = timer;
这个时候定时器就正常工作了.
GCD 与 NSTimer 比较
优点: GCD 不受 RunLoop 运行运行模式的影响.
CFRunLoopSourceRef
CFRunLoopSourceRef 是事件源, 也就是输入源
以前的方法:
- Port-Based Sources : 基于端口的事件
- Custom Input Sources : 自定义的事件
- Cocoa Perform Selector Sources : perform selector 事件
现在的方法:
- Sources0: 非基于 Port 的, 可以理解为是用户主动触发的事件
- Sources1: 基于 Port 的,可以理解为是系统事件
用户点击按钮事件触发的就是Source0, 应征了上面所说的
CFRunLoopObserverRef
CFRunLoopObserverRef 是观察者, 能够监听 RunLoop 的状态改变
可以监听的事件有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入 Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
声明一个方法
- (void)observer {
// 1. 创建观察者
/*
第一个参数: 怎么分配存储空间
第二个参数: 要监听 RunLoop 的哪些状态
第三个参数: 是否要持续监听
第四个参数: 优先级 0 就可以
第五个参数: 当状态改变的时候会回调 block
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler( CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"即将进入 Loop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即将处理 Timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即将处理 Source");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即将进入休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"刚从休眠中唤醒");
break;
case kCFRunLoopExit:
NSLog(@"即将退出 Loop");
break;
}
});
// 2. 给 RunLoop 添加观察者
/*
第一个参数: 要监听哪个 RunLoop
第二个参数: 观察者
第三个参数: 运行模式, 需要传 C语言方法
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
在给 RunLoop 添加观察者的时候有提到, 运行mode 需要传 C语言方法,其实
NSDefaultRunLoopMode
就等同于 kCFRunLoopDefaultMode
NSRunLoopCommonModes
就等同于 kCFRunLoopCommonModes
运行结果:
我们可以看到 RunLoop 运行的各个状态,加上定时器,效果更加明显一些
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
会看到 RunLoop 状态会重复变化,说明 RunLoop 不会彻底退出,只要有任务,会一直执行
所以,我们可以得出一条结论: 通过监听 RunLoop 的状态可以让其进行相应的操作
RunLoop 处理逻辑
官方版文字介绍
这里找到了网上一个处理流程图文介绍
可以清晰的看出 RunLoop 的整个处理流程, 而 RunLoop 最终的状态就是 休眠 状态, 一旦有任务就会唤醒
RunLoop 应用
RunLoop 介绍的差不多了,来看下它都有哪些应用:
开启常驻线程
常驻线程就是让一个子线程不进入消亡状态, 等待其他线程发来消息,及时处理其他任务事件
下图是没有开启常驻线程时, 线程直接就 end 了,显然不能满足我们的需求
我们可以创建一个定时器,以保证线程不会结束,但这样显然也不完美,因为无论有没有任务,定时器都一直在工作
这时候,我们就可以让 RunLoop 调用 addPort: forMode:
方法,这个时候在运行, 线程就不会 end 了,在有新任务要执行的时候也能及时处理,也就达到了我们的目的.
总结
- RunLoop 概念:
- 从字面意思看:运行循环, 跑圈
- 其实它内部就是一个 do-while 循环, 在这个循环内部不断的处理各种任务(比如 source, timer, observer)
- 一个线程对应一个 RunLoop, 主线程的 RunLoop 默认已经启动, 子线程的 RunLoop 需要手动启动(调用 run 方法)
- RunLoop 只能选个一个mode 启动,如果当前 mode 中没有任何 Source(Source0, Source1), Timer, Observer, 那么就直接退出 RunLoop
- RunLoop自动释放池什么时候创建
- 启动 RunLoop 的时候就创建
- RunLoop自动释放池什么时候释放
- RunLoop 退出的时候释放
- RunLoop 即将进入睡眠的时候会销毁之前的自动释放池. 并重新创建一个新的
- 在开发中如何使用 RunLoop? 什么应用场景?
- 开启一个常驻线程(让一个子线程不进入消亡状态, 等待其他线程发来消息,处理其他事件)
- 在子线程中开启一个定时器
- 在子线程中进行一些长期监控
- 可以控制定时器在特定模式下执行
- 可以让某些事情(行为或者任务)在特定模式下执行
- 可以添加 Observer 监听 RunLoop 的状态, 比如监听点击事件的处理(在所有点击事件之前做一些事情)