RunLoop - 原理
RunLoop 是许多 iOS 开发者都会“假装”理解的概念。相关的概念常看常新,每次都有一番新的收获 (每次都不能彻底理解系列)~
一句话概括 RunLoop 是干啥的
就是一种 Event Loop。通过它来避免程序退出,同时高效地管理和相应各种事件。
这个循环在哪里
随着 Swift 的诞生,Apple 开源了一个跨平台的 Foundation 框架:https://github.com/apple/swift-corelibs-foundation 。我们可以在源码中找到这个循环:
1 | void CFRunLoopRun(void) { /* DOES CALLOUT */ |
可以看到就是一个简单的 do-while 循环,传入了当前的 RunLoop 作为参数。但这个循环并不是真正的 RunLoop 循环。
跑个题:啥是 check_for_fork
当我们 fork 出来一个进程的时候,必须要紧接着调用一个 exec
家族的函数,从而让这个进程变成一个“全新的”进程。否则,包括 CoreFoundation, CoreData 甚至 Cocoa 等基础的框架都会出现异常。这里苹果检测了进程是否是 fork 出来的,如果是,就会调用
1 | __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___YOU_MUST_EXEC__ |
这个断言让程序崩溃。
回归正题。
RunLoop 的获取
我们看到在循环中,调用了 CFRunLoopGetCurrent()
函数来获取当前的 RunLoop,并作为参数传入。那么这个函数里都做了什么呢?
我们都知道苹果不允许我们自己手动创建 RunLoop,除了主线程的 RunLoop 会自动被创建之外,其他线程的 RunLoop 都是在第一次获取的时候被创建出来的。来看一下这个获取当前 RunLoop 对象的函数实现:
1 | CFRunLoopRef CFRunLoopGetCurrent(void) { |
这个函数的返回值是 CFRunLoopRef
,也就是 RunLoop
结构体的指针类型。之后,先尝试调用 _CFGetTSD
函数获取,如果拿不到,再调用 _CFRunLoopGet0
函数。
跑个题:啥是 TSD
TSD 全称 Thread-Specific Data,线程特有数据,有时也叫 Thread-Local Storage, TLS。其中的数据对线程内部透明,而对其他线程屏蔽。使用的时候,可以理解成一个 KV 存储,并可以设定一个 destructor
析构函数指针,会在线程销毁时调用。
每一个进程都持有一个 keys 的数组,数组中,每一个元素包含一个用于指示 key 状态的 flag,和 destructor 函数指针。每一个线程的 TCB 也都含有一个指针数组,其中每个元素和 keys 数组一一对应。TCB 中这个数组的每一个元素指向该线程的 TSD。
所以我们看到,其实 RunLoop 是存储在线程的 TSD 中的。这也就是为什么我们说每个 RunLoop 是和线程一一对应的。而在线程退出的时候,对应的 RunLoop 也会被销毁掉。
继续看一下 _CFRunLoopGet0
函数里都做了什么。这里只保留了一些关键的代码。
1 | // should only be called by Foundation |
困惑
既然 RunLoop 已经被存储到线程的 TSD 里了,为什么还需要用一个字典再来记录一遍线程和 RunLoop 的对应关系呢?
RunLoop 的创建
我们看到如果取不到 RunLoop 的时候,会调用 __CFRunLoopCreate
来创建一个。这个函数的实现比较简单,只是创建了一个 RunLoop 的实例,并赋初值。
循环内部逻辑
现在,描述 RunLoop 的对象已经被创建出来了。每次循环中,它都会被传入到 CFRunLoopRunSpecific
函数里。现在来看一下这个函数中每次都会执行哪些逻辑。这个函数比较长,简化之后核心逻辑是这样的:
1 | SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ |
核心就是根据 modeName 拿到 mode,然后传给 __CFRunLoopRun
函数处理。期间通知观察者循环的进入和退出。
再来看看 __CFRunLoopRun
里面都 run 了哪些逻辑。这里删除了不少代码,只留下核心部分。
1 | static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { |
根据苹果的文档,一次 RunLoop 中处理步骤如下:
- 通知观察者进入 RunLoop
- 通知观察者 ready 的计时器即将触发
- 通知观察者不是基于 port 的 input sources 即将触发
- 触发 ready 的非基于 port 的 sources
- 如果有基于 port 的 sources 已经 ready,直接触发,goto 9
- 通知观察者即将休眠
- 让线程休眠,除非被一些事件唤醒
- 通知观察者线程已经苏醒
- 开始处理事件:
- 如果 timer ready 了,处理并继续循环,回到 2
- 超时等情况退出循环
- 通知观察者 RunLoop 退出了。
关于 source0 和 source1:source1 是基于 port 的事件,是来自其他进程或系统内核的消息。source0 是其余的应用层事件。但有的时候,source1 事件会转交给 source0 来处理,比如触摸事件。当我们触摸屏幕时,会产生硬件中断;操作系统内核会把相关的消息通过 port 发送给应用程序,即 source1 事件;接着这些触摸事件会被丢到事件队列里,再交给 source0 处理。
不出意外的话,后面还会有一篇 RunLoop 的使用~
Ref: