此文章的意图
:当你完全细心阅读之后,对runloop认知,会成为你作为一名ios开发人员潜意识里的一部分
一、官方一张图开始
官方文档开宗介绍
-
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 提供
同步
事件, 事件 按照预定时间或者重复的间隔发生
-
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
CFRunLoopObserverContext 结构体
好了代码有了,不妨做个测试,当下我用的M1电脑 模拟器
此时,下面的控制台是没有任何额外输出的,也就是 打印停在了44次
我不做任何操作 ,控制台依旧是安静的
这个时候 我从键盘上随便按下一个键 (注意:此时模拟器应该在前台
) 控制台打印追加到了 observer 回调 60次, 较上一次,增加了16次,记住这个差值16
接下来控制太依旧安静下来
控制台安安静静,而且模拟器什么也不做,也不触发什么操作,这不就是线程休眠
么
我们按下任意键,控制台接着打印,这不就是线程唤醒
么,观察者收到了runloop的通知,16次通知,具体每次信息,我们没做打印,暂且按下不表,继续往后分析
根据初步测试runloop,通过Core Foundation,我们在主线程注册了一个 runloop 观察者,设置了observer 回调函数,成功接收到了 runloop的通知
起码 我们查探runloop 的方向是确立了,runloop给了一定的响应通知信息
(1)主线程Runloop Observer通知信息
由于我的M1 xcode一查看堆栈就崩溃,所以改用我的x86 mac,打印信息有出入的地方,相信你们可以忽略掉
以下为boserver每次通知堆栈信息,以下是严格按照顺序的,请耐心
… kCFRunLoopoBeforeTimers
… kCFRunLoopBeforeSources
。。。。。。接下来线程休眠
activity -
- 0x20: kCFRunLoopBeforeWaiting
- 0x40: kCFRunLoopAfterWaiting
- 0x1: kCFRunLoopEntry
- 0x2: kCFRunLoopBeforeTimers
- 0x4: kCFRunLoopBeforeSources
- 0x80: kCFRunLoopExit
通过主线程注册observer,我们得到了一个runloop的序列活动流程
你会发现 [runloop run] , kCRunLoopExit 之后,马上又 kCFRunLoopEntry,也就是runloop进入之后,基本上不会退出了,因为退出之后 马上又entry了,感兴趣可以自己测试体验下
这个 [runloop run]
(2)run vs runUntilDate
上面的测试中使用runloop run,线程休眠后, 点击屏幕 控制台是没有打印的,也就是touch事件并没有唤醒线程
真的是这样吗?
此时模拟器是黑的,view还未正常load出来呀,我们验证下
我们在threadMain 方法结束之前添加 一句打印
此时我们发现 [runloop run] 后面的打印并未在控制台打印出来,说明 [runloop run] 直接阻塞了后面代码的执行
改用 runloop runUntilDate:
有些不一样了
我们添加的打印正常执行了 这时并没有阻塞后面代码的执行 窗口不是黑背景了 说明view正常加载了
我们还发现 打印语句之前,最后一次打印的activity 为0x80, 正是 kCFRunLoopExit,也就是runloop退出了,所以后面的打印才可能正常执行
-
有个细节可以关注下
-
线程休眠情况下,按下任意键 发现追加的打印 observer追加回调次数 变为
12
次,还记得上面的16
次么- 如果你自己亲自测试的话,你会发现,activity 并没有出现kCFRunLoopExit
-
既然任意键还能唤醒线程,observer还能收到通知,说明runloop肯定是又entry了,这个时候observer能够收到的通知信息就很有限了 再次entry的通知并没有收到
-
这个疑问,我们就得依赖swift Foundation源码 间接揣测 CoreFoundation 来查看分析了
-
休眠情况下,触摸屏幕,observer追加回调次数 变为
18
次,说明事件不一样 必然会影响 observer通知回调有些差别
-
(3)给 runloop runUntilDate 循环多次看看
!发现第一次runloop runUntilDate之后,runloop 退出,再次执行 runloop runUntilDate,再次exit
原来runloop可以这样操作,这些细节 其实是了解runloop的关键,因为有些摸不着头脑的东西 不仔细揣摩这些细节 是没办法get到的
(4)创建一个timer
加个timer之后,你就会发现,不需要再按键或touch,控制台observer通知回调会自动打印,也就是说 timer会不停唤醒线程
(5)配置长的声明周期线程
为一个长的声明周期线程配置runloop时,最好至少添加一个Input Source 来接收消息
尽管您可以只附加一个timer进入运行循环,但一旦timer触发,它通常会失效,这将导致runloop退出
附加一个重复timer可以使runloop运行更长的时间,但是需要定期触发timer来唤醒线程,这实际上是轮询的另一种形式
相比之下,Input Source等待事件发生,在事件发生之前保持线程睡眠