iOS RunLoop 探究
RunLoop常見用法
AFN
AFN2.x中把網絡請求全部都放在一個子線程中進行。由于子線程運行完任務后就會自動銷毀,所以在子線程中運行了一個Runloop保證線程不會被銷毀掉。(線程的創建和銷毀耗費的資源雖然很少,但是大量網絡請求導致大量創建和銷毀所耗費的資源還是十分可觀的)
#pragma mark AFN
+ (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;
}
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
用CFRunloop也可建立一個Runloop
CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
//A perform callback for the run loop source. This callback is called when the source has fired.
//Availability
context.perform = fire;
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
NSLog(@"自定義RunLoopRun");
CFRunLoopRun();
線程與RunLoop的一些概念
線程安全
- CFRunLoopRef 是線程安全的
- NSRunLoop 非線程安全
線程運行
- 子線程執行完任務自動銷毀
- runloop實際是一個循環,并不會自動停止
- 添加runloop后,run,如果要銷毀這個線程,必須要停止runloop。
- 如果當前線程沒有Runloop就
關于AFN 3.x
- 由于NSUrlSession參考了AF的2.x的優點,自己維護了一個線程池,做Request線程的調度與管理,所以在AF3.x中,沒有了常駐線程,都是用的時候run,結束的時候stop。
線程間的通信
- iOS線程間的通信,實際上是各種輸入源,觸發Runloop去處理對應的事件。
在什么情況下使用RunLoop
僅當在為你的程序創建輔助線程的時候,你才需要顯式運行一個run loop。
Run loop在你要和線程有更多的交互時才需要,比如以下情況:
>
- 使用端口或自定義輸入源來和其他線程通信
- 使用線程的定時器
- Cocoa中使用任何performSelector…的方法
- 使線程周期性工作
RunLoop 詳細介紹
Run Loop的處理兩大類事件源:Timer Source和Input Source(包括performSelector* 方法簇、Port或者自定義Input Source),每個事件源都會綁定在Run Loop的某個特定模式mode上,而且只有RunLoop在這個模式運行的時候才會觸發該Timer和Input Source。
- Runloop 處理兩大類事件源 1.Timer Source 2.Input Source
- 如果沒有任何事件源添加到Run Loop上,Run Loop就會立刻exit
Input Source:傳遞異步事件,通常消息來源于其他線程或程序。
- 基于端口的輸入源
- Cocoa和Cocoa Foundation 內置支持使用端口相關的對象和函數來創建的機遇端口的源。
- Cocoa中只要簡單的創建端口對象,將端口添加到Runloop即可。端口對象會自己處理創建和配置輸入源。
- 在Core Fundation中,你必須人工創建端口和他的Runloop源。我們可以使用端口相關的函數(CFMachPortRef,CFMessagePortRef,CFSocketRef)來創建合適的對象。
Example:
void createPortSource()
{
CFMessagePortRef port = CFMessagePortCreateLocal(kCFAllocatorDefault, CFSTR("com.someport"),myCallbackFunc, NULL, NULL);
CFRunLoopSourceRef source = CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
while (pageStillLoading) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
CFRunLoopRun();
[pool release];
}
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}
- 自定義輸入源
- 自定義輸入源需要人工從其他線程發送。
- 使用Core Fundation中的CFRunLoopSourceRef類型相關的函數來創建。
- 需要定義消息傳遞機制
Example:void createCustomSource() { CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}; CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context); CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); while (pageStillLoading) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; CFRunLoopRun(); [pool release]; } CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); CFRelease(source); }
-
Cocoa的Selector源
- 除了基于端口的源,Cocoa定義了自定義輸入源,允許你在任何線程執行selector方法
- 當在其他線程上面執行selector時,目標線程須有一個活動的run loop。對于你創建的線程,這意味著線程在你顯式的啟動run loop之前是不會執行selector方法的,而是一直處于休眠狀態。( 導致Crash )
- 和基于端口的源一樣。執行selector請求會在目標線程上序列化,減緩多線程上允許多個方法容易引起的同步問題。
-
定時源
- 定時源在預設的時間點同步方式傳遞消息,這些消息都會發生在特定時間或者重復的時間間隔。定時源直接傳遞消息給處理例程,不會立即退出run loop.
- 定時器與runloop中的特定模式有關。
Example://方法一: NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0 target:self selector:@selector(backgroundThreadFire:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode]; //方法二: [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(backgroundThreadFire:) userInfo:nil repeats:YES];
事件源按照函數調用棧的分類
- 基于端口的輸入源
- 在runloop中,被定義名為souce1。Cocoa和Core Foundation內置支持使用端口相關的對象和函數來創建的基于端口的源。例如,在Cocoa里面你從來不需要直接創建輸入源。你只要簡單的創建端口對象,并使用NSPort的方法把該端口添加到run loop。端口對象會自己處理創建和配置輸入源。
- 非基于port,自定義輸入源
RunLoop 觀察者
源是在合適的同步或異步事件發生時觸發,而run loop觀察者則是在run loop本身運行的特定時候觸發。你可以使用run loop觀察者來為處理某一特定事件或是進入休眠的線程做準備。你可以將run loop觀察者和以下事件關聯:
- Runloop入口
- Runloop何時處理一個定時器
- Runloop何時處理一個輸入源
- Runloop何時進入睡眠狀態
- Runloop何時被喚醒,但在喚醒之前要處理的事件
- Runloop終止
RunLoop事件隊列
每次運行run loop,你線程的run loop對會自動處理之前未處理的消息,并通知相關的觀察者。具體的順序如下:
- 通知觀察者run loop已經啟動
- 通知觀察者任何即將要開始的定時器
- 通知觀察者任何即將啟動的非基于端口的源
- 啟動任何準備好的非基于端口的源
- 如果基于端口的源準備好并處于等待狀態,立即啟動;并進入步驟9。
- 通知觀察者線程進入休眠
- 將線程置于休眠直到任一下面的事件發生:
- 某一事件到達基于端口的源
- 定時器啟動
- Run loop設置的時間已經超時
- run loop被顯式喚醒
- 通知觀察者線程將被喚醒。
- 處理未處理的事件
- 如果用戶定義的定時器啟動,處理定時器事件并重啟run loop。進入步驟2
- 如果輸入源啟動,傳遞相應的消息
- 如果run loop被顯式喚醒而且時間還沒超時,重啟run loop。進入步驟2
- 通知觀察者run loop結束。
因為定時器和輸入源的觀察者是在相應的事件發生之前傳遞消息,所以 通知的時間和實際事件發生的時間之間可能存在誤差 。如果需要精確時間控制,你可以使用休眠和喚醒通知來幫助你校對實際發生事件的時間。
因為當你運行run loop時定時器和其它周期性事件經常需要被傳遞,撤銷run loop也會終止消息傳遞。典型的例子就是鼠標路徑追蹤。因為你的代碼直接獲取到消息而不是經由程序傳遞,因此活躍的定時器不會開始直到鼠標追蹤結束并將控制權交給程序。
Run loop可以由run loop對象顯式喚醒。其它消息也可以喚醒run loop。例如,添加新的非基于端口的源會喚醒run loop從而可以立即處理輸入源而不需要等待其他事件發生后再處理。
從這個事件隊列中可以看出:
①如果是事件到達,消息會被傳遞給相應的處理程序來處理, runloop處理完當次事件后,run loop會退出,而不管之前預定的時間到了沒有。你可以重新啟動run loop來等待下一事件。
②如果線程中有需要處理的源,但是響應的事件沒有到來的時候,線程就會休眠等待相應事件的發生。這就是為什么run loop可以做到讓線程有工作的時候忙于工作,而沒工作的時候處于休眠狀態。
什么時候使用run loop
僅當在為你的程序創建輔助線程的時候,你才需要顯式運行一個run loop。Run loop是程序主線程基礎設施的關鍵部分。所以,Cocoa和Carbon程序提供了代碼運行主程序的循環并自動啟動run loop。IOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作為程序啟動步驟的一部分,它在程序正常啟動的時候就會啟動程序的主循環。類似的,RunApplicationEventLoop函數為Carbon程序啟動主循環。如果你使用xcode提供的模板創建你的程序,那你永遠不需要自己去顯式的調用這些例程。
對于輔助線程,你需要判斷一個run loop是否是必須的。如果是必須的,那么你要自己配置并啟動它。你不需要在任何情況下都去啟動一個線程的run loop。比如,你使用線程來處理一個預先定義的長時間運行的任務時,你應該避免啟動run loop。
如果你決定在程序中使用run loop,那么它的配置和啟動都很簡單。和所有線程編程一樣,你需要計劃好在輔助線程退出線程的情形。讓線程自然退出往往比強制關閉它更好。
CFRunLoop 對外接口
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
CFRunLoopSourceRef
- Source0 只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用CFRunLoopSourceSignal(source),將這個Source標記為待處理時間,然后手動調用CFRunLoopWeakUp(runloop)來喚醒RunLoop,讓其處理這個事件。
- Source1 包含了一個mach_port和一個回調(函數指針),被用于通過內核和其他線程相互發送消息。這種Source能主動喚醒RunLoop的線程。
CFRunLoopTimerRef 是基于時間的觸發器
CFRunLoopObserverRef 是觀察者
Runloop 使用
Run Loop運行接口
-
要操作Run Loop,Foundation層和Core Foundation層都有對應的接口可以操作Run Loop:
Foundation層對應的是NSRunLoop,Core Foundation層對應的是CFRunLoopRef;
兩組接口差不多,不過功能上還是有許多區別的:
-
例如CF層可以添加自定義Input Source事件源、(CFRunLoopSourceRef)Run Loop觀察者Observer(CFRunLoopObserverRef),很多類似功能的接口特性也是不一樣的。
NSRunLoop的運行接口:
//運行 NSRunLoop,運行模式為默認的NSDefaultRunLoopMode模式,沒有超時限制
- (void)run;
//運行 NSRunLoop: 參數為運時間期限,運行模式為默認的NSDefaultRunLoopMode模式
- (void)runUntilDate:(NSDate *)limitDate;
//運行 NSRunLoop: 參數為運行模式、時間期限,返回值為YES表示是處理事件后返回的,NO表示是超時或者停止運行導致返回的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
模式問題
Run Loop運行時只能以一種固定的模式運行,如果我們需要它切換模式,只有停掉它,再重新開啟它。
運行時它只會監控這個模式下添加的Timer Source和Input Source,如果這個模式下沒有相應的事件源,Run Loop的運行也會立刻返回的。注意Run Loop不能在運行在NSRunLoopCommonModes模式,因為NSRunLoopCommonModes其實是個模式集合,而不是一個具體的模式,我可以在添加事件源的時候使用NSRunLoopCommonModes,只要Run Loop運行在NSRunLoopCommonModes中任何一個模式,這個事件源都可以被觸發。
來自:https://juejin.im/post/58fc3ec8a0bb9f0065bd2889