前言

没有困难的工作,只要勇敢的打工人。

概念

假如你经历过这么一种上班状况,有需求需求开发的时分,开发需求,没有需求开发(小概率工作),下班也没有真实意义上的下班,由于群里随时有线上问题需求呼应,所以得24小时待命,那当你知道 RunLoop 的机制之后,你可能会觉得很亲切。

RunLoop 是 iOS 中的一种机制,来保证你的 app 一向处于能够呼应工作的状况,在有工作做的时分随时呼应,然后没事做的时分歇息,不占用 CPU。

你能够想象你的 app,发动之后就一向运转在一个类似 while(1) { ... } 的循环之中,这样看上去程序如同一向在空转,可是 RunLoop 能够让程序在没有工作履行的时分,进入体系等级的休眠,然后等候工作去触发它,然后再次运转。

这是一个 Event Loop 的概念,根本一切的需求用户交互的体系,比如 Window、Android 等等,都有类似的概念。

RunLoop - 同是天涯打工人

与线程的联系

RunLoop 是和线程一一对应的,app 发动之后,程序进入了主线程,苹果帮咱们在主线程发动了一个 RunLoop。假如是咱们拓荒的线程,就需求自己手动开启 RunLoop,并且,假如你不主动去获取 RunLoop,那么子线程的 RunLoop 是不会开启的,它是懒加载的办法。

别的苹果不允许直接创立 RunLoop,只能经过 CFRunLoopGetMain()CFRunLoopGetCurrent() 去获取,其内部会创立一个 RunLoop 并回来给你(子线程),而它的销毁是在线程结束时。

RunLoop - 同是天涯打工人

结构

在 RunLoop 中有几个概念比较重要,Mode、Observer、Source、Timer,它们的联系如下图:

RunLoop - 同是天涯打工人

Mode

Mode,也便是形式,一个 RunLoop 当时只能处于某一种 Mode 中,就比如当时只能是白天或许夜晚一样。图上能够看到,Mode 之间是互不搅扰的,平行国际,A Mode 中产生的工作与 B Mode 无关。这也是苹果丝滑的一个关键,由于苹果的翻滚和默许状况分别对应两种不同的 Mode,由于 Mode 互不搅扰,所以苹果能够在翻滚时专注处理翻滚时的工作。

RunLoop - 同是天涯打工人

能够自界说 Mode,可是根本不会这样,苹果也为咱们提供了几种 Mode:

  • kCFRunLoopDefaultMode:app 默许 Mode,一般主线程是在这个 Mode 下运转
  • UITrackingRunLoopMode:界面追踪 Mode,比如 ScrollView 翻滚时就处于这个 Mode
  • UIInitializationRunLoopMode:刚发动 app 时进入的第一个 Mode,发动完后不再运用
  • GSEventReceiveRunLoopMode:承受体系工作的内部 Mode,一般用不到
  • kCFRunLoopCommonModes:不是一个真实意义上的 Mode,可是假如你把工作丢到这儿来,那么不论你当时处于什么 Mode,都会触发你想要履行的工作。

根本咱们程序跑起来,你别去动它,画面中止,它就处于一个 kCFRunLoopDefaultMode 状况,当你翻滚它了,它会处于一个 UITrackingRunLoopMode 状况,假如你想要在这两个状况都能呼应同一个工作,那你要么同时添加到这两种 Mode,要么把这件工作放到 kCFRunLoopCommonModes 中去履行。

绝大多数状况下,咱们只会接触到这三种 Mode。

Observer

Observer,调查者,调查啥呢,也很简单,假如 RunLoop 是一名打工人,那肯定是调查它啥时分干活,啥时分摸鱼,啥时分下班。苹果用一个枚举来表示 RunLoop 的打工状况:

/* Run Loop Observer Activities */
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   // 一切的状况
};

开端上班,有活来了干活,干完了歇息,又来活了持续干,下班。

RunLoop - 同是天涯打工人

