1.RunLoop的概念
RunLoop其實就是一個大的do while循環,它的關鍵點在于如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用,在有消息到來時立刻被喚醒。所以RunLoop實際上是一個對象,這個對象管理了其需要處理的時間和消息,并提供了一個函數來執行上面的事件邏輯。因此runLoop可以說就是為了線程而生。
2.RunLoop 的作用:
1.使程序一直運行并接受用戶輸入
2.決定程序在何時應該處理哪些事件
3.調用解耦(事件隊列的分發與派放)
4.節省CPU時間
5.RunLoop也負責autorelease pool的創建與釋放(當一個運行循環結束或者RunLoop退出和休眠的時候,它都會釋放一次autorelease pool)
其中RunLoop運行一次的時間為1/60 S
與Runloop最密切相關的:NSTimer 、UIEvent 、Autorelease
CFRunLoop是基于pthread來管理的。蘋果不允許直接創建RunLoop,它只提供了兩個自動獲取的函數CFRunLoopGetMain() 和 CFRunLoopGetCurrent(),當線程中沒有RunLoop的時候,CFRunLoopGetCurrent()其實會創建一個RunLoop對象并返回。
3.RunLoop的結構
RunLoop接受事件來自兩種不同的來源:輸入源和定時源,輸入源傳遞異步事件,通常消息來自其他線程和程序,定時源則傳遞同步事件,發生在特定時間或者重復的時間間隔。兩種源都使用程序的某一特定處理例程來處理到達的事件。輸入源包括兩種,分別是基于端口的輸入源和自定義輸入源。基于端口的輸入源監聽程序相應的輸入源,自定義輸入源則監聽自定義的事件源。基于端口的輸入源由內核發送,而自定義的輸入源需要人工從其他線程發送。
RunLoop由線程和Mode組成,線程和RunLoop之間是一一對應的關系,其關系是保存在一個字典里面,線程剛創建時并沒有RunLoop,如果你不主動獲取那么它一直不會有。RunLoop的創建是發生在第一次獲取時,RunLoop的銷毀是發生在線程結束時。主線程的RunLoop默認是開啟的,當程序在運行的時候會產生大量的對象,這些對象存儲在RunLoop的釋放池里面,當RunLoop循環完一次之后會釋放自動釋放池同時創建新的自動釋放池。子線程沒有開啟RunLoop需要手動獲取,因為子線程的RunLoop是手動獲取的,所以自動釋放池默認也沒有,當我們在子線程里面創建了大量的臨時對象的時候就需要創建自動釋放池。
一個RunLoop包含若干個RunLoopMode,但是一個RunLoop每次只能加入一種Mode,每一個Mode里面包含若干個source/timer/observer。每次調用RunLoop的主函數時只能指定其中一個Mode,這個Mode被稱為currentMode,如果需要切換Mode只能退出RunLoop然后再重新指定另外的Mode進入。這樣做主要是為了分開不同組的source/timer/observer讓其不受影響。
Run loop模式是所有要監視的輸入源和定時源以及要通知的run loop注冊觀察者的集合。每次運行你的run loop,你都要指定(無論顯示還是隱式)其運行個模式。在run loop運行過程中,只有和模式相關的源才會被監視并允許他們傳遞事件消息。(類似的,只有和模式相關的觀察者會通知run loop的進程)。和其他模式關聯的源只有在run loop運行在其模式下才會運行,否則處于暫停狀態。
4.RunLoop的特點
RunLoop在同一段時間只能且必須在一種特定的Mode下run
更換Mode時,需要停止當前Loop,然后重啟Loop
Mode是iOS App滑動順暢的關鍵
當傳入一個新的mode name 但是RunLoop內部沒有對應的mode時,RunLoop會幫你創建對應的RunLoopMode
5. RunLoopSource
CFRunLoopSourceRef 是事件產生的地方。Souce是RunLoop的數據抽象類。Source有兩個版本:Source0 和 Source1。
· Source0 只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。(Souce0處理App內部事件,App自己負責管理觸發,如UIEvent,CFSocket)
· Source1 包含了一個 mach_port 和一個回調(函數指針),被用于通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程。(Souce1由RunLoop和內核管理,如CFMach,CFMessage)
其實可以簡單的理解為RunLoop通常處理的事件源有兩大種類,分別是time souce和input source,input source是異步消息通常來自其他線程或者程序。time source是timer中的同步事件
6.RunLoopTimer
CFRunLoopTimerRef 是基于時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。(NSTimer受RunLoop的Mode影響,GCD的定時器不受RunLoop的Mode影響)
7.RunLoopObserver
CFRunLoopObserverRef 是觀察者,(它向外部報告RunLoop當前狀態的更改)每個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能通過回調接受到這個變化。可以觀測的時間點有以下幾個:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
上面的 Source/Timer/Observer 被統稱為 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重復加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。
UIKit通過RunLoopObserver在RunLoop兩次Sleep間對AutreleasePool進行push和pop,將這次Loop中產生的Autorelease對象釋放。
8.CFRunLoopMode 和 CFRunLoop 的結構
CFRunLoopMode 和 CFRunLoop 的結構大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
從上面可以看出CFRunLoop里面有一個commonModes,它是一個set集合。系統中共有5個Mode,每一個Mode可以將自己標記為Common屬性(通過將其ModeName屬性添加到RunLoop的commonModes中)。每當RunLoop的內部發生變化時,RunLoop都會將commonModeItems里面的Souce/Observer/Timer同步到具有Common標記的所有Mode里。
系統默認注冊了5個Mode(前兩個跟最后一個常用)
? kCFRunLoopDefaultMode:App的默認Mode,通常主線程是在這個Mode下運行(NSTimer scheduledTimerWithTime這個方法默認加入的就是KCFRunLoopDefaultMode)
? UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
? UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
? GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
? kCFRunLoopCommonModes:這個Mode其實包含了第一個和第二個Mode
GCD中的任務隊列被分配到main queue的block會被分發到main RunLoop中執行。
當RunLoop掛起的時候會指定用于喚醒mach_port的端口。同時會調用mach_msg監聽喚醒端口,被喚醒前,系統會將這個線程掛起,停留在mach_msg_trap狀態
由另一個線程或者另一個進程中的某個線程向內核發送這個端口的msg后,trap狀態被喚醒,runLoop繼續開始干活
CFRunLoop的默認超時時間很長
AutoreleasePool
App啟動后,蘋果在主線程RunLoop里注冊了兩個Observer,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。
第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之后。
在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了。
9.定時器與RunLoop的關系:
NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 注冊到 RunLoop 后,RunLoop 會為其重復的時間點注冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節省資源,并不會在非常準確的時間點回調這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到后,容許有多少最大誤差。
如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延后執行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。
PerformSelecter
當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。
當調用 performSelector:onThread: 時,實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。
10.何時使用RunLoop
我們知道當我們的程序啟動的時候,主線程已經默認創建了一個runLoop,所以只有在二級線程中我們才有機會創建runLoop。RunLoop的主要作用是為了幫助線程常駐進程,所以僅當在為你的程序創建輔助線程的時候,你才需要顯式運行一個run loop。對于輔助線程,你需要判斷一個run loop是否是必須的。如果是必須的,那么你要自己配置并啟動它。你不需要在任何情況下都去啟動一個線程的run loop。比如,你使用線程來處理一個預先定義的長時間運行的任務時,你應該避免啟動run loop。Run loop在你要和線程有更多的交互時才需要,比如以下情況:
使用端口或自定義輸入源來和其他線程通信
在線程中執行定時事件源的任務
Cocoa中使用任何performSelector...的方法
在線程中執行較為頻繁的,周期性的任務
如果你決定在程序中使用run loop,那么它的配置和啟動都很簡單。和所有線程編程一樣,你需要計劃好在輔助線程退出線程的情形。讓線程自然退出往往比強制關閉它更好。
11.線程保活
在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];
}
}
這段代碼用單例創建了一個線程同時將線程加入了RunLoop中,這是AFN中用來線程保活的方法。這里為什么要加入RunLoop是因為我們創建的線程是脫離線程,默認在執行完任務之后就會被系統回收,為了讓線程一直存活下去必須讓它加入RunLoop.至于RunLoop為什么要調用addport forMode方法是因為如果RunLoop里面沒有任何的modelItem的話,RunLoop會直接退出。
我們可以試著來仿照AFN中的線程保活來仿寫一段代碼:
-(void)threadTest
{
for (int i = 0; i < 100000; i ++) {
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(addToRunLoop) object:nil];
[thread start];
}
}
-(void)addToRunLoop
{
NSLog(@"test");
[[NSThread currentThread]setName:@"test"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
當我們運行程序的時候我們會發現,內存在不斷的上漲,同時控制臺會輸出[NSThread start]: Thread creation failed with error 35這個錯誤。我們嘗試把addToRunLoop這個方法里面的代碼封掉,發現程序運行正常并且內存并不會一直上漲,那么可以猜測,是因為線程加入了RunLoop導致了線程不能銷毀因此內存上漲。
那么我們取消RunLoop和線程,那么看看會有什么變化呢:
-(void)threadTest
{
for (int i = 0; i < 1000000; i ++) {
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(addToRunLoop) object:nil];
[thread start];
[self performSelector:@selector(stopRunLoopAndThread) onThread:thread withObject:nil waitUntilDone:YES];
}
}
-(void)addToRunLoop
{
NSLog(@"test");
[[NSThread currentThread]setName:@"test"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
-(void)stopRunLoopAndThread
{
CFRunLoopStop(CFRunLoopGetCurrent());
NSThread *thread = [NSThread currentThread];
[thread cancel];
}
運行程序發現內存還是在增長,并且控制臺也會輸出[NSThread start]: Thread creation failed with error 35這個錯誤,看來我們沒有正確的取消RunLoop。
RunLoop的啟動方式:
(1)run (直接進入,但會使線程進入死循環從而不利于控制RunLoop,結束RunLoop的唯一方式就是kill它)
(2)runUntilDate(RunLoop會在處理完事件或者超時時間后結束)
(3)runMode:beforeDate: (指定RunLoop的超時時間以及運行在何種模式下)
runMode:beforeDate:是單次調用,其他兩種是循環調用runMode:beforeDate:方法。