2021.2 @Hanniyahtml
最近因準備面試,有較多學習內容。計劃產出的是有較多我我的理解和知識結構的幾篇學習內容:RunLoop、Runtime、AutoreleasePool,本篇是 RunLoop 相關,歡迎各位做爲查缺補漏來閱讀~ios
是什麼web
作什麼面試
performTask()
執行任務:Block、Source0、Source一、Main queue、Timercallout_to_observer()
通知外部:Activity、Source0、Timersleep()
睡眠應用:Timer、線程保活、卡頓檢測數組
程序一啓動,在 UIApplicationMain 就會開一個主線程,跑一個和主線程對應的 RunLoop,這個 RunLoop 保證主線程不會被銷燬,也就保證了程序的持續運行。安全
當沒任務時,RunLoop會告訴CPU要去休息,這時CPU就會將其資源釋放出來去作其餘的事情,當有事情作的時候RunLoop就會去作事markdown
線程和 RunLoop 之間一一對應,其對應關係保存在一個全局的 Dictionary 裏,線程是 key,RunLoop 是 value。app
子線程的 RunLoop 的建立發生在第一次獲取時(若建立子線程後不主動獲取,則不會建立,能夠理解爲懶加載),RunLoop 的銷燬發生在線程結束時。框架
只能在一個線程的內部獲取其 RunLoop(主線程除外)。函數
//Foundation
[NSRunLoop currentRunLoop]; // 得到當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 得到主線程的RunLoop對象
//Core Foundation
CFRunLoopGetCurrent(); // 得到當前線程的RunLoop對象
CFRunLoopGetMain(); // 得到主線程的RunLoop對象
複製代碼
NSRunLoop 是對 CFRunLoopRef 的一層封裝
CFRunLoopRef 的 API 是線程安全的;NSRunLoop 提供了面向對象的 API,但這些 API 不是線程安全的。
開一個子線程時建立 RunLoop,不是經過 alloc init 方法建立,而是直接經過調用 currentRunLoop 方法來建立,由於它自己是一個懶加載。
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //每次進入Runloop(如切換mode後)
kCFRunLoopBeforeTimers = (1UL << 1), //即將DoTimers
kCFRunLoopBeforeSources = (1UL << 2),//即將DoSources
kCFRunLoopBeforeWaiting = (1UL << 5),//當前線程即將進入睡眠(若當前隊列無多餘消息則進入睡眠)
kCFRunLoopAfterWaiting = (1UL << 6), //當前線程從睡眠中恢復(讀出隊列消息,繼續執行)
kCFRunLoopExit = (1UL << 7), //退出Runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼
表示將要處理Timer
表示將要處理Source0
Runloop 的每次 loop 不老是按順序執行上面的各類
performTask
和callout_to_observer
,而是糅合在一塊兒各類跳轉
借用mrpeak的圖來理解完整的流程:
睡眠喚醒 RunLoop 後 DoSource1/DoMainQueue/DoTimers 只會三選一
RunLoop 結構體
struct __CFRunLoop {
...//省略非核心成員
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode; //指向_CFRunLoopMode結構體的指針
CFMutableSetRef _modes; //多個mode數組
};
複製代碼
Mode結構體
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
... //省略非核心成員
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
複製代碼
mainQueue 任務的執行和 mode 無關,mode 內沒有相關信息
Mode 分爲 Common Mode 和 Private Mode,因此 observer 並不會監控到全部 Runloop 的動態
RunLoop啓動時選擇其中一個Mode做爲currentMode;
須要切換Mode時,只能退出RunLoop,再從新指定一個Mode進入,這樣作主要是爲了分隔開不一樣組的Source、Timer、Observer,讓其互不影響
若當前mode內沒有任何Source/Timer/Observer,RunLoop不會空轉,會馬上退出。
即將進入 RunLoop 時,經過 observer 觀察到 kCFRunLoopEntry 狀態,主線程 RunLoop 會建立一個 AutoreleasePool。
不會響應。
緣由:NSTimer 默認只會調度到 kCFRunLoopDefaultMode,當 scrollView 滑動的時候,runloop 會進入 UITrackingRunLoopMode,那麼在 doTimer 的時候天然就不會觸發 NSTimer 的任務了
解決辦法:
但即便這樣,當 RunLoop 使用系統 private mode 時,也會存在不執行 Timer 的問題。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製代碼
CGD 定時器更精準。由於
RunLoop爲了節省資源,並不會在很是準確的時間點回調這個Timer。Timer 有個屬性叫作 Tolerance (寬容度),標示了當時間點到後,允許有多少最大偏差。
若是某個時間點被錯過了,例如執行了一個很長的任務且也過了Timer的寬容度,則那個時間點的回調也會跳過去,不會延後執行。
//建立隊列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//1.建立一個GCD定時器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 須要對timer進行強引用,保證其不會被釋放掉,纔會按時調用block塊
// 局部變量,讓指針強引用
self.timer = timer;
//2.設置定時器的開始時間,間隔時間,精準度
//精準度 通常爲0 在容許範圍內增長偏差可提升程序的性能
//GCD的單位是納秒 因此要 * NSEC_PER_SEC
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//3.設置定時器要執行的事情
dispatch_source_set_event_handler(timer, ^{
NSLog(@"---%@--", [NSThread currentThread]);
});
dispatch_resume(timer); // 啓動
複製代碼
手指觸摸屏幕
系統註冊了一個 Observer 監測 BeforeWaiting (RunLoop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取全部剛被標記爲待處理的 GestureRecognizer,並執行 GestureRecognizer 的回調。 當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。
當在操做 UI 時,好比改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法後,這個 UIView/CALayer 就被標記爲待處理,並被提交到一個全局的容器去。 系統註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行函數,會遍歷全部待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面。
能夠理解爲一個和屏幕刷新率一致的定時器。若是在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去,形成界面卡頓的感受。在快速滑動TableView時,即便一幀的卡頓也會讓用戶有所察覺。
AFURLConnectionOperation 這個類是基於 NSURLConnection 構建的,其但願能在後臺線程接收 Delegate 回調。AFNetworking 單首創建了一個線程,並在這個線程中啓動了一個 RunLoop。
Facebook 推出的用於保持界面流暢性的框架,將繪製和排版放在後臺線程進行。使用 Node 來封裝 View 和 Layer,並實現了相似的一套界面更新的機制:在主線程的 RunLoop 中添加一個 Observer,監聽了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回調時,遍歷全部以前放入隊列的待處理的任務,而後一一執行。
參考:
解密 Runloop
iOS學習——淺談RunLoop - 雲+社區 - 騰訊雲
深刻理解RunLoop | Garan no dou
iOS底層原理總結 - RunLoop - 掘金
源碼:CFRunLoop.c