視頻地址:http://v.youku.com/v_show/id_XODgxODkzODI0.html
1、Runloop是什么東西?
普通的命令式執行如下所示,程序順序執行代碼,執行完了就結束了:
int main(int argc, char *argv[]) {
NSLog(@"hello world!");
return 0;
}
Runloop就是一個循環,跑圈,一個死循環,程序會一直運行并不會退出,如下面的Event驅動,平時處于睡眠狀態,如果有Event喚醒了,那么就執行事件Event。
int main(int argc, char *argv[]) {
while (AppIsRunning) {
id whoWakesMe = SleepForWakingUp();
id event = GetEvent(whoWakesMe);
HandleEvent(event);
}
return 0;
}
2、Runloop的作用
為什么要有Runloop呢?Runloop主要由以下幾個方面的作用:
1、使程序一直運行并接受用戶輸入:我們的app必然不能像命令式執行一樣,執行完就退出了,我們需要app在我們不殺死它的時候一直運行著,并在由用戶事件的時候能夠響應,比如網絡輸入,用戶點擊等等,這是Runloop的首要任務;
2、決定程序在何時應該處理哪些事件:實際上程序會有很多事件,Runloop會有一定的機制來管理時間的處理時機等;
3、調用解耦(Message Queue):比方說手指點擊滑動會產生UIEvent事件,對于主調方來說,我不可能等到這個事件被執行了才去產生下一個事件,也就是主調方不能被被調方卡住。那么在實際實現中,被調方會有一個消息隊列,主調方會把消息扔到消息隊列中,然后不管了,消息的處理是由被調方不斷去從消息隊列中取消息,然后執行的。這樣的好處是主調方不需要知道消息是具體是怎么執行的,只要產生消息即可,從而實現了解耦;
4、節省CPU時間:在app沒設可干的時候,讓CPU閑著。
Runloop機制并不是iOS特有的,Android和Windows上面都有,只要有這種需要接受事件的程序都有這種實現機制,只是其他平臺上面不叫Runloop而已。
3、Runloop的封裝結構
如下圖所示是NSRunloop的實現:
1、NSRunloop:最上層的NSRunloop層實際上是對C語言實現的CFRunloop的一個封裝,實際上它沒干什么事,比如CFRunloop有一個過期時間是double類型,NSRunloop把它變味了NSDate類型;
2、CFRunloop:這是真正干事的一層,源代碼是開源的,并且是跨平臺的;
3、系統層:底層實現用到了GCD,mach kernel是蘋果的內核,比如runloop的睡眠和喚醒就是用mach kernel來實現的。
下面是跟Runloop有關的,我們平時用到的一些模塊,功能等等:
1)NSTimer計時器;
2)UIEvent事件;
3)Autorelease機制;
4)NSObject(NSDelayedPerforming):比如這些方法:performSelector:withObject:afterDelay:,performSelector:withObject:afterDelay:inModes:,cancelPreviousPerformRequestsWithTarget:selector:object:等方法都是和Runloop有關的;
5)NSObject(NSThreadPerformAddition):比如這些方法:performSelectorInBackground:withObject:,performSelectorOnMainThread:withObject:waitUntilDone:,performSelector:onThread:withObject:waitUntilDone:等方法都是和Runloop有關的;
4、Core Animation層的一些東西:CADisplayLink,CATransition,CAAnimation等;
5、dispatch_get_main_queue();
6、NSURLConnection;
4、從調用堆棧來看Runloop
如下圖是常見的調用堆棧:
從下往上一層層的看,最開始的start是dyld干的,然后是main函數,main函數接著調用UIApplicationMain,然后的GSEventRunModal是Graphics Services是處理硬件輸入,比如點擊,所有的UI事件都是它發出來的。緊接著的就是Runloop了,從圖中的可以看到從13到104個調用都是Runloop相關的。再上面的就是事件隊列處理,以及UI層的事件分發了。
5、Runloop的調用
幾乎所有線程的所有函數都是從下面六個函數之一調起:
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
6、Runloop的構成
1、Runloop與Thread是一一綁定的,但是并不是一個Thread只能起一個Runloop,它可以起很多,但是必須是嵌套結構,根Runloop只有一個;
2、RunloopMode是指的一個事件循環必須在某種模式下跑,系統會預定義幾個模式。一個Runloop有多個Mode;
3、CFRunloopSource,CFRunloopTimer,CFRunloopObserver這些元素是在Mode里面的,Mode與這些元素的對應關系也是1對多的;
CFRunloopTimer:比如下面的方法都是CFRunloopTimer的封裝:
CFRunloopSource:source是RunLoop的數據源(輸入源)的抽象類(protocol),Runloop定義了兩個Version的Source:
1、Source0:處理App內部事件,App自己負責管理(觸發),如UIEvent,CFSocket;
2、Source1:由Runloop和內核管理,mach port驅動,如CFMachPort(輕量級的進程間通信的方式,NSPort就是對它的封裝,還有Runloop的睡眠和喚醒就是通過它來做的),CFMessagePort;
CFRunloopObserver:這個是用來向外界報告Runloop當前的狀態的更改。
kCFRunLoopEntry = (1UL << 0),// 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1),// 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2),// 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5),// 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7),// 即將退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU//所有狀態
我們可以去設自己的Observer,來觀察Runloop的狀態變化。很多機制由RunloopObserver來觸發,比如CAAnimation,動畫并不是馬上就會被調用的,而是會等到kCFRunLoopBeforeWaiting或者kCFRunLoopAfterWaiting來執行,會等到Runloop執行一圈,收集所有的Animation之后一起來調用。
** CFRunloopObserver與Autorelease Pool:
** CFRunloopMode:Runloop在同一時間只能且必須在某一種特定的Mode下面Run,更換Mode時,必須要停止當前的Loop,然后重啟新的Loop,重啟的意思是退出當前的while循環,然后重新設置一個新的while。Mode是iOS App滑動流暢的關鍵,我們也可以自己創建一個Mode,但是基本不會這樣去做。NSDefaultRunLoopMode:默認狀態,空閑狀態,點擊事件,普通的回調等;NSTrackingRunLoopMode:ScrollView滑動時;UIInitializationRunLoopMode:私有的,App啟動的時候,第一個頁面加載之后就切換為NSDefaultRunLoopMode了,避免啟動的時候受到影響;NSRunLoopCommonModes:這個mode包含第一個和第二個,都可以跑。
ScrollView滑動過程:下面看看scrollView在開始滑動和停止滑動時候的調用堆棧,設置符號斷點CFRunLoopWakeUp,查看主線程的調用堆棧,如下所示:
查看調用堆棧可以看到開始滑動的時候有一個pushRunLoopMode方法的調用,在停止滑動的時候有一個pushRunLoopMode方法的調用,實際上Mode的切換過程是這樣的:
NSDefaultRunLoopMode -> UITrakingRunLoopMode -> NSDefaultRunLoopMode
RunLoop和dispatch_get_main_queue():GCD中分發到main queue中的block輩分發到main runloop執行,這是main queue的特別之處,dispatch_after到main queue同理:
5、RunLoop的掛起和喚醒
空閑時刻暫停程序時(按下debug的暫停鍵),會看到如下的堆棧:
這就是Runloop的掛起,實際上是指定一個端口,給內核發消息,這里是一個等待消息,就是等待被喚醒。
1、指定用于被喚醒的mach port端口;
2、調用mach_msg監聽喚醒端口,被喚醒前,系統內核將這個線程掛起,停留在mach_msg_trap狀態;
3、由另一個線程(或者另一個進程中的某個線程)向內核發送這個端口的msg后,trap狀態被喚醒,RunLoop繼續開始干活。
6、Runloop迭代執行順序
下面是偽代碼:
//設定過期時間
SetupThisRunLoopRunTimeOutTimer(); //by GCD timer
do{
//通知Observer要跑timer跟source
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
//運行到此刻,去檢測當前加到消息隊列source0的消息,此方法遍歷source0去執行
__CFRunLoopDoSource0();
//詢問GCD有沒有分到主線程的東西需要調用
CheckIfExistMessageInMainDispatchQueue(); //GCD
//通知Observer要進入睡眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
//此刻獲取到是哪個端口把我叫醒
var wakeUpPort = SleepAndWaitForWakingUpPorts();
// mach_msg_trap
// Zzz...
// Received mach_msg, wake up!
//通知Observer我要醒了~
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
//Handler msgs
if(wakeUpPort == timerPort){
//如果是timer喚醒就去執行timer
__CFRunLoopDoTimer();
}else if(wakeUpPort == mainDispatchQueuePort){
//GCD需要我,就去調GCD的事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE();
}else{
//比如說網絡來數據了就會用這個端口喚醒,然后做數據處理
__CFRunloopDoSource1();
}
__CFRunLoopDoBlocks();
}while (!stop && !timeOut);//如果沒被外部干掉或者時間沒到,繼續循環
跑while循環之前需要通過GCD來設置過期時間,不然真成死循環了;然后告訴observer要開始執行timer和source的狀態了;然后遍歷消息隊列中source0的消息并執行;然后詢問GCD中有沒有分到主線程的任務需要執行;然后通知要進入睡眠掛起狀態了;然后會一直卡在SleepAndWaitForWakingUpPorts函數這里,直到有事件喚醒并返回喚醒的端口;然后通知我要醒了;然后根據喚醒端口類型來進行相應的處理;
7、RunLoop實踐
這是AFNetworking中的Runloop的創建代碼:
所以說currentRunLoop方法是獲得RunLoop,如果沒有就會創建RunLoop,后面一行代碼是為了讓線程活下來,如果沒有這一行代碼,RunLoop并不會掛起,線程運行完就會退出;這是創建一個常駐服務線程得很好的樣例代碼。
TableView延遲加載圖片的新思路:
將setImage放到NSDefaultRunLoopMode去做,也就是在滑動的時候并不會去調用這個方法,而是會等到滑動完畢切換到NSDefaultRunLoopMode下面才會調用。
UIImage *downLoadImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
讓Crash的App回光返照:
1、program received signal:SIGABRT SIGABRT一般是過度release或者發送unrecogized selector導致。
2、EXC_BAD_ACCESS是訪問已被釋放的內存導致,野指針錯誤。
由 SIGABRT 引起的Crash 是系統發這個signal給App,程序收到這個signal后,就會把主線程的RunLoop殺死,程序就Crash了 該例只針對 SIGABRT引起的Crash有效。
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//獲取所有Mode,因為可能有很多Mode,每個Mode都需要跑,此處可以選擇提交下崩潰信息之類的
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"程序崩潰了" message:@"崩潰信息" delegate:nil cancelButtonTitle:@"取消" otherButtonTitles:nil];
[alertView show];
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runloop));
while (1) {
//快速切換Mode
for (NSString *mode in allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}