Timer 和 Source 便是 RunLoop 要干的活。

Timer

从结构的那张图能够看到,Mode 中有一个 Timer 的数组,一个 Mode 中能够有多个 Timer。Timer 其实便是定时器,它的工作原理是,你生成一个 Timer,确定要履行的使命,和多久履行一次,将其注册到 RunLoop 中,RunLoop 就会依据你设定的时刻点,当时刻点届时,去履行这个使命,假如它正在休眠,那么就会先唤醒 RunLoop,再去履行。

当然这个时刻点并不是那么精确,由于 RunLoop 的履行是有一个次序的,要处理的工作也都有一个先后次序,假如时刻点到了,RunLoop 会将 Timer 要履行的工作添加到待履行清单,可是也得等候履行清单前面的工作履行完了才会履行到 Timer 的工作,所以它并不保证一定是准的。

Source

Source 是别的一种 RunLoop 要干的活,看源码的话,Source 其实是 RunLoop 的数据源抽象类,是一个 Protocol,也便是说,只要你继承这个 Protocol,你也能够自己完成自己的 Source(根本不会运用)。

RunLoop 中界说了两种 Version 的 Source。一个叫 Source 苹果,一个叫 Source 香蕉。。。

开玩笑的,一个叫 Source0、一个叫 Source1(是不是觉得和苹果香蕉差不多)。

  • Source0:处理 App 内部工作,App 自己担任办理(触发),如 UIEventCFSocket
  • Source1:由 RunLoop 内核办理,Mach port 驱动,如 CFMackPortCFMessagePort

个人了解是 Source1 能够认为用来作为进程间或许线程间通信的一种办法,比如我这个 RunLoop 在线程 A,线程 B 想给我发点东西 C,经过 port 进行传输,然后体系将传输的东西包装成 Source1,在线程 A 中监听 port 是否有东西传输过来,接纳到后,唤醒 RunLoop 进行处理。

手势的监听,发送网络数据的回调监听,都会被包装成 Source,然后再由 RunLoop 进行处理。

RunLoop 履行流程

RunLoop - 同是天涯打工人
(转自 Runloop-实践开发你想用的运用场景)

  1. 告诉 Observer 现已进入 RunLoop
  2. 告诉 Observer 行将处理 Timer
  3. 告诉 Observer 行将处理 Source0
  4. 处理 Source0
  5. 假如有 Source1,跳到第 9 步(处理 Source1)
  6. 告诉 Observer 行将休眠
  7. 将线程置于休眠状况,直到产生以下工作之一
    • 有 Source0
    • Timer 届时刻履行
    • 外部手动唤醒
    • 为 RunLoop 设定的时刻超时
  8. 告诉 Observer 线程刚被唤醒
  9. 处理待处理工作
    • 假如是 Timer 工作,处理 Timer 并重新发动循环,跳到 2
    • 假如 Source1 触发,处理 Source1
    • 假如 RunLoop 被手动唤醒但没有超时,重新发动循环,跳到 2
  10. 告诉 Observer 行将退出 Loop

实践上 RunLoop 内部便是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一向停留在这个循环里,直到超时或手动中止,该函数才会回来。

默许的超时时刻是一个巨大的数,能够了解为无穷大,也便是不会超时。

也能够看到,RunLoop 内部的工作也是有一个先后次序的,当使命很深重的时分,就可能会呈现定时器禁绝的状况。

之前一向说 do-while,可能会有人担心假如一向是 do-while,那其实线程并没有中止下来,一向在等候。但其实 RunLoop 进入休眠所调用的函数是 mach_msg(),其内部会进行一个体系调用,然后内核会将线程置于等候状况,所以这是一个体系等级的休眠,不必担心 RunLoop 在休眠时会占用 CPU。

RunLoop 的运用

AutoreleasePool

有一个比较经典的标题是:主动开释池是什么时分开端开释的?

它的开释机遇是和 RunLoop 有关的,前面提到过,RunLoop 有几种打工状况,苹果在主线程的 RunLoop 中注册了两个 Observer。

