[toc]
## RunLoop是什么?有什么作用?
`RunLoop`:翻譯過來是運行環路(中式翻譯: 跑圈)。我們在創建命令行項目和創建ios項目時,發現命令行項目當最后一行代碼執行完后項目就自動退出了,而ios項目確可以一直運行,知道用戶手動點擊退出按鈕。這就是因為ios項目在main函數中自動創建了runLoop,從而可以使項目可以一直響應用戶的操作。
```
int main(int argc, char * argv[]) {
@autoreleasepool {
//這行代碼 會自動創建主線程的RunLoop
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
```
我們可以將這個過程我們可以簡化成:

我們從這個過程可以看出RunLoop的基本作用:
- 保持程序的持續運行
- 處理App中的各種事件(比如觸摸事件、定時器事件等)
- 節省CPU資源,提高程序性能:該做事時做事,該休息時休息
- ......
我們平時開發中,涉及到RunLoop的挺多的,比如說定時器、手勢識別、網絡請求等等.

## 一、RunLoop的結構
iOS中有2套API來訪問和使用RunLoop:
>① Foundation:`NSRunLoop`,它是基于 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API `不是線程安全`的。
>② Core Foundation:`CFRunLoopRef`,它提供了純 C 函數的 API,所有這些 API都是`線程安全`的。([CFRunLoopRef](https://opensource.apple.com/tarballs/CF/)是開源的)
兩者關系:

所以我們獲取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的結構是這樣的:**

- `_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的狀態有一下幾種:

**總結 `CFRunLoopMode`的圖示:**

>需要注意的一點是:如果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`, 并把線程作為參數傳遞

繼續,跳轉至__CFRunLoopGet0,如下:

>發現,RunLoop與線程的關系是`一對一`的,并且用了個`全局字典`保存了起來,`線程作為key`,`RunLoop作為value`。
我們發現如果線程沒有啟用RunLoop后會執行完馬上銷毀:

>添加RunLoop后,發現還是運行完就銷毀:這是因為如果`Mode里`沒有任何`Source0/Source1/Timer/Observer`,RunLoop會立馬退出

所以我們需要往`Model中`添加一個數據:

發現確實執行完后,線程阻塞了,一直沒有被銷毀,這是因為當runloop創建后,如果沒有被事件喚醒后它就一直在`休眠`,cpu就不會繼續處理事情,所以`阻塞`在這。
## 三、RunLoop的運行邏輯
我們在了解RunLoop的結構以及與線程的關系后,我們再來看一下RunLoop的運行流程:

**接下來,我們通過源碼來看一下RunLoop是如何處理這些事件的?**
關于入口的查找,我們可以現在touchesBegan:方法中打個斷點,查看程序是怎么執行到這的:

```
//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;
}
```
簡化成流程圖 則是:

## 四、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 自制模型訓練