Runloop原理(一)
此文章的意图
:当你完全细心阅读之后,对runloop认知,会成为你作为一名ios开发人员潜意识里的一部分
一、官方一张图开始
image.png官方文档开宗介绍
-
Run loops are part of the fundamental infrastructure associated with threads.
-
runloop是与线程相关的基础架构的一部分,说白了runloop是与线程密不可分的,离开线程,runloop无从谈起
-
A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events.
-
runloop是一个事件处理循环,你可以使用它安排工作,对接收进来的事件进行统筹处理
-
The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
-
runloop的目的 - 为了达到这样一种效果,有工作就处理,没有工作就休眠
这几句就够了,回答了哲学三问 WDP:What -> runloop是什么?Do -> runloop干嘛用?Purpose -> runloop目的?
二、虽然官方英文描述很晦涩,但是为了准确,还是对官方说明做个解释
(一) runloop 描述
-
runloop的管理不是完全自动的,你必须设计线程代码,在合适的时机启动runloop,对接收进来的事件进行响应。
-
Cocoa(NSRunLoop)和Core Foundation(CFRunLoop)均提供了runloop对象帮助你配置和管理你自己线程的runloop; 你的应用不需要显示创建这些runloop对象
-
每个线程都有一个相关联的runloop对象
-
应用程序框架会自动在主线程上建立并运行run循环,作为应用程序启动过程的一部分
-
只有子线程需要显式地运行runloop
(二)接下来 官方剖析了你如何为你的应用配置runloop
1)意识形态
-
线程进入循环,运行事件处理程序,响应runloop接收到的事件
-
runloop控制如何实现 - 通过while 或者 for循环 来驱动runloop, 运行事件处理程序
-
runloop从两种不同的source接收事件: Input sources 和 Timer sources
-
Input sources 提供
异步
事件, 事件 来自于其他线程或进程 -
Timer sources 提供
同步
事件, 事件 按照预定时间或者重复的间隔发生
image.png
-
Input sources: runloop对象执行 runUntilDate: 方法,然后 runloop 退出
-
Timer sources: 不会引起runloop退出
-
-
runloop接收到Input sources,会触发一个通知
-
对于Input Sources, 如果你想要处理更多,那就自己注册runloop观察者 来接收这个通知
- 你可以通过Core Foundation在你的线程里配置 runloop 观察者
-
2)Run Loop Modes
runloop Mode,可以通俗的来讲两个集合,就是要监视的对象
集合 和 要通知的对象
集合
-
监视的对象,当然就是 两种Sources了, Input 和 Timer
- 为什么要监视,其实可以理解为监听,有事件进来,就处理响应
-
通知的对象,自然就是要通知给 观察者了
- 一般情况下,事件本身不关注自己什么时候被处理,就等着runloop处理,等到什么时候算什么时候, 但架不住好管闲事,比如我就想在处理事件之前打印一个信息 就需要注册观察者接收通知了
每次运行runloop,指定一个mode或使用默认mode, 这样只有与指定mode相关联的 sources(存在两种source)会被监听,同样与mode相关联的observers会被通知
runloop 的几种mode
-
Default
- 默认
-
Connection
- 你应该很少用到这种mode
-
Modal
- Cocoa使用此模式来识别用于模态面板的事件
-
Event tracking
- 在鼠标拖动和其他类型的用户界面交互跟踪期间,Cocoa通过这种模式 来限制传入的事件
-
Common modes (有点费解,仔细理解下)
- 是个集合,Cocoa默认为 集合(或者group) [Default, Modal, Event tracking],Core Foundation默认为[Default]; 如果一个Input Source 与 Default关联,则如果指定mode为 Common modes,同样也就与 Modal关联,也与Event tracking关联,苹果提供了 CFRunLoopAddCommonMode 方法往集合里添加 其他mode
3)Input Sources
Input Sources 往线程交付异步事件,分为两种
-
基于Port的 source监视Mach ports ,由内核自动signal
-
自定义source 监视自定义事件, 由另一个线程手动 signal
在任何时刻,Modes都会影响 Input sources
通常情况下,runloop在 default mode下run,也可以指定 自定义modes
如果Input sources不处于当前关联mode,则它生成的任何event都将保持,直到Input sources处于关联的mode
4)Timer Sources
Timer Sources 在未来一个预设的时间同步地向你的线程交付事件
虽然Timer Sources 产生了一个基于时间的通知,但这个timer并不是一个实时机制
如果Timer sources 不处于当前监视的关联mode,则timer不会被触发
如果timer触发时,runloop正在执行handler处理,则timer自己的handler处理将等待下一次time到来执行
如果runloop没run起来,则timer不会被触发
timer根据计划的时间间隔重新调度自己,并不根据实际触发时间,即使触发时间比计划延时了
-
换句话说就是设定5秒触发一次,从0开始,等到8秒才执行,下一次还会在10秒调度执行
-
如果触发时,已经错过了多个5秒间隔,timer会按照计划的时间间隔,自动空过已错过的计划间隔,也就是错过了多个5秒,比如4个5秒,这4个5秒内,只执行一次
5)Run Loop Observers
Sources VS Runloop Observers
-
Sources在同步或异步事件发生时 触发
-
runloop observers在 runloop本身执行到特殊的位置触发
你可以使用runloop Observers准备你的线程来处理给定的事件
你也可以在线程进入休眠之前准备线程
你可以将runloop Observers与以下事件关联
- The entrance to the run loop. 【进入runloop】
- When the run loop is about to process a timer. 【runloop即将处理timer】
- When the run loop is about to process an input source. 【runloop即将处理input source】
- When the run loop is about to go to sleep. 【runloop即将休眠】
- When the run loop has woken up, but before it has processed the event that woke it up. 【runloop被唤醒时, 但是在runloop处理唤醒它的事件之前】
- The exit from the run loop. 【退出runloop】
你可以通过 Core Foundation 添加 runloop Observers,可以根据自己感兴趣的事件,设置自定义回调 和 活动
创建一个runloop Observer时,你可以指定 是一次性 还是 重复的(once or repeatedly)
- once observer在触发后,将自身从runloop中移除
- repeatedly observer在触发后,仍然附加在runloop中
6)runloop事件序列
事件序列:
-
Notify observers that the run loop has been entered.
通知observer 已经进入runloop
-
Notify observers that any ready timers are about to fire.
通知observer 即将处理timer
-
Notify observers that any input sources that are not port based are about to fire.
通知observer 即将处理非基于port的 input source
-
Fire any non-port-based input sources that are ready to fire.
通知observer 处理 非基于port的 input source
-
If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
如果基于port的 input source 已ready,等待触发,则立即处理事件。执行步骤9
-
Notify observers that the thread is about to sleep.
通知observer 线程即将休眠
-
Put the thread to sleep until one of the following events occurs:
线程休眠 直到以下几个事件之一发生
-
An event arrives for a port-based input source.
基于port的 input source事件到来
-
A timer fires.
timer 触发
-
The timeout value set for the run loop expires.
runloop 设置的超时 过期
-
The run loop is explicitly woken up.
runloop 被显式唤醒
-
-
Notify observers that the thread just woke up.
通知observer 线程被唤醒
-
Process the pending event.
处理挂起的事件
-
If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
如果用户定义的timer触发,处理timer事件,并重启runloop 跳转2
-
If an input source fired, deliver the event.
如果input source触发,交付事件
-
If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
如果runloop被显式唤醒,但还没有超时,则重启runloop 跳转2
-
-
Notify observers that the run loop has exited.
通知observer 退出runloop
由于timer和input sources的observer通知 在事件发生之前,所以通知和事件实际发生有时间缝隙
可以用sleep 和 awake-from-sleep 通知来帮助关联实际事件之间的间隔
由于timer和其他周期性事件是在runloop run时交付的,因此绕过该循环将中断这些事件的交付
7)何时使用runloop
主线程runloop自动启动,你不需要主动调用run
子线程
-
如果运行长时任务,很可能要避免启动runloop
-
以下情形 启动runloop
-
Use ports or custom input sources to communicate with other threads
使用端口或 自定义input sources与其他线程通讯
-
Use timers on the thread.
线程中使用timers
-
Use any of the
performSelector
… methods in a Cocoa application.Cocoa应用程序中 使用 任何performSelector 方法
-
Keep the thread around to perform periodic tasks
保持线程执行周期性任务
-
8)创建一个runloop observer
image.png
image.png
CFRunLoopObserverContext 结构体
image.png
好了代码有了,不妨做个测试,当下我用的M1电脑 模拟器
image.png
此时,下面的控制台是没有任何额外输出的,也就是 打印停在了44次
我不做任何操作 ,控制台依旧是安静的
这个时候 我从键盘上随便按下一个键 (注意:此时模拟器应该在前台
) 控制台打印追加到了 observer 回调 60次, 较上一次,增加了16次,记住这个差值16
接下来控制太依旧安静下来
控制台安安静静,而且模拟器什么也不做,也不触发什么操作,这不就是线程休眠
么
我们按下任意键,控制台接着打印,这不就是线程唤醒
么,观察者收到了runloop的通知,16次通知,具体每次信息,我们没做打印,暂且按下不表,继续往后分析
根据初步测试runloop,通过Core Foundation,我们在主线程注册了一个 runloop 观察者,设置了observer 回调函数,成功接收到了 runloop的通知
起码 我们查探runloop 的方向是确立了,runloop给了一定的响应通知信息
(1)主线程Runloop Observer通知信息
由于我的M1 xcode一查看堆栈就崩溃,所以改用我的x86 mac,打印信息有出入的地方,相信你们可以忽略掉
以下为boserver每次通知堆栈信息,以下是严格按照顺序的,请耐心
image.png image.pngimage.png
image.png
image.png
image.png
image.png
image.png
image.png
..... kCFRunLoopoBeforeTimers
..... kCFRunLoopBeforeSources
image.png。。。。。。接下来线程休眠
image.png image.pngactivity -
- 0x20: kCFRunLoopBeforeWaiting
- 0x40: kCFRunLoopAfterWaiting
- 0x1: kCFRunLoopEntry
- 0x2: kCFRunLoopBeforeTimers
- 0x4: kCFRunLoopBeforeSources
- 0x80: kCFRunLoopExit
通过主线程注册observer,我们得到了一个runloop的序列活动流程
image.png你会发现 [runloop run] , kCRunLoopExit 之后,马上又 kCFRunLoopEntry,也就是runloop进入之后,基本上不会退出了,因为退出之后 马上又entry了,感兴趣可以自己测试体验下
这个 [runloop run]
(2)run vs runUntilDate
上面的测试中使用runloop run,线程休眠后, 点击屏幕 控制台是没有打印的,也就是touch事件并没有唤醒线程
真的是这样吗?
此时模拟器是黑的,view还未正常load出来呀,我们验证下
我们在threadMain 方法结束之前添加 一句打印
image.png此时我们发现 [runloop run] 后面的打印并未在控制台打印出来,说明 [runloop run] 直接阻塞了后面代码的执行
改用 runloop runUntilDate:
image.png有些不一样了
我们添加的打印正常执行了 这时并没有阻塞后面代码的执行 窗口不是黑背景了 说明view正常加载了
我们还发现 打印语句之前,最后一次打印的activity 为0x80, 正是 kCFRunLoopExit,也就是runloop退出了,所以后面的打印才可能正常执行
-
有个细节可以关注下
-
线程休眠情况下,按下任意键 发现追加的打印 observer追加回调次数 变为
12
次,还记得上面的16
次么- 如果你自己亲自测试的话,你会发现,activity 并没有出现kCFRunLoopExit
-
既然任意键还能唤醒线程,observer还能收到通知,说明runloop肯定是又entry了,这个时候observer能够收到的通知信息就很有限了 再次entry的通知并没有收到
-
这个疑问,我们就得依赖swift Foundation源码 间接揣测 CoreFoundation 来查看分析了
-
休眠情况下,触摸屏幕,observer追加回调次数 变为
18
次,说明事件不一样 必然会影响 observer通知回调有些差别
-
(3)给 runloop runUntilDate 循环多次看看
image.png发现第一次runloop runUntilDate之后,runloop 退出,再次执行 runloop runUntilDate,再次exit
原来runloop可以这样操作,这些细节 其实是了解runloop的关键,因为有些摸不着头脑的东西 不仔细揣摩这些细节 是没办法get到的
(4)创建一个timer
image.png加个timer之后,你就会发现,不需要再按键或touch,控制台observer通知回调会自动打印,也就是说 timer会不停唤醒线程
(5)配置长的声明周期线程
为一个长的声明周期线程配置runloop时,最好至少添加一个Input Source 来接收消息
尽管您可以只附加一个timer进入运行循环,但一旦timer触发,它通常会失效,这将导致runloop退出
附加一个重复timer可以使runloop运行更长的时间,但是需要定期触发timer来唤醒线程,这实际上是轮询的另一种形式
相比之下,Input Source等待事件发生,在事件发生之前保持线程睡眠