第一个 Observer,监听一个工作,便是 Entry,行将进入 Loop 的时分,创立一个主动开释池,并且给了一个最高的优先级,保证主动开释池的创立产生在其他回调之前,这是为了保证能办理一切的引用计数。

第二个 Observer,监听两个工作,一个 BeforeWaiting,一个 ExitBeforeWaiting 的时分,干两件事,一个开释旧的池,然后创立一个新的池,所以这个时分,主动开释池就会有一次开释的操作,是在 RunLoop 行将进入休眠的时分。Exit 的时分,也开释主动开释池,这儿也有一次开释的操作。

也便是:

  1. 进入 RunLoop,先创立个主动开释池
  2. RunLoop 要歇息了,开释当时的主动开释池,搞个新的
  3. RunLoop 要退出了,开释当时的主动开释池

触控工作的呼应

苹果提早在 App 内注册了一个 Source1 来监听体系工作。

比如,当一个 触摸/锁屏/摇晃 之类的体系工作产生,体系会先包装,包装好了,经过 mach port 传输给需求的 App 进程,传输后,提早注册的 Source1 就会触发回调,然后由 App 内部再进行分发。

  1. 注册一个 Source1 用于接纳体系工作
  2. 硬件工作产生
  3. IOKit.framework 生成 IOHIDEvent 工作并由 SpringBoard 接纳
  4. SpringBoard 用 mach port 转发给需求的 App
  5. 注册的 Source1 触发回调
  6. 回调中奖 IOHIDEvent 包装成 UIEvent 进行处理或分发

RunLoop - 同是天涯打工人

改写界面

咱们都知道改变 UI 的参数后,它并不会立马改写。而它的改写,也是经过 RunLoop 来完成。

当 UI 需求更新,先符号一个 dirty,然后提交到一个大局容器中去。然后,在 BeforeWaitingExit 时,会遍历这个容器,履行实践的制作和调整,并更新 UI 界面。

PerformSelector

当调用 performSelector:afterDelay: 时,其实内部会创立一个定时器,注册到当时线程的 RunLoop 中(假如当时线程没有 RunLoop,这个办法就会失效)。

有时分会看到 afterDelay:0,这样的作用是防止在当时的这个循环中履行,等下一次循环再履行。比如有时分会判别当时的 Mode 是否是 Tracking 或许 Default,为了防止判别过错,会运用 afterDelay:0 的办法将判别延迟到下一次 RunLoop 再履行。

其他

还有其他的运用能够看看 深化了解RunLoop,或许直接上网搜一下,这儿就不列举了。

实战

了解了原理之后,来处理一点实践的问题,UI 线程中,假如呈现深重的使命,就会导致界面卡顿,这类使命一般分为 3 类,排版、制作、UI 目标操作。

排版一般是计算视图巨细、计算文本高度、计算子视图的排版等,便是各种 layout 的计算。 制作一般有文本制作,图片制作、元素制作等。 UI 目标操作便是 UI 目标(如 UIView/CALayer)的创立、设置属性和销毁。

前两种操作咱们能够经过各种办法放到异步线程履行,后边的那种只能在 UI 线程也便是主线程履行,可是咱们能够尽量推延履行的时刻(如在 BeforeWaitingExit 时)。

一个比较能想到的办法便是,在 BeforeWaitingExit 时,这时分能够肯定用户没有在操作界面,RunLoop 是空闲的,在这个时刻点去处理使命,用户是无法感知到的,所以咱们能够自己完成一个监听,就监听这两个时刻点,然后抛出一个回调,去处理咱们的使命。

大家能够参阅 YYKit 中的 YYTranscation,或许这个 RunLoopWorkDistribution 库。

核心思想便是监听主线程的 RunLoop,在 DefaultModeBeforeWaitingExit 时,回调一个办法,然后咱们能够将一些使命放到这个时分去履行。

参阅

深化了解RunLoop

线下共享视频