[toc]
## RunLoop是什么?有什么作用?
`RunLoop`:翻译过来是运行环路(中式翻译: 跑圈)。我们在创建命令行项目和创建ios项目时,发现命令行项目当最后一行代码执行完后项目就自动退出了,而ios项目确可以一直运行,知道用户手动点击退出按钮。这就是因为ios项目在main函数中自动创建了runLoop,从而可以使项目可以一直响应用户的操作。
```
int main(int argc, char * argv[]) {
@autoreleasepool {
//这行代码 会自动创建主线程的RunLoop
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
```
我们可以将这个过程我们可以简化成:
![](https://img.kancloud.cn/3a/ab/3aab8e377e133f62cb08fcdf96c468c9_891x487.png)
我们从这个过程可以看出RunLoop的基本作用:
- 保持程序的持续运行
- 处理App中的各种事件(比如触摸事件、定时器事件等)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
- ......
我们平时开发中,涉及到RunLoop的挺多的,比如说定时器、手势识别、网络请求等等.
![](https://img.kancloud.cn/78/df/78dfa2947f327a8ae0ba4280dfdedbfa_750x500.png)
## 一、RunLoop的结构
iOS中有2套API来访问和使用RunLoop:
>① Foundation:`NSRunLoop`,它是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API `不是线程安全`的。
>② Core Foundation:`CFRunLoopRef`,它提供了纯 C 函数的 API,所有这些 API都是`线程安全`的。([CFRunLoopRef](https://opensource.apple.com/tarballs/CF/)是开源的)
两者关系:
![](https://img.kancloud.cn/b3/ae/b3ae06021348f6257baa614e28ae11b5_592x416.png)
所以我们获取RunLoop对象也有两种方法:
```
// Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
// Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
```
因为CFRunLoopRef是开源的,所以我们可以通过它来看一下它的实现结构。来到CFRunLoop.c文件中,找到了RunLoop的结构体定义:
```
//已剔除非必要部分struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};
```
>这里的Set和数组类似,只不过数组是有序的,而set是无序的,都是用来存放数据的,所以 CFMutableSetRef可以理解成可变数组,也就是说在一个RunLoop对象中,存储着一个线程对象,三个可变数组,一个当前模式。
**那么`CFRunLoopModeRef`又是什么呢**?我们找到了它的定义:
```
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
//剔除了其他无关属性
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
```
**所以RunLoop的结构是这样的:**
![](https://img.kancloud.cn/fc/9b/fc9b12191aa06c5bb986f318dcb1fe0e_1676x658.png)
- `_pthread`就是RunLoop对应的`线程`,每条线程都有唯一的一个与之对应的RunLoop对象。
- `_commonModeItems`和`_commonModes`是用来存放某些特定模式和模式内事件的,接下来会讲到。
- `_currentMode`,RunLoop当前所处的模式,当前模式是从`_modes`里面选择的。
- `_modes`:RunLoop的运行模式,一共有`五种`,但是我们经常用的就两三种:
>- `kCFRunLoopDefaultMode`: App的默认运行模式,通常`主线程`是在这个运行模式下运行
>- `UITrackingRunLoopMode`: `跟踪用户交互事件`(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)页面滚动式所处的模式
>- `kCFRunLoopCommonModes`: `伪模式`,不是一种真正的运行模式
>- UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用
>- GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
我们上面提到的`_commonModeItems`和`_commonModes`就是存放`kCFRunLoopCommonModes`这种模式的数据的。CommonModes其实并不是一种真正的模式,而是指可以在标记为`Common Modes`的模式下运行的`伪模式`。
简单来说目前kCFRunLoopCommonModes就是指: `kCFRunLoopDefaultMode+UITrackingRunLoopMode`。比如,我们经常遇到在tableview添加定时器后,当tableview滚动后timer就不响应了。
>这是因为tableview滚动式处在`UITrackingRunLoopMode`模式下的,而定时器默认是处在`kCFRunLoopDefaultMode`下的,所以当模式切换后,RunLoop就无法响应之前模式的时间了,故而无法响应定时器时间。所以我们的方案是将定时器添加到RunLoop的`kCFRunLoopCommonModes`模式下,这样无论是否滑动tableview都可以响应定时器事件了。
>这里还需要注意的一点是:如果需要`切换 Mode`,只能`退出Loop`,再重新指定一个 Mode 进入。这样做主要是为了`分隔`开不同组的 `Source/Timer/Observer`,让其互不影响。
**接下来,我们再来看一下这个RunLoop中的模式指的是什么?有什么作用?**
我们前面通过源码,看到了`CFRunLoopMode`的结构,里面有`sources0、sources1、timer、observers`,其实这里面就存储着app要处理的种种事情,它们分别负责不同的工作。它们的分工是这样的:(个人认为sources0和sources1其实是一个整体,当事件发生时sources1先去获取这个时间,涉及不到端口或内核或其他线程的事情的话就交给sources0处理,其余的自己处理)
- `sources0`:只包含了一个`回调`(函数指针),它并不能主动触发事件,比如点击事件等操作都是通过sources0处理的。
- `sources1`:包含了一个 `mach_port` 和一个`回调`(函数指针),用于通过内核和其他线程相互发送消息,这种 Source 能`主动唤醒` RunLoop 的线程。
- `timer`:是基于`时间`的`触发器`,其包含一个`时间长度`和一个`回调`(函数指针)。当其加入到 RunLoop 时,RunLoop会`注册`对应的时间点,当时间点到时,RunLoop会被`唤醒`以`执行`那个回调。
- `observers`:是`观察者`,当 RunLoop 的`状态`发生变化时,观察者就能通过回调接收到这个变化。
RunLoop的状态有一下几种:
![](https://img.kancloud.cn/fc/74/fc746e215c2dda7797e1f734e5930c48_1262x517.png)
**总结 `CFRunLoopMode`的图示:**
![](https://img.kancloud.cn/3b/70/3b7019d4b94582d8dd112466c5f6c59d_954x824.png)
>需要注意的一点是:如果Mode里没有任何`Source0/Source1/Timer/Observer`,RunLoop会`立马退出`。
## 二、RunLoop与线程
关于RunLoop与线程的关系,我们可以总结以下几点:
>1. 每条线程都有`唯一`的一个`与之对应`的RunLoop对象 (也就是RunLoop宿主于线程)
>2. 线程刚创建时并没有RunLoop对象,`RunLoop会在第一次获取它时创建`
>3. `主线程的RunLoop已经自动获取(创建)`,`子线程默认没有开启RunLoop`,子线程没有开启RunLoop的话就跟命令行项目一样,任务执行完就会结束
>4. RunLoop保存在一个`全局的Dictionary里`,`线程作为key`,`RunLoop作为value`
>5. RunLoop会在线程结束时`销毁 `
接下来,我们通过源码来验证:
当我们获取线程的Runloop的时候,发现RunLoop没有获取到话,都会调用`__CFRunLoopGet0`, 并把线程作为参数传递
![](https://img.kancloud.cn/be/88/be88039a30e46af90e6a99e73f0cc259_1080x434.png)
继续,跳转至__CFRunLoopGet0,如下:
![](https://img.kancloud.cn/ec/bf/ecbff424b5bcab4a6a25e8976cf69815_1080x792.png)
>发现,RunLoop与线程的关系是`一对一`的,并且用了个`全局字典`保存了起来,`线程作为key`,`RunLoop作为value`。
我们发现如果线程没有启用RunLoop后会执行完马上销毁:
![](https://img.kancloud.cn/ee/1a/ee1a0718a00d4fd3293610eb0a8fd164_1334x600.png)
>添加RunLoop后,发现还是运行完就销毁:这是因为如果`Mode里`没有任何`Source0/Source1/Timer/Observer`,RunLoop会立马退出
![](https://img.kancloud.cn/f9/f3/f9f362694ce948e4f112b0c62377d917_1286x816.png)
所以我们需要往`Model中`添加一个数据:
![](https://img.kancloud.cn/eb/cf/ebcf0778e7820a1872af3d064e0f4dcf_1600x894.png)
发现确实执行完后,线程阻塞了,一直没有被销毁,这是因为当runloop创建后,如果没有被事件唤醒后它就一直在`休眠`,cpu就不会继续处理事情,所以`阻塞`在这。
## 三、RunLoop的运行逻辑
我们在了解RunLoop的结构以及与线程的关系后,我们再来看一下RunLoop的运行流程:
![](https://img.kancloud.cn/69/d8/69d8ad58baa25db1b0b3430227b8c9ad_1345x703.png)
**接下来,我们通过源码来看一下RunLoop是如何处理这些事件的?**
关于入口的查找,我们可以现在touchesBegan:方法中打个断点,查看程序是怎么执行到这的:
![](https://img.kancloud.cn/84/12/8412d35a6fcdc27906af370074e0e0d6_1778x876.png)
```
//RunLoop入口
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
//通知Observers 进入RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
//RunLoop的具体运行
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//通知Observers 退出RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
//RunLoop的具体运行
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
//通知Observers 即将处理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//通知Observers 即将处理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//处理Block
__CFRunLoopDoBlocks(rl, rlm);
//处理Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
//处理Block
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 如果当前是主线程的runloop,并且主线程有事情需要处理,则跳转至handle_msg处理,即跳过休眠 这条指令网上大部分说法是指判断Sources1中是否有事情处理,个人觉得这个说法不太对,这篇文章中有正面:资料
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
//通知Observers 即将休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//开始休眠
__CFRunLoopSetSleeping(rl);
//等待别的消息来唤醒当前线程 如果没有消息就会一直在这休眠 阻塞在这 cpu不工作 有消息的话则唤醒执行
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
//结束休眠
__CFRunLoopUnsetSleeping(rl);
//通知Observers 结束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//handle_msg
handle_msg:;
if (被timer唤醒) {
//处理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
}
else if (被gcd唤醒) {
//处理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {//被sources1唤醒
//处理Sources1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
//处理Block
__CFRunLoopDoBlocks(rl, rlm);
//处理返回值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
```
简化成流程图 则是:
![](https://img.kancloud.cn/14/4a/144a09f3c28d6edd4bfc29792db981bf_1323x679.png)
## 四、RunLoop的应用
- [控制线程生命周期(线程保活)](https://www.cnblogs.com/chglog/p/9585068.html)
- [解决NSTimer在滑动时停止工作的问题](https://blog.csdn.net/M316625387/article/details/83270639)
- 监控应用卡顿
- 性能优化
- 前言
- WebRTC知识集
- iOS 集成WebRTC各知识点小集
- iOS WebRTC集成时遇到的问题总结
- WebRTC多人音视频聊天架构及实战
- iOS端 使用WebRTC实现1对1音视频实时通话
- iOS 基于WebRTC的点对点音视频通信 总结篇
- WebRTC Native 源码导读 - iOS 相机采集实现分析
- OC 底层原理
- OC runtime 运行时详解
- GCD dispatch_queue_create 创建队列
- iOS底层 Runtime深入理解
- iOS底层 RunLoop深入理解
- iOS底层 Block的本质与使用
- iOS内存泄漏
- iOS中isKindOfClass和isMemberOfClass
- 从预编译的角度理解Swift与Objective-C及混编机制
- 移动支付集成
- iOS 微信支付集成及二次封装
- iOS 支付宝支付 Alipay集成及二次封装
- iOS Paypal 贝宝支付集成及二次封装
- iOS 微信、支付宝、银联、Paypal 支付组件封装
- iOS 微信、支付宝、银联支付组件的进一步设计
- iOS 组件化
- iOS 组件化实施过程
- iOS 组件化的二进制化
- 使用pod package打包framework 实现组件的二进制化
- iOS 自制Framework 获取指定bundle并读取里面的资源
- .podSpec文件相关知识整理
- 开发并上传静态库到CocoaPods
- pod引用第三方库的几种方式
- 如何在.podspec 文件中添加对本地库的依赖
- lipo 命令合并真机与模拟器生成的framework
- iOS多线程
- NSOperation相关知识点
- 自定义NSOperation
- ios多个网络请求之间的并行与串行场景的处理
- iOS动画
- ios animation 动画学习总结
- CABasicAnimation使用总结
- UITableView cell呈现的动效整理
- CoreAnimation动画使用详解
- iOS音视频开发
- iOS 音视频开发之AVCaptureMetadataOutput
- iOS操作本地视频 - 获取,压缩,取第一帧
- 使用 GPUImage 实现一个简单相机
- 直播App架构及思维导图
- 如何快速的开发一个完整的iOS直播app
- iOS视频拖动预览及裁剪
- iOS 直播流程概述
- iOS直播:评论框与粒子系统点赞动画
- iOS音视频开发 - 采集
- 基于AVFoundation实现视频录制的两种方式
- Swift知识集
- Swift 的枚举、结构体和类详解
- Swift 泛型详解
- Swift属性的包装器@PropertyWrapper
- SwiftHub项目 之网络层封装的一点见解
- Moya+RxSwift+HandyJson 实现网络请求及模型转换
- Swift开发小记(含面试题)
- RxSwift 入坑手册 - 基础概念
- 理解 Swift 中的元类型:.Type 与 .self
- Swift HandyJSON库中的类型相互转换的实现
- Swift 中使用嵌套结构体定义一组相关的常量
- Swift Type-Erased(类型擦除)
- Swift中的weak和unowned关键字
- Swift 中的错误处理
- Swift中的Result 类型的简单介绍
- Swift Combine 入门导读
- Swift CustomStringConvertible 协议的使用
- 跨平台
- Cordova跨平台方案 iOS工程创建的步骤
- 使用Cordova 打包WebApp为原生应用详解 (加壳封装)
- RAC响应式编程
- 快速上手ReactiveCocoa之基础篇
- RAC ReactiveCocoa 使用小集
- 优雅的 RACCommand
- 三方库集成及使用
- 融云IM iOS sdk 集成 一篇就够了
- iOS YYTextView使用笔记
- iOS YYLabel使用笔记
- iOS 苹果集成登录及苹果图标的制作要求
- iOS 面向切面编程 Aspects 库的使用
- VKMsgSend库对oc runtime的封装
- OC Protocol协议分发器
- iOS 高德地图实现大头针展示,分级大头针,自定制大头针,在地图上画线,线和点共存,路线规划(驾车路线规划),路线导航,等一些常见的使用场景
- 工作总结
- 自定义UINavigationBar 适配iOS11, iOS15的问题
- SFSafariViewController 加载的网页与原生oc之间的交互
- UICollectionView 设置header的二种方法
- UIPanGestureRecognizer进行视图滑动并处理手势冲突
- OC与Swift混编 注意事项
- UICollectionView 设置水平滑动后,调整每个Item项的排列方式
- oc 下定义字符串枚举
- 高性能iOS应用开发中文版读书笔记
- iOS 图集滑动到最后时添加“显示更多”效果的view组件 实现
- CocoaPods 重装
- WKWebview使用二三事
- IOS电商首页如何布局
- iOS中的投屏方案
- CGAffineTransform 介绍
- 用Block实现链式编程
- iOS 本地化简明指南
- iOS 检查及获取相机、麦克风、相册、位置等权限
- iOS 手势UIGestureRecognizer详解
- ios 编译时报 Could not build module xxx 的解决方法尝试
- iOS 常见编译报错及解决方案汇总(持续更新)
- AVMakeRectWithAspectRatioInsideRect 的使用
- graphhopper-ios 编译过程详解
- 算法
- iOS实现LRU缓存
- 架构
- IOS项目架构
- 其他杂项
- 推荐一个好用的Mac精品软件下载站
- 如何能成为一位合格的职业经理人
- 零基础怎么学习视频剪辑?这篇初剪辑学者指南你一定不要错过
- 免费SSL证书的制作
- 《一部手机拍全景》汇总课
- Linux下JAVA常用命令大全
- 即时通讯
- 通讯协议与即时通讯杂谈
- 简述移动端IM开发的那些坑:架构设计、通信协议和客户端
- 基于实践:一套百万消息量小规模IM系统技术要点总结
- PaddleOCR 文字识别深度学习
- PaddleOCR mac 安装指南
- PaddleOCR 标注工具PPOCRLabel的使用
- PaddleOCR 更换模型
- PaddleOCR 自制模型训练