RunLoop 从理解到应用

2021.2 @Hanniyahtml

最近因准备面试,有较多学习内容。计划产出的是有较多我我的理解和知识结构的几篇学习内容:RunLoop、Runtime、AutoreleasePool,本篇是 RunLoop 相关,欢迎各位做为查缺补漏来阅读~ios

面试思路大纲

是什么web

  • 是在一个【线程】中,持续调度各类任务的运行循环机制【本质:while循环】

作什么面试

  1. performTask() 执行任务:Block、Source0、Source一、Main queue、Timer
  2. callout_to_observer() 通知外部:Activity、Source0、Timer
  3. sleep() 睡眠

应用:Timer、线程保活、卡顿检测数组

1. RunLoop 简介

1.1 做用

  • 保持程序持续运行:

程序一启动,在 UIApplicationMain 就会开一个主线程,跑一个和主线程对应的 RunLoop,这个 RunLoop 保证主线程不会被销毁,也就保证了程序的持续运行。安全

  • 处理App中的各类事件,如触摸事件、定时器事件、Selector事件等
  • 节省CPU资源,提升程序性能

当没任务时,RunLoop会告诉CPU要去休息,这时CPU就会将其资源释放出来去作其余的事情,当有事情作的时候RunLoop就会去作事markdown

1.2 特色

  • 与线程的关系

线程和 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 方法来建立,由于它自己是一个懒加载。

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 不老是按顺序执行上面的各类 performTaskcallout_to_observer,而是糅合在一块儿各类跳转

借用mrpeak的图来理解完整的流程: rl00.png

  • 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 的任务了

解决办法:

  1. 将 NSTimer 也加入到 UITrackingRunLoopMode(但这样timer被添加了两次,不是同一个timer)
  2. 把 NSTimer 加入到 NSRunLoopCommonModes 里,至关于将本身标记为Common,全部也标记为common的mode都会继续处理这个事件。

但即便这样,当 RunLoop 使用系统 private mode 时,也会存在不执行 Timer 的问题。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
复制代码

4.2 NSTimer 和 GCD 哪一个更精准?为何

CGD 定时器更精准。由于

  1. NSTimer 是每次 Runloop 检查一次到没到时间,有偏差。

RunLoop为了节省资源,并不会在很是准确的时间点回调这个Timer。Timer 有个属性叫作 Tolerance (宽容度),标示了当时间点到后,允许有多少最大偏差。
若是某个时间点被错过了,例如执行了一个很长的任务且也过了Timer的宽容度,则那个时间点的回调也会跳过去,不会延后执行。

  1. 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 事件历程:

手指触摸屏幕

  1. IOKit.framework 封装事件为 IOHIDEvent 对象
  2. 端口通讯:经过 mach port 转发到 APP,主线程 Runloop 中 Source1接收
  3. Runloop 进行回调(Source1回调 -> Source0)
  4. Source0 的回调将触摸事件添加到事件队列(FIFO)
  5. 出队列时 UIApplication 开始寻找最佳响应者(Hit-testing)
  6. 事件被发送至最佳响应者,进行响应或传递
  • 手势:

系统注册了一个 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

相关文章
相关标签/搜索