2021.2 @Hanniyahtml
最近因准备面试,有较多学习内容。计划产出的是有较多我我的理解和知识结构的几篇学习内容:RunLoop、Runtime、AutoreleasePool,本篇是 RunLoop 相关,欢迎各位做为查缺补漏来阅读~ios
是什么web
作什么面试
performTask()
执行任务:Block、Source0、Source一、Main queue、Timercallout_to_observer()
通知外部:Activity、Source0、Timersleep()
睡眠应用:Timer、线程保活、卡顿检测数组
程序一启动,在 UIApplicationMain 就会开一个主线程,跑一个和主线程对应的 RunLoop,这个 RunLoop 保证主线程不会被销毁,也就保证了程序的持续运行。安全
当没任务时,RunLoop会告诉CPU要去休息,这时CPU就会将其资源释放出来去作其余的事情,当有事情作的时候RunLoop就会去作事markdown
线程和 RunLoop 之间一一对应,其对应关系保存在一个全局的 Dictionary 里,线程是 key,RunLoop 是 value。app
子线程的 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 方法来建立,由于它自己是一个懒加载。
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
};
复制代码
表示将要处理Timer
表示将要处理Source0
Runloop 的每次 loop 不老是按顺序执行上面的各类
performTask
和callout_to_observer
,而是糅合在一块儿各类跳转
借用mrpeak的图来理解完整的流程:
睡眠唤醒 RunLoop 后 DoSource1/DoMainQueue/DoTimers 只会三选一
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;
};
复制代码
mainQueue 任务的执行和 mode 无关,mode 内没有相关信息
Mode 分为 Common Mode 和 Private Mode,因此 observer 并不会监控到全部 Runloop 的动态
RunLoop启动时选择其中一个Mode做为currentMode;
须要切换Mode时,只能退出RunLoop,再从新指定一个Mode进入,这样作主要是为了分隔开不一样组的Source、Timer、Observer,让其互不影响
若当前mode内没有任何Source/Timer/Observer,RunLoop不会空转,会马上退出。
即将进入 RunLoop 时,经过 observer 观察到 kCFRunLoopEntry 状态,主线程 RunLoop 会建立一个 AutoreleasePool。
不会响应。
缘由:NSTimer 默认只会调度到 kCFRunLoopDefaultMode,当 scrollView 滑动的时候,runloop 会进入 UITrackingRunLoopMode,那么在 doTimer 的时候天然就不会触发 NSTimer 的任务了
解决办法:
但即便这样,当 RunLoop 使用系统 private mode 时,也会存在不执行 Timer 的问题。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
复制代码
CGD 定时器更精准。由于
RunLoop为了节省资源,并不会在很是准确的时间点回调这个Timer。Timer 有个属性叫作 Tolerance (宽容度),标示了当时间点到后,允许有多少最大偏差。
若是某个时间点被错过了,例如执行了一个很长的任务且也过了Timer的宽容度,则那个时间点的回调也会跳过去,不会延后执行。
//建立队列
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); // 启动
复制代码
手指触摸屏幕
系统注册了一个 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 界面。
能够理解为一个和屏幕刷新率一致的定时器。若是在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去,形成界面卡顿的感受。在快速滑动TableView时,即便一帧的卡顿也会让用户有所察觉。
AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其但愿能在后台线程接收 Delegate 回调。AFNetworking 单首创建了一个线程,并在这个线程中启动了一个 RunLoop。
Facebook 推出的用于保持界面流畅性的框架,将绘制和排版放在后台线程进行。使用 Node 来封装 View 和 Layer,并实现了相似的一套界面更新的机制:在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历全部以前放入队列的待处理的任务,而后一一执行。
参考:
解密 Runloop
iOS学习——浅谈RunLoop - 云+社区 - 腾讯云
深刻理解RunLoop | Garan no dou
iOS底层原理总结 - RunLoop - 掘金
源码:CFRunLoop.c