浅谈 iOS 中的 RunLoop
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. 步骤图例
Paste_Image.pngIII. 自动释放池什么时候释放?
通过 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的状态
比如,监听点击事件的处理,在所有点击事件之前做一些处理