NSRunLoop底层原理详解
- 正常情况下, 一个线程执行完, 程序就会立即退出, 比如一个命令行项目
NSRunLoop
是iOS
中的消息处理机制,执行完某个事件后线程不会退出,而是进入休眠状态,当再次监测到需要出发事件时,线程激活,继续处理事件,处理完成后再次进入休眠- 这种时间运行循环, 类似于一个
while
循环 - 默认情况下, 不需要我们手动创建
RunLoop
, 因为cocoa
框架为我们创建了一个默认的RunLoop
RunLoop
的主要作用- 保持程序的持续运行
- 处理
App
中的各种事件(手势、定时器、Selector
等) - 节省
CPU
资源、提高程序性能:该做任务的时候做任务,没事干的时候休息
RunLoop
和线程的关系- 每条线程都有唯一的一个与之对应的
RunLoop
对象 RunLoop
保存在一个全局的Dictionary
里, 线程作为key
,RunLoop
作为value
- 线程刚创建时并没有
RunLoop
对象,RunLoop
会在第一次获取线程时创建 RunLoop
会在线程结束时销毁- 主线程的
RunLoop
已经自动获取(创建), 子线程默认没有开启RunLoop
- 每条线程都有唯一的一个与之对应的
RunLoop对象
- 在
iOS
开发中RunLoop
有两套API
框架, 分别是Foundation
的NSRunLoop
Core Foundation
的CFRunLoopRef
CFRunLoopRef
是基于C
语言的开源框架, 有兴趣的可以到源码地址下载源码, 不过没有C
语言功底的只怕很难看懂NSRunLoop
是对CFRunLoopRef
的有一层封装, 是OC
语法的框架- 简单使用, 获取
RunLoop
对象
1 | // 获取当前线程的RunLoop |
RunLoop相关的类
- 因为
NSRunLoop
是不开源的, 但是CFRunLoopRef
却是开源的, 从源码地址下载CFRunLoopRef
的源码 - 在源码中可以看到, 在
Core Foundation
中CFRunLoopRef
有以下5个相关的类CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopRef
CFRunLoopRef
对象的主要核心代码如下
1 | typedef struct __CFRunLoop * CFRunLoopRef; |
主要属性介绍
CFMutableSetRef
是一个无序的集合, 在上面的代码中存储的都是CFRunLoopModeRef
对象- 其中
_modes
存储的是所有的mode
对象 _currentMode
是指当前的mode
CFRunLoopModeRef
1 | typedef struct __CFRunLoopMode *CFRunLoopModeRef; |
_name
: 该__CFRunLoopMode
的名称_sources0
和_sources1
: 一个无序集合, 存储的都是CFRunLoopSourceRef
对象_observers
: 一个有序集合数组,存储的都是CFRunLoopObserverRef
对象_timers
: 一个有序集合数组,存储的都是CFRunLoopTimerRef
对象- 从这里我们可以看出以上几个类之间的关系, 大概可如下图所示
CFRunLoopModeRef
代表RunLoop
的运行模式- 一个
RunLoop
只能对应一个线程, 却包含若干个Mode
,每个Mode
又包含若干个Source0/Source1/Timer/Observer
RunLoop
启动时只能选择其中一个Mode
,作为currentMode
同样只能有一个- 如果需要切换
Mode
,只能退出当前Loop
,再重新选择一个Mode
进入,
不同组的Source0/Source1/Timer/Observer
能分隔开来,互不影响 - 如果
Mode
里没有任何Source0/Source1/Timer/Observer
,RunLoop
会立马退出 - 以下是系统默认的集中
mode
1 | FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode; |
kCFRunLoopDefaultMode
(NSDefaultRunLoopMode
):App
的默认Mode
,通常主线程是在这个Mode
下运行UITrackingRunLoopMode
:界面跟踪Mode
,用于ScrollView
追踪触摸滑动,保证界面滑动时不受其他Mode
影响kCFRunLoopCommonModes
(NSRunLoopCommonModes
): 并不是某一种特定的mode
, 而是通用模式, 包括kCFRunLoopDefaultMode
和UITrackingRunLoopMode
CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者,能够监听RunLoop
所有的状态改变。
可以监听的时间点有如下几种:
1 | /* Run Loop Observer Activities */ |
在主线程监听所有的状态
1 | // 创建observer |
RunLoop消息类型
从上图可以看出消息类型大概可以分出两种, 第一种类型又可以细分为三种, 这三种都是异步执行的
Port
监听程序的mach ports
,ports
可以简单的理解为:内核通过port
这种方式将信息发送,而mach
则监听内核发来的port
信息,然后将其整理,打包发给runloop
Customer
由开发人员自己发送, 苹果也提供了一个CFRunLoopSource
来帮助处理, 简单介绍核心实:
- 定义输入源(数据结构)
- 将输入源添加到runloop,那么这样就有了接受者,即为R1
- 协调输入源的客户端(单独线程),专门监听消息,然后将消息打包成
runloop
能够处理的样式,即第一步定义的输入源。它类似Mach
的功能
Selector Sources
NSObject类提供了很多方法供我们使用,这些方法是添加到runloop的,所以如果没有开启runloop的话,不会运行
1 | /// 主线程 |
- 上面提到的前四个方法是在指定线程运行
aSelector
, 一般情况下aSelector
会添加到指定线程的runloop
- 如果调用线程和指定线程为同一线程,且
wait
参数设为YES
,那么aSelector
会直接在指定线程运行,不再添加到runloop
; - 因为
wait
参数设为YES
, 意味着要等待aSelector
执行完成之后才回去执行后面的逻辑
RunLoop运行逻辑
根据苹果在文档里的说明,RunLoop
内部的逻辑大致如下:
未查看RunLoop
的执行流程, 我们可以新建一个项目, 并简单加一个触发事件, 如下所示
- 如图所示, 添加一个简单的触发事件, 并加上断点, 在打印区域输入
bt
命令后, 就能看到完整的执行流程了 - 从下往上查看, 所执行的相关函数大概流程是:
UIApplicationMain
CFRunLoopRunSpecific
__CFRunLoopRun
__CFRunLoopDoSources0
- 最后就是
[UIResponder touchesBegan:withEvent:]
触发函数了 - 下面的事情就是找到源码, 依次查看所执行的函数了
- 在源码中找到
CFRunLoop.c
文件, 搜索CFRunLoopRunSpecific
方法, 就是核心代码了, 一起来看看吧 - 删除其他不相关的代码, 核心代码大概如下
1 | /// RunLoop的实现, 大概在文件的2622行 |
`RunLoop`
从上面的代码可以看到RunLoop
其内部是一个do-while
循环; 当你调用CFRunLoopRun()
时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回
RunLoop的底层实现
- 从上面代码可以看到,
RunLoop
的核心是基于mach port
的,其进入休眠时调用的函数是mach_msg()
Mach
本身提供的API
非常有限,而且苹果也不鼓励使用Mach
的API
- 但是这些
API
非常基础,如果没有这些API
的话,其他任何工作都无法实施 - 在
Mach
中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象” - 和其他架构不同,
Mach
的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。 - “消息”是
Mach
中最基础的概念,消息在两个端口 (port
) 之间传递,这就是Mach
的IPC
(进程间通信) 的核心。
Mach
的消息定义是在<mach/message.h>
头文件的,很简单:
1 | typedef struct { |
- 一条
Mach
消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口local_port
和目标端口remote_port
- 发送和接受消息是通过同一个
API
进行的,其option
标记了消息传递的方向:
1 | mach_msg_return_t mach_msg( |
- 为了实现消息的发送和接收,
mach_msg()
函数实际上是调用了一个Mach
陷阱(trap
),即函数mach_msg_trap()
,陷阱这个概念在Mach
中等同于系统调用 - 当你在用户态调用
mach_msg_trap()
时会触发陷阱机制,切换到内核态;内核态中内核实现的mach_msg()
函数会完成实际的工作 - 内核态中的
mach_msg()
, 如果没有消息就让线程休眠,有消息就唤醒线程
RunLoop
的核心就是一个mach_msg()
(见上面代码的第7步),RunLoop
调用这个函数去接收消息,如果没有别人发送port
消息过来,内核会将线程置于等待状态- 例如你在模拟器里跑起一个
iOS
的App
,然后在App
静止时点击暂停,你会看到主线程调用栈是停留在mach_msg_trap()
这个地方
NSRunLoop
应用实践
NSTimer问题
解决NSTimer
在滑动时停止工作的问题
- 上文有说到
CFRunLoopMode
主要使用的一般有三种Mode
DefaultMode
是App
平时所处的状态,TrackingRunLoopMode
是追踪ScrollView
滑动时的状态- 当你创建一个
Timer
并加到DefaultMode
时,Timer
会得到重复回调,但此时滑动一个TableView
时,RunLoop
会将mode
切换为TrackingRunLoopMode
,这时Timer
就不会被回调,并且也不会影响到滑动操作 - 下面我来看一下这个例子
1 |
|
- 上面代码中
scheduledTimerWithTimeInterval
方式添加的NSTimer
会默认被添加到DefaultMode
中 - 当程序运行的时候回正常执行定时器的方法
- 当我们正常滚动
UITextView
的时候, 从打印结果可以看到, 定时器停止执行了, 结束滚动UITextView
的时候, 定时器方法会继续执行 - 输出结果如下所示
1 | 2019-08-20 21:46:01.986843+0800 RunLoop[86811:3484205] --------0 |
- 有时你需要一个
Timer
,在两个Mode
中都能得到回调 - 一种办法就是将这个
Timer
分别加入这两个Mode
- 另一种方式,就是将
Timer
加入到顶层的RunLoop
的commonModeItems
中 commonModeItems
被RunLoop
自动更新到所有具有Common
属性的Mode
里去CommonModes
并不是一个真的模式,它只是一个标记
1 | NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { |