孫源的Runloop視頻整理

視頻地址: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的實現:


Paste_Image.png

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

如下圖是常見的調用堆棧:

Paste_Image.png

從下往上一層層的看,最開始的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的構成

Paste_Image.png

1、Runloop與Thread是一一綁定的,但是并不是一個Thread只能起一個Runloop,它可以起很多,但是必須是嵌套結構,根Runloop只有一個;
2、RunloopMode是指的一個事件循環必須在某種模式下跑,系統會預定義幾個模式。一個Runloop有多個Mode;
3、CFRunloopSource,CFRunloopTimer,CFRunloopObserver這些元素是在Mode里面的,Mode與這些元素的對應關系也是1對多的;
CFRunloopTimer:比如下面的方法都是CFRunloopTimer的封裝:

Paste_Image.png

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

Paste_Image.png

** CFRunloopMode
:Runloop在同一時間只能且必須在某一種特定的Mode下面Run,更換Mode時,必須要停止當前的Loop,然后重啟新的Loop,重啟的意思是退出當前的while循環,然后重新設置一個新的while。Mode是iOS App滑動流暢的關鍵,我們也可以自己創建一個Mode,但是基本不會這樣去做。NSDefaultRunLoopMode:默認狀態,空閑狀態,點擊事件,普通的回調等;NSTrackingRunLoopMode:ScrollView滑動時;UIInitializationRunLoopMode:私有的,App啟動的時候,第一個頁面加載之后就切換為NSDefaultRunLoopMode了,避免啟動的時候受到影響;NSRunLoopCommonModes:這個mode包含第一個和第二個,都可以跑。
ScrollView滑動過程:下面看看scrollView在開始滑動和停止滑動時候的調用堆棧,設置符號斷點CFRunLoopWakeUp,查看主線程的調用堆棧,如下所示:
Paste_Image.png

Paste_Image.png

查看調用堆棧可以看到開始滑動的時候有一個pushRunLoopMode方法的調用,在停止滑動的時候有一個pushRunLoopMode方法的調用,實際上Mode的切換過程是這樣的:
NSDefaultRunLoopMode -> UITrakingRunLoopMode -> NSDefaultRunLoopMode
RunLoop和dispatch_get_main_queue():GCD中分發到main queue中的block輩分發到main runloop執行,這是main queue的特別之處,dispatch_after到main queue同理:
Paste_Image.png

5、RunLoop的掛起和喚醒

空閑時刻暫停程序時(按下debug的暫停鍵),會看到如下的堆棧:


Paste_Image.png

這就是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的創建代碼

Paste_Image.png

所以說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);  
        }  
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容