標籤:
李洪強iOS開發之RunLoop的原理和核心機制
搞iOS之後一直沒有深入研究過RunLoop,非常的慚愧。剛好前一陣子負責效能最佳化項目,需要利用RunLoop做效能最佳化和效能檢測,趁著這個機會深入研究了RunLoop的原理和特性。
RunLoop的定義
當有持續的非同步任務需求時,我們會建立一個獨立的生命週期可控的線程。RunLoop就是控制線程生命週期並接收事件進行處理的機制。
RunLoop是iOS事件響應與任務處理最核心的機制,它貫穿iOS整個系統。
Foundation: NSRunLoop
Core Foundation: CFRunLoop 核心部分,代碼開源,C 語言編寫,跨平台
目的
通過RunLoop機制實現省電,流暢,響應速度快,使用者體驗好
理解
進程是一家工廠,線程是一個流水線,Run Loop就是流水線上的主管;當工廠接到商家的訂單分配給這個流水線時,Run Loop就啟動這個流水線,讓流水線動起來,生產產品;當產品生產完畢時,Run Loop就會暫時停下流水線,節約資源。
RunLoop管理流水線,流水線才不會因為無所事事被工廠銷毀;而不需要流水線時,就會辭退RunLoop這個主管,即退出線程,把所有資源釋放。
RunLoop並不是iOS平台的專屬概念,在任何平台的多線程編程中,為控制線程的生命週期,接收處理非同步訊息都需要類似RunLoop的迴圈機制實現,Android的Looper就是類似的機制。
特性
- 主線程的RunLoop在應用啟動的時候就會自動建立
- 其他線程則需要在該線程下自己啟動
- 不能自己建立RunLoop
- RunLoop並不是安全執行緒的,所以需要避免在其他線程上調用當前線程的RunLoop
- RunLoop負責管理autorelease pools
- RunLoop負責處理訊息事件,即輸入源事件和計時器事件
RunLoop機制
主線程 (有 RunLoop 的線程) 幾乎所有函數都從以下六個之一的函數調起:
CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
CFRunloop is calling out to an abserver callback function
用於向外部報告 RunLoop 目前狀態的更改,架構中很多機制都由 RunLoopObserver 觸發,如 CAAnimation
CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
CFRunloop is calling out to a block
訊息通知、非延遲的perform、dispatch調用、block回調、KVO
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
CFRunloop is servicing the main desipatch queue
CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
CFRunloop is calling out to a timer callback function
延遲的perform, 延遲dispatch調用
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
CFRunloop is calling out to a source 0 perform function
處理App內部事件、App自己負責管理(觸發),如UIEvent、CFSocket。普通函數調用,系統調用
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
CFRunloop is calling out to a source 1 perform function
由RunLoop和核心管理,Mach port驅動,如CFMachPort、CFMessagePort
RunLoop 架構
??
主要有以下六種狀態:
- kCFRunLoopEntry -- 進入runloop迴圈
- kCFRunLoopBeforeTimers -- 處理定時調用前回調
- kCFRunLoopBeforeSources -- 處理input sources的事件
- kCFRunLoopBeforeWaiting -- runloop睡眠前調用
- kCFRunLoopAfterWaiting -- runloop喚醒後調用
- kCFRunLoopExit -- 退出runloop
RunLoop 運行時調用棧
- RunLoopObserver與Autorelease Pool的關係
UIKit 通過 RunLoopObserver 在 RunLoop 兩次 Sleep 間對 Autorelease Pool 進行 Pop 和 Push 將這次 Loop 中產生的 Autorelease 對象釋放。
指定用於喚醒的 mach_port 連接埠
調用 mach_msg 監聽喚醒連接埠,被喚醒前系統核心將這個線程掛起,停留在mach_msg_trap狀態。
由另一個線程向核心發送這個連接埠的msg後,trap狀態被喚醒,RunLoop繼續工作。
RunLoop支援的訊息事件(Events)
//錯誤做法 NSRunLoop *runLoop = [NSRunLoop currentRunLoop];while (!self.isCancelled && !self.isFinished) { [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];};//正確做法NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];while (!self.isCancelled && !self.isFinished) { @autoreleasepool { [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]]; }}
Run Loop Modes
理解
Run Loop Mode就是流水線上支援生產的產品類型,流水線在一個時刻只能在一種模式下運行,生產某一類型的產品。訊息事件就是訂單。
Cocoa定義了四中Mode
Default:NSDefaultRunLoopMode,預設模式,在Run Loop沒有指定Mode的時候,預設就跑在Default Mode下
Connection:NSConnectionReplyMode,用來監聽處理網路請求NSConnection的事件
Modal:NSModalPanelRunLoopMode,OS X的Modal面板事件
Event tracking:UITrackingRunLoopMode,拖動事件
Common mode:NSRunLoopCommonModes,是一個模式集合,當綁定一個事件來源到這個模式集合的時候就相當於綁定到了集合內的每一個模式
RunLoop可以通過[acceptInputForMode:beforeDate:]和[runMode:beforeDate:]來指定在一段時間內的運行模式。如果不指定的話,RunLoop預設會運行在Default下(不斷重複調用runMode:NSDefaultRunLoopMode beforDate:)
在主線程啟動一個計時器Timer,然後拖動UITableView或者UIScrollView,計時器不執行。這是因為,為了更好的使用者體驗,在主線程中Event tracking模式的優先順序最高。在使用者拖動控制項時,主線程的Run Loop是運行在Event tracking Mode下,而建立的Timer是預設關聯為Default Mode,因此系統不會立即執行Default Mode下接收的事件。解決方案:
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFireMethod:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; //或 [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];[timer fire];
Run Loop應用實踐
Run Loop主要有以下三個應用情境:
- 維護線程的生命週期,讓線程不自動結束,isFinished為Yes時退出。
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];while (!self.isCancelled && !self.isFinished) { @autoreleasepool { [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]]; }}
- 建立常駐線程,執行一些會一直存在的任務。該線程的生命週期跟App相同
@autoreleasepool { NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run];}
- 在一定時間內監聽某種事件,或執行某種任務的線程
如下代碼,在30分鐘內,每隔30s執行onTimerFired:。這種情境一般會出現在,如我需要在應用啟動之後,在一定時間內持續更新某項資料。
@autoreleasepool { NSRunLoop * runLoop = [NSRunLoop currentRunLoop]; NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30 target:self selector:@selector(onTimerFired:) userInfo:nil repeats:YES]; [runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes]; [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];}
+ (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; // 這裡主要是監聽某個 port,目的是讓這個 Thread 不會回收 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; }}+ (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread;}
李洪強iOS開發之RunLoop的原理和核心機制