1)作用
- 让线程能随时处理事件但并不退出
平常:一个线程一次只能执行一个任务,执行完成后线程就会退出 - 处理 APP 中的各种事件「触摸、定时器、Selector时间」
- 节省 CPU 资源,提高性能:让CPU该做事时做事,该休息时休息
- main 函数启动了 RunLoop「UIApplicationMain 函数里启动,一直没有返回」 程序不会马上退出,保持持续运行状态
2)RunLoop 对象
iOS 有两套 API 访问和使用 RunLoop
- Foundation
类:NSRunLoop「基于 CFRunLoopRef 的一层 OC包装」 - Core Foundation
类:CFRunLoopRef - 桥接 __bridge
Foundation 框架 和 Core Foundation框架类型的转换需要桥接- F类型 → CF类型:
CFStringRef CFDataType = (__bridge NSString*)FDataType
- CF类型 → F类型:
NSString *FDataType = (__bridge CFStringRef)CFDataType
- F类型 → CF类型:
3)RunLoop 和 线程
- 每一条线程都有 唯一 一个与之对应的 RunLoop 对象
- 主线程的 RunLoop自动创建好了,子线的 RunLoop 程需要自己创建
- RunLoop 在第一次获取时创建,在线程结束时销毁
线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直不会创建 RunLoop
4)RunLoop 相关类
I. CFRunLoopModeRef「RunLoop的运行模式」
一个 RunLoop 包含多个 Mode,每个 Mode 里有多个 Source「Set 存储」、Timer、Observer「Array 存储」
如果RunLoop 所有的 Mode 里没有 Source、Timer,RunLoop 会退出「有Observer没用」每次启动 RunLoop,只能指定一种 Mode,这个 Mode 被称作 CurrentMode
要切换 Mode 只能退出 Loop,在重新指定一个 Mode 进入
这是为了分隔开不同组的 Source、Timer、Observer,让其互不干扰
系统默认注册了 5 个Mode
- kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- kCFRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode
- UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到
II. CFRunLoopTimerRef「基于时间的触发器,基本上就是 NSTimer」
1.已经自动添加到 RunLoop 中,默认模式是 kCFRunLoopDefaultMode
// 由于 CFRunLoopTimerRef 和 NSTimer 可以混用,这里使用 NSTimer
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 以下 2 中的代码等价于上面的代码
2.修改模式
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// case1:定时器只运行在 NSDefaultRunLoopMode 下,一旦RunLoop进入其他模式,这个定时器就不会工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// case2:定时器会跑在标记为 common modes 的模式下
// 标记为common modes的模式:UITrackingRunLoopMode 和 kCFRunLoopDefaultMode
[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
III. CFRunLoopSourceRef「事件源,输入源」
按照官方文档分类:
- Port-Based Sources 基于端口,和其他线程交互内核消息
- Custom Input Sources 自定义
- Cocoa Perform Selector Sources 用于处理 performSelector 函数
按照函数调用栈分类:
- Source0:非基于 Port,不能主动触发事件,接收 Source1 分发的事件
- Source1:基于 Port,能主动触发事件,通过内核和其他线程通讯、接收、分发系统事件
IV. 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
};
CF「CoreFundation」的内存管理
- 凡事带有 Create、Copy、Retain等字眼的函数,创建出来的对象,最后都要做一次 release
- release函数:
CFRelease(要释放的对象);
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// CF开头的函数不受 ARC控制,带 Create的 要释放
// 释放Observer
CFRelease(observer);
5)RunLoop 处理事件的步骤
每次运行 RunLoop,线程的RunLoop对自动处理之前未处理的消息,并通知相关的观察者。
I. 步骤如下
看 Mode是否为空,若不空,通知 Observer RunLoop 已经启动<b>(之后创建一个自动释放池)</b>
-
通知 Observer「观察者」
- 即将开始的 定时器「Timer」
- 即将启动的 非基于端口的源「Source0」
启动准备好的任何 非基于端口的源「Source0」
如果 基于端口的源「Source1」 准备好并处于等待状态,立即启动 → 步骤 8
通知 Observer线程 → 休眠 <b>(休眠前会 销毁自动释放池,然后在创建自动释放池)</b>
-
以下任意事件 可以唤醒 已经休眠的程序
- 基于端口的源「Source1」 接收到事件
- 定时器启动
- RunLoop 设置的循环时间超时
- RunLoop 被唤醒
通知 Observer线程 → 唤醒
-
处理 未处理的 事件
- 定义的定时器启动,处理定时器事件,重启RunLoop → 步骤2
- 输入源/时间源 启动,传递信息
- RunLoop 被显式唤醒 且 时间没超过RunLoop固定循环的时间,重启RunLoop → 步骤2
通知 Observer RunLoop 结束<b>(销毁自动释放池)</b>
II. 步骤图例
III. 自动释放池什么时候释放?
通过 Observer监听 RunLoop的状态,一旦监听到RunLoop即将进入睡眠等待状态「kCFRunLoopBeforeWaiting」就释放自动释放池
6)RunLoop 应用
I. 某些事件「行为、任务」在特定模式下执行
大图渲染耗时,这时候多线程多任务可能会造成卡顿,下载完后不急于显示,而是等其他线程不忙时显示
// 只在 NSDefaultRunLoopMode 主线程模式下显示图片,一旦RunLoop进入其他模式,这个函数不会执行
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
II. 常驻线程
适用于:经常要做后台操作,频繁开启线程的情况,让一个线程常驻,可以避免频繁的开启使用线程的麻烦。等待其他线程发消息,处理事件
- 在子线程中开启一个定时器
- 在子线程中进行行为的长期监控
@autoreleasepool{
// 方法一
// Mode 里没有 任何东西的 RunLoop 会马上退出,这里随便加点东西,防止退出
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 开启 RunLoop 线程
[[NSRunLoop currentRunLoop] run];
}
// 方法二、不推荐
while(flag){ [[NSRunLoop currentRunLoop] run]; }
III. 添加 Observer监听 RunLoop的状态
比如,监听点击事件的处理,在所有点击事件之前做一些处理