2021.2 @Hanniya
最近因准备面试,有较多学习内容。计划产出的是有较多我个人理解和知识结构的几篇学习内容:RunLoop、Runtime、AutoreleasePool,本篇是 RunLoop 相关,欢迎各位作为查缺补漏来阅读~
面试思路大纲
是什么
- 是在一个【线程】中,持续调度各种任务的运行循环机制【本质:while循环】
做什么
performTask()
执行任务:Block、Source0、Source1、Main queue、Timercallout_to_observer()
通知外部:Activity、Source0、Timersleep()
睡眠
应用:Timer、线程保活、卡顿检测
1. RunLoop 简介
1.1 作用
- 保持程序持续运行: 程序一启动,在 UIApplicationMain 就会开一个主线程,跑一个和主线程对应的 RunLoop,这个 RunLoop 保证主线程不会被销毁,也就保证了程序的持续运行。
- 处理App中的各种事件,如触摸事件、定时器事件、Selector事件等
- 节省CPU资源,提高程序性能 当没任务时,RunLoop会告诉CPU要去休息,这时CPU就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会去做事
1.2 特点
- 与线程的关系 线程和 RunLoop 之间一一对应,其对应关系保存在一个全局的 Dictionary 里,线程是 key,RunLoop 是 value。
- 生命周期 子线程的 RunLoop 的创建发生在第一次获取时(若创建子线程后不主动获取,则不会创建,可以理解为懒加载),RunLoop 的销毁发生在线程结束时。
- 获取 只能在一个线程的内部获取其 RunLoop(主线程除外)。
//Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
//Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
NSRunLoop 是对 CFRunLoopRef 的一层封装
CFRunLoopRef 的 API 是线程安全的;NSRunLoop 提供了面向对象的 API,但这些 API 不是线程安全的。
开一个子线程时创建 RunLoop,不是通过 alloc init 方法创建,而是直接通过调用 currentRunLoop 方法来创建,因为它本身是一个懒加载。
2. RunLoop 做什么
2.1 performTask() 执行任务
DoBlocks()
- 开发者可使用
DoSources0()
- 开发者可使用
- Source 0 不能主动唤醒 RunLoop
DoSources1()
- 只能系统使用
- Source 1 能够主动唤醒 RunLoop
- 基于 mach_msg 函数,通过读取 port 上内核消息队列的消息来决定执行的任务。
- 任务包括渲染 UI 等
DoMainQueue()
- 开发者可使用,调用 GCD 的 API 将任务放入到 main queue 中
DoTimers()
- 开发者可使用,调用 NSTimer 的 API 即可注册被执行的任务
2.2 callout_to_observer() 通知外部
DoObservers-Activity 当前状态
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //每次进入Runloop(如切换mode后)
kCFRunLoopBeforeTimers = (1UL << 1), //即将DoTimers
kCFRunLoopBeforeSources = (1UL << 2),//即将DoSources
kCFRunLoopBeforeWaiting = (1UL << 5),//当前线程即将进入睡眠(若当前队列无多余消息则进入睡眠)
kCFRunLoopAfterWaiting = (1UL << 6), //当前线程从睡眠中恢复(读出队列消息,继续执行)
kCFRunLoopExit = (1UL << 7), //退出Runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
DoObservers-Timer
表示将要处理Timer
DoObservers-Source0
表示将要处理Source0
2.3 sleep() 睡眠
2.4 综合流程图
Runloop 的每次 loop 不总是按顺序执行上面的各种
performTask
和callout_to_observer
,而是糅合在一起各种跳转
借用mrpeak的图来理解完整的流程:
- Poll:如果处理了source0任务,poll值为true,睡眠前后不会进行通知。
- DoBlocks -> DoSource0 -> (睡眠) -> DoSource1/DoMainQueue/DoTimers -> DoBlocks 循环 睡眠唤醒 RunLoop 后 DoSource1/DoMainQueue/DoTimers 只会三选一
3. RunLoop 原理
3.1 本质:结构体
RunLoop 结构体
struct __CFRunLoop {
...//省略非核心成员
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode; //指向_CFRunLoopMode结构体的指针
CFMutableSetRef _modes; //多个mode数组
};
Mode结构体
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
... //省略非核心成员
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
- 一个RunLoop包含多个Mode,每个Mode又包含多个Source/Timer/Observer
mainQueue 任务的执行和 mode 无关,mode 内没有相关信息
3.2 Mode:CFRunLoopModeRef
Mode 分为 Common Mode 和 Private Mode,所以 observer 并不会监控到所有 Runloop 的动态
- RunLoop 的 Mode 切换
RunLoop启动时选择其中一个Mode作为currentMode;
需要切换Mode时,只能退出RunLoop,再重新指定一个Mode进入,这样做主要是为了分隔开不同组的Source、Timer、Observer,让其互不影响
若当前mode内没有任何Source/Timer/Observer,RunLoop不会空转,会立刻退出。
3.3 Source(0/1)/Timer/Observer
- Source 事件产生
- Source0:包含一个函数指针(回调),接受外界触发的事件,不能主动唤醒RunLoop,只能通过Wakeup接口唤醒RunLoop来处理事件(触摸事件、performSelectors)
- Source1:包含一个mach_port和一个函数指针(回调),能主动唤醒RunLoop(基于Port的线程间通信)
- Timer:定时器,包含一个时间长度和一个函数指针(回调)
- Observer:观察者,包含一个函数指针(回调),通过回调监听RunLoop的状态
3.4 RunLoop 的内存管理
即将进入 RunLoop 时,通过 observer 观察到 kCFRunLoopEntry 状态,主线程 RunLoop 会创建一个 AutoreleasePool。
4. 面试题
4.1 不做处理时当拖动 tableview 时 NSTimer 会响应吗?怎么解决
不会响应。
原因:NSTimer 默认只会调度到 kCFRunLoopDefaultMode,当 scrollView 滑动的时候,runloop 会进入 UITrackingRunLoopMode,那么在 doTimer 的时候自然就不会触发 NSTimer 的任务了
解决办法:
- 将 NSTimer 也加入到 UITrackingRunLoopMode(但这样timer被添加了两次,不是同一个timer)
- 把 NSTimer 加入到 NSRunLoopCommonModes 里,相当于将自己标记为Common,所有也标记为common的mode都会继续处理这个事件。
但即使这样,当 RunLoop 使用系统 private mode 时,也会存在不执行 Timer 的问题。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
4.2 NSTimer 和 GCD 哪个更精准?为什么
CGD 定时器更精准。因为
- NSTimer 是每次 Runloop 检查一次到没到时间,有误差。
RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务且也过了Timer的宽容度,则那个时间点的回调也会跳过去,不会延后执行。
- NSTimer 有可能因为 Mode 问题被延迟处理。
//创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//1.创建一个GCD定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
// 局部变量,让指针强引用
self.timer = timer;
//2.设置定时器的开始时间,间隔时间,精准度
//精准度 一般为0 在允许范围内增加误差可提高程序的性能
//GCD的单位是纳秒 所以要 * NSEC_PER_SEC
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(@"---%@--", [NSThread currentThread]);
});
dispatch_resume(timer); // 启动
4.3 RunLoop 如何响应用户事件、手势、界面刷新
- UIEvent 事件历程: 手指触摸屏幕
- IOKit.framework 封装事件为 IOHIDEvent 对象
- 端口通信:通过 mach port 转发到 APP,主线程 Runloop 中 Source1接收
- Runloop 进行回调(Source1回调 -> Source0)
- Source0 的回调将触摸事件添加到事件队列(FIFO)
- 出队列时 UIApplication 开始寻找最佳响应者(Hit-testing)
- 事件被发送至最佳响应者,进行响应或传递
-
手势: 系统注册了一个 Observer 监测 BeforeWaiting (RunLoop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
-
界面: 当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。 系统注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行函数,会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
-
CADisplayLink 可以理解为一个和屏幕刷新率一致的定时器。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去,造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。
4.4 RunLoop 在第三方库的实际应用
-
AFNetworking AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop。
-
AsyncDisplayKit Facebook 推出的用于保持界面流畅性的框架,将绘制和排版放在后台线程进行。使用 Node 来封装 View 和 Layer,并实现了类似的一套界面更新的机制:在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
5. 关于RunLoop可供复习的精选文章
参考:
解密 Runloop
iOS学习——浅谈RunLoop - 云+社区 - 腾讯云
深入理解RunLoop | Garan no dou
iOS底层原理总结 - RunLoop - 掘金
源码:CFRunLoop.c