Runloop
Runloop是iOS系统中的事件循环,它保证了我们的程序不会在main函数执行完后就被退出,(线程保活),可以粗糙地理解成一个
while(true)
的循环,但它的实现并没有那么简单。实际上它是一个NSRunLoop的对象,在对象内部维护了一个事件循环,当没有事件要处理时,Runloop将线程控制器交给系统,即从用户态->内核态
,当被唤醒时又从内核态->用户态
,实现了在休眠时不占用CPU资源。
基础概念
正如前面的引言提到的,一般程序运行完毕后就会自动退出,比如当我们在Xcode中新建一个macOS的CommandLine项目,当main函数return后程序即运行完毕并退出。
然而,我们的APP显然不能这样,所以我们要让APP可以随时响应而不退出。这样的机制通常使用事件循环(Event Loop)来实现,在iOS中即为Runloop。
与Runtime不同的是,Runloop是一个可实际获取的对象,对应Foundation框架的NSRunloop
类与Core Foundation框架的CFRunloop
类,NSRunloop是基于CFRunloop的上层封装。
Runloop核心
前面我们提到,Runloop可以简单地概括成一个while(true)
的循环,但实际上这样的实现会使CPU进行大量无谓的空转。所以,Runloop机制的核心就是保证线程在有events需要处理时能唤醒,在没有events时能进行休眠。
而实现真正的休眠,是靠没有events时从用户态->内核态
实现的,当有事件时,系统内核通过mach_msg()
或者mach port
方法将事件发送给对应的Runloop,Runloop收到事件后从休眠状态切换到唤醒状态,并从内核态->用户态
。
如何唤醒Runloop
Source
Source是Runloop中一个重要的概念,它代表了在上文中提到的events。
在Runloop中,Source分为两类
- Source0:该类Source是App的内部事件,不具有独立唤醒Runloop的能力。一个Source0需要被处理时,他需要被
CFRunLoopSourceSignal()
函数标记为待处理,并调用CFRunLoopWakeUp
函数来唤醒Runloop,CFRunLoopWakeUp
函数内部通过一个_wakeUpPort
成员变量来唤醒Runloop,推测该变量是一个mach port,Runloop只有通过mach port与mach_msg()才可以唤醒。唤醒后通过调用__CFRunLoopDoSources0
函数来处理Source0事件,并在之后将该事件标记为已处理。 - Source1:该类Source是由硬件事件生成的Source,如触摸、摇晃、旋转等。此类Source可唤醒Runloop。
Timer
使用NSTimer API注册执行的任务,就属于这一类
Observer
某个Observer可以监听runloop的状态变化,并作出反应
Runloop与线程的关系
- Runloop与线程是一一对应的,且子线程的Runloop无法获取到其他子线程的Runloop,一一对应的关系以key-value存储在一个全局字典里。在
CFRunloop
中,只有CFRunloopGetMain
和CFRunloopGetCurrent
两个函数可以获取到Runloop - 主线程会自动创建Runloop以响应事件,但子线程并不会自动创建Runloop。由于NSTimer对象需要加入到Runloop中的mode,所以在子线程中调用
performSel afterdelay
系列方法并不会被调用,因为这些方法都会注册一个NSTimer到Runloop中,而子线程默认情况下是没有Runloop的。 - Runloop会在线程销毁时销毁
CFRunLoopMode
mode是管理着Runloop与source/timer/observer之间的桥梁,在一开始会注册五个mode
- kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
- UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。默认NSTimer是被加入到default mode中的,所以当滑动时Runloop切换到tracking mode,这时default mode中的Timer回调不会被调用,所以NSTimer的精度没有CADisplayLinker高。
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
- kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
- 如果需要将事件加入到多个mode中,则将它注册到commonMode中,该mode实际上是多个mode的集合。
- 出于将source/timer/observer分隔开的目的,RunLoop一次只能运行在一个mode下,当运行时在RunLoop的currentMode属性中会标记当前运行的mode。而当要切换mode时,RunLoop必须先退出,并选中一个mode重新进入,达到切换mode的目的。在切换mode时,被加入到commonModes中的事件会被拷贝一次到运行的mode中。
源码验证
__CFRunLoop
RunLoop在Core Foundation中对应的类是CFRunLoopRef
,其对应的结构是__CFRunLoop
可以看到结构体中包含了上文提到的mode,对应CFRunLoopModeRef
,其结构如下图
在这里我们看到了上文提到的source,observer,timer,这是能唤醒RunLoop的三种类型,当然能独立唤醒RunLoop的只有sources1. Mode与source,observer,timer的关系如下图
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
_CFRunloopGet0()
在Core Foundation中,提供了两个接口来获取Runloop,分别是CFRunloopGetMain
和CFRunloopGetCurrent
,先来看看他们的源码实现。
可以看到,两个函数实际上都是调用了_CFRunLoopGet0()
方法,方法的参数是线程pthread_t
。在CFRunLoopGetCurrent()
中,如果当前线程的Runloop已存在,那么会在_CFGetTSD()
函数中找到并返回。
接下来继续看看_CFRunLoopGet0()
函数,显然这是获取RunLoop的关键函数。先看一下第一部分。
这里注意到__CFRunLoops
变量,它是一个CFMutableDictionaryRef
类型的字典,key为线程,value为CFRunLoopRef
在第一次进入时,_CFRunLoopGet0()
函数先创建了CFMutableDictionaryRef
类型的字典变量,显然这个就是全局RunLoop表了。这里也印证了前文提到的线程与RunLoop一一对应的结论。
接着我们可以看到,这里调用__CFRunLoopCreate()
函数创建了主线程的RunLoop,所以RunLoop是直到被获取时才会被创建,如果不获取便不会被创建。RunLoop创建后以key为线程,value为CFRunLoopRef存储在全局RunLoop表中。
接着看看第二部分,如果不是第一次进入则是走到这个流程的代码。
首先定义了一个loop
变量,从全局RunLoops表__CFRunLoops
中查找线程对应的RunLoop,如果找到了则返回该CFRunLoopRef。
如果在__CFRunLoops
中没有查找到该线程对应的RunLoop,则调用__CFRunLoopCreate()
函数创建RunLoop,并添加到__CFRunLoops
中。
__CFRunLoopSource
在mode中,Source对应__CFRunLoopSource
结构体。在上文我们知道Source是分为Source0和Source1的,而他们其实都是__CFRunLoopSource
,在结构体中,以CFRunLoopSourceContext
来区分不同的Source,其中的version0
和version1
分别对应source0
和source1
。
上图即为CFRunLoopSourceContext
和CFRunLoopSourceContext1
的结构定义,可以看到CFRunLoopSourceContext1与CFRunLoopSourceContext0一个明显的区别就是CFRunLoopSourceContext1具有一个mach_port_t
类型的变量。从这里就可以知道为什么Source0不可以独立唤醒RunLoop而Source1可以,在前文中我们提到只有mach port
和mach_msg()
可以独立唤醒RunLoop。
__CFRunLoopCreate(_CFThreadRef t)
该函数被用来创建RunLoop,在_CFRunloopGet0()
函数中若获取不到线程对应的RunLoop则调用该函数来创建一个新RunLoop。可以看到入参是一个_CFThreadRef
类型的变量,代表着线程,因为线程与RunLoop是一一对应的。
在该函数中进行了对__CFRunLoop
结构的分配内存与初始化,可以看到实际上是调用了_CFRuntimeCreateInstance()
函数创建了CFRunLoopRef
类型的实例,从该函数的名字以及代码可以看到,其实RunLoop的创建是利用Runtime的动态创建类的特性来创建的。
CFRunLoopWakeUp
该函数在CFRunLoop中用来唤醒RunLoop,可以看到在TARGET_OS_MAC
下,函数的关键调用是__CFSendTrivialMachMessage()
函数,该函数使用了CFRunLoop
中的_wakeUpPort
属性。
可以看到在__CFSendTrivialMachMessage
函数内部,的确是使用了mach_msg
的方式来给mach port发送信息,以达到唤醒RunLoop的目的。
Runloop应用之事件响应
从用户触摸屏幕,到我们的app响应这个触摸,中间其实需要经过多步的处理,并且涉及到的是硬件->软件的通信。之前关于Runloop的
Source
中提到Source1
是一类由硬件生成的事件,那么以触摸事件为例子,看看Runloop是怎么处理事件响应的。
事件响应链
首先我们来梳理一下事件响应链。
- 用户触发事件
- 系统将事件转交到对应 APP 的事件队列
- APP 从消息队列头取出事件
- 交由 Main Window 进行消息分发
- 找到合适的 Responder 进行处理,如果没找到,则会沿着 Responder chain 返回到 APP 层,丢弃不响应该事件。
从上面的五步,我们可以看到其实1和2是独立于app外的,需要涉及到硬件,从第3步开始事件才被发送到app内进行处理。
用户触发事件
这一步始于用户点触屏幕,此时系统的IOKit.framework
会生成一个IOHIDEvent
事件,该事件会被Spring board
接收。
事件转发到app
当IOHIDEvent
被接收后,将会通过mach port转发给对应的app进程。而这时,app中一个名为com.apple.uikit.eventfetch-thread
的线程中已经注册了一个Source1,其回调是__IOHIDEventSystemClientQueueCallback()
函数。
消息队列
IOHIDEvent
被mach port转发到对应app进程后,就唤醒了com.apple.uikit.eventfetch-thread
线程的Runloop,并调用了Source1对应的__IOHIDEventSystemClientQueueCallback()
回调。该回调从消息队列中取出event,并将main thread__handleEventQueue
对应的Source0
设置为待处理状态,同时唤醒main thread的Runloop。
消息分发
此时就开始调用__eventQueueSourceCallback
函数进行消息分发,_UIApplicationHandleEventQueue
会把IOHIDEvent
封装成UIEvent
并分发出去,开始hitTest等函数的调用,并且TouchBegan/end/move/cancel等函数都是在该回调中调用的。
In short
用户触发事件, IOKit.framework
生成一个 IOHIDEvent
事件并由 SpringBoard
接收,SpringBoard 会利用 mach port
,产生 source1
,来唤醒目标 APP 的 com.apple.uikit.eventfetch-thread
的 RunLoop。Eventfetch thread
会将 main runloop 中 __handleEventQueue
所对应的 source0
设置为 signalled = Yes
状态,同时唤醒 main RunLoop。mainRunLoop 则调用 __eventQueueSourceCallback
进行事件队列处理。
Runloop与autorelease pool
Runloop会注册几个监听自己状态变化的回调,当Runloop进入一次事件循环时,会调用AutoreleasePoolPage::push()
方法创建一个新的自动释放池并传入哨兵对象,而在即将退出或者休眠时则会将自动释放池中的对象pop出。
写在最后
运行的主要函数CFRunLoopRun()
有点长...留待下次分析
如有错漏,欢迎指出
Tino Wu
more at tinowu.top