1.RunLoop的概念
RunLoop其實就是一個大的do while循環(huán),它的關鍵點在于如何管理事件/消息泼返,如何讓線程在沒有處理消息時休眠以避免資源占用,在有消息到來時立刻被喚醒宝恶。所以RunLoop實際上是一個對象符隙,這個對象管理了其需要處理的時間和消息,并提供了一個函數(shù)來執(zhí)行上面的事件邏輯垫毙。因此runLoop可以說就是為了線程而生霹疫。
2.RunLoop 的作用:
1.使程序一直運行并接受用戶輸入
2.決定程序在何時應該處理哪些事件
3.調用解耦(事件隊列的分發(fā)與派放)
4.節(jié)省CPU時間
5.RunLoop也負責autorelease pool的創(chuàng)建與釋放(當一個運行循環(huán)結束或者RunLoop退出和休眠的時候,它都會釋放一次autorelease pool)
其中RunLoop運行一次的時間為1/60 S
與Runloop最密切相關的:NSTimer 综芥、UIEvent 丽蝎、Autorelease
CFRunLoop是基于pthread來管理的。蘋果不允許直接創(chuàng)建RunLoop,它只提供了兩個自動獲取的函數(shù)CFRunLoopGetMain() 和 CFRunLoopGetCurrent()膀藐,當線程中沒有RunLoop的時候屠阻,CFRunLoopGetCurrent()其實會創(chuàng)建一個RunLoop對象并返回。
3.RunLoop的結構
RunLoop接受事件來自兩種不同的來源:輸入源和定時源额各,輸入源傳遞異步事件国觉,通常消息來自其他線程和程序,定時源則傳遞同步事件虾啦,發(fā)生在特定時間或者重復的時間間隔麻诀。兩種源都使用程序的某一特定處理例程來處理到達的事件。輸入源包括兩種傲醉,分別是基于端口的輸入源和自定義輸入源蝇闭。基于端口的輸入源監(jiān)聽程序相應的輸入源硬毕,自定義輸入源則監(jiān)聽自定義的事件源呻引。基于端口的輸入源由內核發(fā)送吐咳,而自定義的輸入源需要人工從其他線程發(fā)送逻悠。
RunLoop由線程和Mode組成,線程和RunLoop之間是一一對應的關系韭脊,其關系是保存在一個字典里面蹂风,線程剛創(chuàng)建時并沒有RunLoop,如果你不主動獲取那么它一直不會有乾蓬。RunLoop的創(chuàng)建是發(fā)生在第一次獲取時惠啄,RunLoop的銷毀是發(fā)生在線程結束時。主線程的RunLoop默認是開啟的任内,當程序在運行的時候會產生大量的對象撵渡,這些對象存儲在RunLoop的釋放池里面,當RunLoop循環(huán)完一次之后會釋放自動釋放池同時創(chuàng)建新的自動釋放池死嗦。子線程沒有開啟RunLoop需要手動獲取趋距,因為子線程的RunLoop是手動獲取的,所以自動釋放池默認也沒有越除,當我們在子線程里面創(chuàng)建了大量的臨時對象的時候就需要創(chuàng)建自動釋放池节腐。
一個RunLoop包含若干個RunLoopMode,但是一個RunLoop每次只能加入一種Mode,每一個Mode里面包含若干個source/timer/observer外盯。每次調用RunLoop的主函數(shù)時只能指定其中一個Mode,這個Mode被稱為currentMode,如果需要切換Mode只能退出RunLoop然后再重新指定另外的Mode進入。這樣做主要是為了分開不同組的source/timer/observer讓其不受影響翼雀。
Run loop模式是所有要監(jiān)視的輸入源和定時源以及要通知的run loop注冊觀察者的集合饱苟。每次運行你的run loop,你都要指定(無論顯示還是隱式)其運行個模式狼渊。在run loop運行過程中箱熬,只有和模式相關的源才會被監(jiān)視并允許他們傳遞事件消息。(類似的狈邑,只有和模式相關的觀察者會通知run loop的進程)城须。和其他模式關聯(lián)的源只有在run loop運行在其模式下才會運行,否則處于暫停狀態(tài)米苹。
4.RunLoop的特點
RunLoop在同一段時間只能且必須在一種特定的Mode下run
更換Mode時糕伐,需要停止當前Loop,然后重啟Loop
Mode是iOS App滑動順暢的關鍵
當傳入一個新的mode name 但是RunLoop內部沒有對應的mode時,RunLoop會幫你創(chuàng)建對應的RunLoopMode
5. RunLoopSource
CFRunLoopSourceRef 是事件產生的地方蘸嘶。Souce是RunLoop的數(shù)據(jù)抽象類赤炒。Source有兩個版本:Source0 和 Source1。
· Source0 只包含了一個回調(函數(shù)指針)亏较,它并不能主動觸發(fā)事件莺褒。使用時,你需要先調用 CFRunLoopSourceSignal(source)雪情,將這個 Source 標記為待處理遵岩,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件巡通。(Souce0處理App內部事件尘执,App自己負責管理觸發(fā),如UIEvent,CFSocket)
· Source1 包含了一個 mach_port 和一個回調(函數(shù)指針)宴凉,被用于通過內核和其他線程相互發(fā)送消息誊锭。這種 Source 能主動喚醒 RunLoop 的線程。(Souce1由RunLoop和內核管理弥锄,如CFMach,CFMessage)
其實可以簡單的理解為RunLoop通常處理的事件源有兩大種類丧靡,分別是time souce和input source,input source是異步消息通常來自其他線程或者程序。time source是timer中的同步事件
6.RunLoopTimer
CFRunLoopTimerRef 是基于時間的觸發(fā)器籽暇,它和 NSTimer 是toll-free bridged 的温治,可以混用。其包含一個時間長度和一個回調(函數(shù)指針)戒悠。當其加入到 RunLoop 時熬荆,RunLoop會注冊對應的時間點,當時間點到時绸狐,RunLoop會被喚醒以執(zhí)行那個回調卤恳。(NSTimer受RunLoop的Mode影響累盗,GCD的定時器不受RunLoop的Mode影響)
7.RunLoopObserver
CFRunLoopObserverRef 是觀察者,(它向外部報告RunLoop當前狀態(tài)的更改)每個 Observer 都包含了一個回調(函數(shù)指針)突琳,當 RunLoop 的狀態(tài)發(fā)生變化時若债,觀察者就能通過回調接受到這個變化”窘瘢可以觀測的時間點有以下幾個:
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 被統(tǒng)稱為 mode item,一個 item 可以被同時加入多個 mode主巍。但一個 item 被重復加入同一個 mode 時是不會有效果的冠息。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出孕索,不進入循環(huán)逛艰。
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集合搞旭。系統(tǒng)中共有5個Mode,每一個Mode可以將自己標記為Common屬性(通過將其ModeName屬性添加到RunLoop的commonModes中)散怖。每當RunLoop的內部發(fā)生變化時,RunLoop都會將commonModeItems里面的Souce/Observer/Timer同步到具有Common標記的所有Mode里肄渗。
系統(tǒng)默認注冊了5個Mode(前兩個跟最后一個常用)
? kCFRunLoopDefaultMode:App的默認Mode镇眷,通常主線程是在這個Mode下運行(NSTimer scheduledTimerWithTime這個方法默認加入的就是KCFRunLoopDefaultMode)
? UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動翎嫡,保證界面滑動時不受其他 Mode 影響
? UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode欠动,啟動完成后就不再使用
? GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內部 Mode,通常用不到
? kCFRunLoopCommonModes:這個Mode其實包含了第一個和第二個Mode
GCD中的任務隊列被分配到main queue的block會被分發(fā)到main RunLoop中執(zhí)行惑申。
當RunLoop掛起的時候會指定用于喚醒mach_port的端口具伍。同時會調用mach_msg監(jiān)聽喚醒端口,被喚醒前圈驼,系統(tǒng)會將這個線程掛起人芽,停留在mach_msg_trap狀態(tài)
由另一個線程或者另一個進程中的某個線程向內核發(fā)送這個端口的msg后,trap狀態(tài)被喚醒绩脆,runLoop繼續(xù)開始干活
CFRunLoop的默認超時時間很長
AutoreleasePool
App啟動后萤厅,蘋果在主線程RunLoop里注冊了兩個Observer,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop)靴迫,其回調內會調用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池祈坠。其 order 是-2147483647,優(yōu)先級最高矢劲,保證創(chuàng)建釋放池發(fā)生在其他所有回調之前赦拘。
第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池芬沉。這個 Observer 的 order 是 2147483647躺同,優(yōu)先級最低阁猜,保證其釋放池子發(fā)生在其他所有回調之后。
在主線程執(zhí)行的代碼蹋艺,通常是寫在諸如事件回調剃袍、Timer回調內的。這些回調會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著捎谨,所以不會出現(xiàn)內存泄漏民效,開發(fā)者也不必顯示創(chuàng)建 Pool 了。
9.定時器與RunLoop的關系:
NSTimer 其實就是 CFRunLoopTimerRef涛救,他們之間是 toll-free bridged 的畏邢。一個 NSTimer 注冊到 RunLoop 后,RunLoop 會為其重復的時間點注冊好事件检吆。例如 10:00, 10:10, 10:20 這幾個時間點舒萎。RunLoop為了節(jié)省資源,并不會在非常準確的時間點回調這個Timer蹭沛。Timer 有個屬性叫做 Tolerance (寬容度)臂寝,標示了當時間點到后,容許有多少最大誤差摊灭。
如果某個時間點被錯過了咆贬,例如執(zhí)行了一個很長的任務,則那個時間點的回調也會跳過去帚呼,不會延后執(zhí)行素征。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交萝挤,那我只能等 10:20 這一趟了御毅。
PerformSelecter
當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創(chuàng)建一個 Timer 并添加到當前線程的 RunLoop 中怜珍。所以如果當前線程沒有 RunLoop端蛆,則這個方法會失效。
當調用 performSelector:onThread: 時酥泛,實際上其會創(chuàng)建一個 Timer 加到對應的線程去今豆,同樣的,如果對應線程沒有 RunLoop 該方法也會失效柔袁。
10.何時使用RunLoop
我們知道當我們的程序啟動的時候呆躲,主線程已經默認創(chuàng)建了一個runLoop,所以只有在二級線程中我們才有機會創(chuàng)建runLoop。RunLoop的主要作用是為了幫助線程常駐進程捶索,所以僅當在為你的程序創(chuàng)建輔助線程的時候插掂,你才需要顯式運行一個run loop。對于輔助線程,你需要判斷一個run loop是否是必須的辅甥。如果是必須的酝润,那么你要自己配置并啟動它。你不需要在任何情況下都去啟動一個線程的run loop璃弄。比如要销,你使用線程來處理一個預先定義的長時間運行的任務時,你應該避免啟動run loop夏块。Run loop在你要和線程有更多的交互時才需要疏咐,比如以下情況:
使用端口或自定義輸入源來和其他線程通信
在線程中執(zhí)行定時事件源的任務
Cocoa中使用任何performSelector...的方法
在線程中執(zhí)行較為頻繁的,周期性的任務
如果你決定在程序中使用run loop脐供,那么它的配置和啟動都很簡單浑塞。和所有線程編程一樣,你需要計劃好在輔助線程退出線程的情形患民。讓線程自然退出往往比強制關閉它更好缩举。
11.線程笨寻穑活
在AFN中匹颤,把網(wǎng)絡的請求和解析都放在了一個子線程中,就是下面這段代碼
+ (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];
}
}
這段代碼用單例創(chuàng)建了一個線程同時將線程加入了RunLoop中托猩,這是AFN中用來線程庇”停活的方法。這里為什么要加入RunLoop是因為我們創(chuàng)建的線程是脫離線程京腥,默認在執(zhí)行完任務之后就會被系統(tǒng)回收赦肃,為了讓線程一直存活下去必須讓它加入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];
}
當我們運行程序的時候我們會發(fā)現(xiàn),內存在不斷的上漲欠气,同時控制臺會輸出[NSThread start]: Thread creation failed with error 35這個錯誤厅各。我們嘗試把addToRunLoop這個方法里面的代碼封掉,發(fā)現(xiàn)程序運行正常并且內存并不會一直上漲预柒,那么可以猜測队塘,是因為線程加入了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];
}
運行程序發(fā)現(xiàn)內存還是在增長憔古,并且控制臺也會輸出[NSThread start]: Thread creation failed with error 35這個錯誤,看來我們沒有正確的取消RunLoop淋袖。
RunLoop的啟動方式:
(1)run (直接進入鸿市,但會使線程進入死循環(huán)從而不利于控制RunLoop,結束RunLoop的唯一方式就是kill它)
(2)runUntilDate(RunLoop會在處理完事件或者超時時間后結束)
(3)runMode:beforeDate: (指定RunLoop的超時時間以及運行在何種模式下)
runMode:beforeDate:是單次調用,其他兩種是循環(huán)調用runMode:beforeDate:方法灸芳。