iOS老司機的RunLoop原理探究及實用Tips
本文正在參加「金石計劃 . 瓜分6萬現金大獎」
前言
- iOS中的
RunLoop
除了面試中跟面試官的探討, 在實際開發中就沒用了嗎? 初入iOS開發大門時, 可能很多人都會有這個疑惑. - 誠然, 日常的iOS開發中,
RunLoop
的直接使用頻率確實相對不高, 但是一旦深入理解RunLoop
的原理和機制, 我們就會發現, iOS開發中的方方面面都包含著RunLoop
的影子. RunLoop
的資料結構設計和機制也體現著iOS作業系統兼顧效能和耗電的使用者態
和核心態
切換的精妙.- 下面就
RunLoop
的底層資料結構原理及應用, 跟各位同仁聊一聊自己的淺見, 拋磚引玉. - 文章純手打, 拋磚引玉, 如有錯誤還請評論區指正, 先行謝過了:)
1. RunLoop的概念和資料結構
1.1 RunLoop的概念
- 有事做的時候做事,沒事做的時候休息
- 通過內部維護的事件迴圈來對事件/訊息進行管理的一個 物件
- 沒有訊息需要處理時, 休眠以避免資源佔用
-
- 使用者態到核心態切換
- 有訊息需要處理時, 立刻被喚醒
-
- 核心態到使用者態切換
- 核心態到使用者態切換
1.2 RunLoop的資料結構
- NSRunLoop是CFRunLoop的封裝, 提供了面向物件的API
1.3 RunLoop模式有哪些?
- 常用的3個Mode:
NSDefaultRunLoopMode
, 預設的模式, 有事件響應的時候, 會阻塞舊事件NSRunLoopCommonModes
, 普通模式, 不會影響任何事件UITrackingRunLoopMode
, 只能是有事件的時候才會響應的模式- App剛啟動的時候會執行一次的模式
- 系統檢測App各種事件的模式
- 蘋果官方文件對5個Mode的介紹: ```
System Run Loop Modes
A pseudo-mode that includes one or more other run loop modes.
The mode set to handle input sources other than connection objects.
The mode set when tracking events modally, such as a mouse-dragging loop.
The mode set when waiting for input from a modal panel, such as a save or open panel.
The mode set while tracking in controls takes place. ```
1.4 關於RunLoop的5個類
CFRunLoopRef
: 代表RunLoop的物件CFRunLoopModeRef
: 代表RunLoop的執行模式CFRunLoopSourceRef
: 就是RunLoop模型圖中提到的輸入源(事件源)CFRunLoopTimerRef
: 就是RunLoop模型圖中提到的定時源CFRunLoopObserverRef
: 觀察者, 能夠監聽RunLoop的狀態改變.- 一個RunLoop物件中包含若干個執行模式.每一個執行模式下又包含若干個輸入源、定時源、觀察者.
-
- 每次RunLoop啟動時, 只能指定其中一個執行模式, 這個執行模式被稱作當前執行模式
CurrentMode
.
- 每次RunLoop啟動時, 只能指定其中一個執行模式, 這個執行模式被稱作當前執行模式
-
- 如果需要切換執行模式, 只能退出當前Loop, 再重新指定一個執行模式進入.
-
- 這樣做主要是為了分隔開不同組的輸入源、定時源、觀察者, 讓其互不影響.
- 這樣做主要是為了分隔開不同組的輸入源、定時源、觀察者, 讓其互不影響.
1.5 CFRunLoopSourceRef
CFRunLoopSourceRef
是事件源, 有兩種分類方法.- 按照官方文件來分類
-
- Port-Based Sources (基於埠)
-
- Custom Input Sources (自定義)
-
- Cocoa Perform Selector Sources
- 按照函式呼叫棧來分類
-
- Source0: 非基於Port
-
- Source1: 基於Port, 通過核心和其他執行緒通訊, 接收、分發系統事件
1.6 RunLoop的基本執行原理
- 原本系統就有一個RunLoop在檢測App內的事件, 當輸入源有執行操作的時候, 系統的RunLoop會監聽輸入源的狀態, 進而在系統內部做一些對應的操作. 處理完事件後, 會自動回到睡眠狀態, 等待下一次被喚醒.
- 在每次執行開啟RunLoop的時候, 所線上程的RunLoop會自動處理之前未處理的事件, 並且通知相關的觀察者.
1. 通知觀察者RunLoop已經啟動
2. 通知觀察者即將要開始定時器
3. 通知觀察者任何即將啟動的非基於埠的源Source0
4. 啟動任何準備好的非基於埠的源Source0
5. 如果基於埠的源Source1準備好並處於等待狀態, 立即啟動, 並進入步驟9
6. 通知觀察者執行緒進入休眠狀態
7. 將執行緒置於休眠直到下面任一種事件發生:
- - 某一事件到達基於埠的源Source1
- - 定時器啟動
- - RunLoop設定的時間已經超時
- - RunLoop被顯示喚醒
8. 通知觀察者執行緒將被喚醒
9. 處理未處理的事件
- - 如果使用者定義的定時器啟動, 處理定時器事件並重啟RunLoop, 進入步驟2
- - 如果輸入源啟動, 傳遞相應的訊息
- - 如果RunLoop被顯示喚醒而且時間還沒超時, 重啟RunLoop. 進入步驟2
10. 通知觀察者RunLoop結束.
2. RunLoop在iOS中的落地使用細節
2.1 RunLoop和執行緒的關係
- 在預設情況下, 執行緒執行完之後就會退出, 就不能再繼續任務了. 這時我們需要採用一種方式來讓執行緒能夠不斷地處理任務, 並不退出. 所以, 我們就有了RunLoop.
- 一條執行緒對應一個RunLoop物件, 每條執行緒都有唯一一個與之對應的RunLoop物件.
- RunLoop並不保證執行緒安全. 我們只能在當前執行緒內部操作當前執行緒的RunLoop物件, 而不能在當前執行緒內部去操作其他執行緒的RunLoop物件方法.
- RunLoop物件在第一次獲取RunLoop時建立, 銷燬則是線上程結束的時候.
- 主執行緒的RunLoop物件系統自動幫助我們建立好了(
UIApplicationMain
函式), 子執行緒的RunLoop物件需要我們主動建立和維護. ```
import
import "AppDelegate.h"
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ```
2.1.1 RunLoop與常駐執行緒
- 常駐執行緒
-
- 指的就是那些不會停止,一直存在於記憶體中的執行緒。
- 後臺常駐執行緒測試程式碼: ```
-
(void)viewDidLoad { // 建立執行緒,並呼叫run1方法執行任務 self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil]; // 開啟執行緒 [self.thread start]; }
-
(void)run1 { // 這裡寫任務 NSLog(@"----run1-----"); // 新增下邊兩句程式碼,就可以開啟RunLoop,之後self.thread就變成了常駐執行緒,可隨時新增任務,並交於RunLoop處理 [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; // 測試是否開啟了RunLoop,如果開啟RunLoop,則來不了這裡,因為RunLoop開啟了迴圈。 NSLog(@"未開啟RunLoop"); }
-
(void)touchesBegan:(NSSet
)touches withEvent:(UIEvent )event { // 利用performSelector,在self.thread的執行緒中呼叫run2方法執行任務 [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO]; } - (void)run2 { NSLog(@"----run2-----"); } ```
2.1.2 AFN2.0 和3.0的主要區別--去除常駐執行緒
- AFN3.0去除了所有
NSURLConnection
請求的API - AFN3.0使用
NSURLSession
代替AFN2.0的常駐執行緒
2.1.2.1 AFN2.X常駐線分析
- 常駐執行緒
-
- 指的就是那些不會停止,一直存在於記憶體中的執行緒。
-
- AFNetworking 2.0 專門建立了一個執行緒來接收 NSOperationQueue 的回撥,這個執行緒其實就是一個常駐執行緒。 ```
- (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
// 先用 NSThread 建立了一個執行緒
[[NSThread currentThread] setName:@"AFNetworking"];
// 使用 run 方法新增 runloop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
```
-
雖然說,在一個 App 裡網路請求這個動作的佔比很高,但也有很多不需要網路的場景,所以執行緒一直常駐在記憶體中,也是不合理的。
-
在請求完成後我們需要對資料進行一些處理, 如果我們在主執行緒中處理就會導致UI卡頓
- 這時我們就需要一個子執行緒來處理事件和網路請求的回撥. 但是子執行緒在處理完事件後就會自動結束生命週期,
-
- 這時後面的一些網路請求的回撥我們就無法接收了,
-
- 所以我們就需要開啟子執行緒的RunLoop使執行緒常駐來保活執行緒.
2.1.2.2 AFN3.X不在常駐執行緒的分析
- AFNetworking 在 3.0 版本時,使用蘋果公司新推出的 NSURLSession 替換了 NSURLConnection,從而避免了常駐執行緒這個坑。
-
- NSURLSession 可以指定回撥 NSOperationQueue,這樣請求就不需要讓執行緒一直常駐在記憶體裡去等待回調了。
self.operationQueue = [[NSOperationQueue alloc] init]; self.operationQueue.maxConcurrentOperationCount = 1; self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
- NSURLSession 可以指定回撥 NSOperationQueue,這樣請求就不需要讓執行緒一直常駐在記憶體裡去等待回調了。
-
- NSURLSession 發起的請求,可以指定回撥的 delegateQueue,不再需要在當前執行緒進行代理方法的回撥。所以說,NSURLSession 解決了 NSURLConnection 的執行緒回撥問題。
-
AFNetworking 2.0 使用常駐執行緒也是無奈之舉,一旦有方案能夠替代常駐執行緒,它就會毫不猶豫地廢棄常駐執行緒。
-
在AFN3.X中使用的是NSURLSession進行封裝,
-
- 對比NSURLConnection, NSURLSession不需要再當前的執行緒等待網路回撥,
-
- 而是可以讓開發者自己設定需要回調的佇列.
- 在AFN3.X中使用了NSOperationQueue管理網路,
-
- 並設定
self.operationQueue.maxConcurrentOperationCount = 1;
,保證了最大的併發數為1,
- 並設定
-
- 也就是說讓網路請求序列執行. 避免了多執行緒環境下的資源搶奪問題.
-
- AFNetworking 2.0 專門建立了一個執行緒來接收 NSOperationQueue 的回撥,這個執行緒其實就是一個常駐執行緒。 ```
- (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { // 先用 NSThread 建立了一個執行緒 [[NSThread currentThread] setName:@"AFNetworking"]; // 使用 run 方法新增 runloop NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } } ```
- 雖然說,在一個 App 裡網路請求這個動作的佔比很高,但也有很多不需要網路的場景,所以執行緒一直常駐在記憶體中,也是不合理的。
- AFNetworking 在 3.0 版本時,使用蘋果公司新推出的 NSURLSession 替換了 NSURLConnection,從而避免了常駐執行緒這個坑。
-
- NSURLSession 可以指定回撥 NSOperationQueue,這樣請求就不需要讓執行緒一直常駐在記憶體裡去等待回調了。
self.operationQueue = [[NSOperationQueue alloc] init]; self.operationQueue.maxConcurrentOperationCount = 1; self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
- NSURLSession 可以指定回撥 NSOperationQueue,這樣請求就不需要讓執行緒一直常駐在記憶體裡去等待回調了。
-
- NSURLSession 發起的請求,可以指定回撥的 delegateQueue,不再需要在當前執行緒進行代理方法的回撥。所以說,NSURLSession 解決了 NSURLConnection 的執行緒回撥問題。
- AFNetworking 2.0 使用常駐執行緒也是無奈之舉,一旦有方案能夠替代常駐執行緒,它就會毫不猶豫地廢棄常駐執行緒。
2.2 NSTimer與RunLoop
2.2.1 NSTimer中的scheduledTimerWithTimeInterval
方法和RunLoop的關係.
``` NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
/ 上面這句程式碼呼叫了scheduledTimer返回的定時器, NSTimer會自動加入到RunLoop的NSDefaultRunLoop模式下, 相當於下面兩句程式碼. / NSTimer timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
/* 因為預設已經添加了NSDefaultRunLoopMode, 所以只給timer1添加了UITrackingRunLoopMode後, 效果跟添加了NSRunLoopCommonModes一致, 拖動也不影響定時器 / [[NSRunLoop currentRunLoop] addTimer:timer1 forMode:UITrackingRunLoopMode];
// 開發中推薦使用 NSTimer *timer = [NSTimer timerWithTimeInterval:duration target:self selector:@selector(cs_toastTimerDidFinish:) userInfo:toast repeats:NO]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; ```
2.2.2 為什麼說NSTimer不準確
- NSTimer的觸發時間到的時候, runloop如果在阻塞狀態, 觸發時間就會推遲到下一個runloop週期
- 可利用GCD優化 ``` NSTimeInterval interval = 1.0; _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);
dispatch_source_ser_evernt_handler(_timer, ^{ NSLog(@"GCD timer test"); });
dispatch_resume(_timer); ```
2.3 RunLoop使用的其他小Tips
NSTimer
不被手勢操作影響- 滑動
tableview
時cell
中的ImageView
推遲顯示[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:@[NSDefaultRunLoopMode]];
3. 如何用RunLoop原理去監控卡頓
- 戴銘老師的RunLoop示意圖
- 卡頓跟FPS關係不大, 24幀的動畫也是流暢的
- 通過監控RunLoop的狀態, 就能夠發現呼叫方法是否執行時間過長, 從而判斷出是否會出現卡頓.
1. 要想監聽 RunLoop,你就首先需要建立一個 CFRunLoopObserverContext 觀察者,程式碼如下:
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
2. 將建立好的觀察者 runLoopObserver 新增到主執行緒 RunLoop 的 common 模式下觀察。然後,建立一個持續的子執行緒專門用來監控主執行緒的 RunLoop 狀態。
3. 一旦發現進入睡眠前的 kCFRunLoopBeforeSources 狀態,或者喚醒後的狀態 kCFRunLoopAfterWaiting,在設定的時間閾值內一直沒有變化,即可判定為卡頓。
4. 接下來,我們就可以通過三方庫PLCrashReporter
dump 出堆疊的資訊,從而進一步分析出具體是哪個方法的執行時間過長。
發文不易, 喜歡點讚的人更有好運氣👍 :), 定期更新+關注不迷路~
ps:歡迎加入筆者18年建立的研究iOS稽核及前沿技術的三千人扣群:662339934,坑位有限,備註“掘金網友”可被群管通過~
本文正在參加「金石計劃 . 瓜分6萬現金大獎」
- iOS老司機聊聊實際專案開發中的<<人月神話>>
- iOS老司機可落地在中大型iOS專案中的5大接地氣設計模式合集
- iOS老司機的跨端跨平臺Hybrid開發Tips
- iOS老司機的2022年回顧, 聊聊寒冬下的實用<<談判力>>
- iOS老司機可落地的中大型iOS專案中的設計模式優化Tips_橋接模式
- iOS老司機的多執行緒PThread學習分享
- iOS老司機整理, iOSer必會的經典演算法_2
- iOS老司機的<<藍海轉型>>讀書分享
- iOS老司機的<<程式設計師的自我修養:連結、裝載與庫>>讀書分享
- iOS老司機的接地氣演算法Tips
- iOS老司機的RunLoop原理探究及實用Tips
- iOS老司機整理, iOSer必會的經典演算法_1
- iOS老司機的App啟動優化Tips, 讓啟動速度提升10%
- iOS老司機的網路相關Tips
- 戀上資料結構與演算法
- iOS老司機帶你一起把App的崩潰率降到0.1%以下
- 探究Swift的String底層實現
- iOS老司機萬字整理, 可能是最全的Swift Tips
- iOS老司機可落地的中大型iOS專案中的設計模式優化Tips
- 聊一聊Swift中的閉包