什么是Runloop
Runloop即運行循環。為什么你的APP放在那里不去動它,在某個時間點去操作它,它還會給你反饋。就是因為Runloop的存在。
總結一下,因為Runloop的存在,保證你的程序不會死。
主要負責什么?
使程序一直運行并接受用戶輸入
決定程序在何時處理一些Event
調用解耦(Message Queue)
節省CPU時間(沒事的時候閑著,有事的時候處理)
誰依賴NSRunloop
NSTimer
UIEvent
autorelease
NSObject(NSDelaydPerforming)
NSObject(NSThreadPerformAddtion)
CADisplayLink
CATransition
CAAnimation
dispatch_get_main_queue()
AFNetworking(NSURLConnection)
...
主線程幾乎所有的函數都從以下的6個之1的調起
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
構成元素
因為NSRunloop是對CFRunloop的封裝,所以這里只看CFRunLoop就可以了。
CFRunLoopTimer的封裝
系統提供的NSTimer、CADisplayLink、performSelector等都是對CFRunLoopTimer的封裝。
CFRunLoopSource
Source是RunLoop的數據源抽象類(用OC的話來講就是protocol)。
RunLoop定義了兩個版本的Source,分別是Source0和Source1。
Source0:處理APP內部事件、APP自己負責管理(觸發),如UIEvent、CFSocket
Source1:由RunLoop和內核管理,Mach Port驅動,如CFMachPort、CFMessagePort
CFRunLoopObserver
觀察者,向外部報告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
};
框架中很多機制都由CFRunLoopObserver觸發,比如CAAnimation
舉例:
self.navigationController pushViewController:<#(nonnull UIViewController *)#> animated:<#(BOOL)#>
當程序執行完這行代碼時,我們可以看到經歷push動畫之后,到達了一個新的界面。
但其實并不是執行完這行代碼就出現了Push的動畫。
其實,執行這段代碼時不會立刻就掉push動畫,而是要RunLoop循環一圈收集所有的Animation操作,匯集起來一起去調。
CFRunLoopObserver與AutoreleasePool
對象的釋放并不是在{}括號結束。而是稍微延遲了一點。
堆棧如下:
_wrapRunLoopAutoreleasePoolHandler
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
UIKit通過RunLoopOberser在RunLoop兩次Sleep間對AutoreleasePool進行Pop和Push,將這次Loop產生的Autorelease對象釋放。
也就是RunLoop跑一圈沒事了就睡,被喚醒了再跑下一圈,在兩次sleep之間對自動釋放池進行釋放。
CFRunLoopMode
注意
RunLoop在同一段時間只能且必須在一種特定Mode下Run。
更換Mode時,需要停止當前Loop,然后重啟新Mode。
Mode是iOS滑動順暢的關鍵。
類型
- NSDefaultRunLoopMode
默認狀態(空閑狀態),比如點擊按鈕都是這個狀態 - UITrackingRunLoopMode
滑動時的Mode。比如滑動UIScrollView時。 - UIInitializationRunLoopMode
私有的,APP啟動時。就是從iphone桌面點擊APP的圖標進入APP到第一個界面展示之前,在第一個界面顯示出來后,UIInitializationRunLoopMode就被切換成了NSDefaultRunLoopMode。 - NSRunLoopCommonModes
它是NSDefaultRunLoopMode和UITrackingRunLoopMode的集合。結構類似于一個數組。在這個mode下執行其實就是兩個mode都能執行而已。
典型的應用場景這樣:當前界面有開啟一個NSTimer,并且滑動UIScrollView。正常開啟NSTimer后,滑動UIScrollView時它是不滑動的。解決辦法就是把這個timer加入到當前的RunLoop,并把RunLoop的mode設置為NSRunLoopCommonModes。這樣就可以保證不管你是NSDefaultRunLoopMode里跑,還是UITrackingRunLoopMode里跑,這個timer都可以執行。
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.0625
target:self
selector:@selector(progressChange)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
當你開始滑動UIScrollView時,RunLoop的mode狀態變化如下:
NSDefaultRunLoopMode -> UITrackingRunLoopMode -> NSDefaultRunLoopMode
開始滑動時,第一次mode的切換會把NSDefaultRunLoopMode停掉。然后開啟新的UITrackingRunLoopMode。當滑動停止時,由UITrackingRunLoopMode切換回NSDefaultRunLoopMode,這時UITrackingRunLoopMode被停止,又切換回了老的NSDefaultRunLoopMode(這個老的NSDefaultRunLoopMode應該是重新開始的)。
RunLoop和GCD的關系
RunLoop和GCD的關系,準確來說是只要使用了dispatch_get_main_queue(),就與RunLoop有了關系。
因為GCD中dispatch到main queue的block被分發到main RunLoop執行。
RunLoop的掛起和喚醒
我寫了個demo,運行,然后點擊debug欄的暫停,查看堆棧,如下:
- 指定用于喚醒的mach_port接口
- 調用mach_msg監聽喚醒端口,被喚醒前,系統內核將這個線程掛起,停留在mach_msg_trap狀態
- 由另一個線程(或另一個進程中的某個線程)向內核發送這個端口的msg后,trap狀態被喚醒,RunLoop繼續開始干活。
RunLoop迭代執行順序
SetupThisRunLoopRunTimeoutTimer(); //by GCD timer
do{
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
__CFRunLoopDoSource0();
CheckIfExistMessagesInMainDispatchQueue(); //GCD
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
var wakeUpPort = SleepAndWaitForWakingUpPorts();
//mach_msg_trap
//Zzz...
//Received mach_msg , wake up
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
//Handle msgs
if(wakeUpPort == timerPort){
__CFRunLoopDoTimers();
}else if(wakeUpPort == mainDispatchQueuePort) {
//GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
}else {
__CFRunLoopDoSource1();
}
__CFRunLoopDOBlocks();
}while(!stop && !timeout);
代碼解讀
//首先do..while循環不能是一個死循環,所以在這里設置一個過期時間
//這件事是GCD干的,用來檢測do..while循環跑了多久
SetupThisRunLoopRunTimeoutTimer();
//開始跑循環
do{
//告訴observer我要跑timer了
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
//告訴observer我要跑source了
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
//程序跑到這里會查詢Source0有什么消息
__CFRunLoopDoSource0();
//詢問GCD你有沒有存在主線程的東西需要我幫你調
CheckIfExistMessagesInMainDispatchQueue(); //GCD
//告訴observer我要睡了,RunLoop進入到掛起狀態
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
//進入trap狀態,程序跑到這里就卡在這不動了,等待被某個Port喚醒
var wakeUpPort = SleepAndWaitForWakingUpPorts();
//被喚醒后,告訴observer我被喚醒了
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
//假如是被timer喚醒的
if(wakeUpPort == timerPort){
//就去循環遍歷和timer有關的回調
__CFRunLoopDoTimers();
}else if(wakeUpPort == mainDispatchQueuePort) {
//如果是主線程的GCD把我喚醒的,那RunLoop就知道GCD要讓它做事了,然后就取調GCD的這些事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
}else {
//如果都不是,就是Source1,Source1是基于Port事件的,比如網絡某個端口來數據了,就會把RunLoop喚醒,去對來的數據進行處理
__CFRunLoopDoSource1();
}
__CFRunLoopDoBlocks();
}while(!stop && !timeout);
//判斷條件:有沒有被外部干掉 && 到了過期時間
//如果過期時間不手動進行設置的話,默認值是一個很大的值,可能是Int_Max
AFNetworking是如何玩轉RunLoop的
+ (void)networkRequestThreadEntryPoint:(id)_unuserd object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
//為了不讓runloop run起來沒事干導致消失
//所以給runloop加了一個NSMachPort,給它一個mode去監聽
//實際上port什么也沒干,就是讓runloop一直在等,目的就是讓runloop一直活著
//這是一個創建常駐服務線程的好方法
NSRunloop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkReuqestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread =
[[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
一個TableView延遲加載圖片的新思路
以前是怎么解決的?
通過UITableView的代理方法,判斷如果處于滑動狀態就不去設置cell上的圖片,如果沒有處于滑動狀態就取設置cell上的圖片。
而現在通過Runloop就有一個十分簡便的方法。
//在cell里面把設置圖片的事情在NSDefaultRunloopMode里面去做。
//當主線程的tableview不再滑動的時候就會去設置圖片
UIImage *dowloadImage = ...;
[self.iconImageView performSelector:@selector(setImage:) withObject:dowloadImage afterDelay:0 inModes:@[NSDefaultRunloopMode]];
這樣去設置圖片就簡便了很多,不用再去判斷tableview的代理方法。代碼也會很清爽。
文明轉帖,原貼出處http://www.lxweimin.com/p/296f182c8faa