iOS技術文檔No.15 AppKit_NSRunloop

什么是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__

構成元素

1.png

因為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滑動順暢的關鍵。

類型
  1. NSDefaultRunLoopMode
    默認狀態(空閑狀態),比如點擊按鈕都是這個狀態
  2. UITrackingRunLoopMode
    滑動時的Mode。比如滑動UIScrollView時。
  3. UIInitializationRunLoopMode
    私有的,APP啟動時。就是從iphone桌面點擊APP的圖標進入APP到第一個界面展示之前,在第一個界面顯示出來后,UIInitializationRunLoopMode就被切換成了NSDefaultRunLoopMode。
  4. 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欄的暫停,查看堆棧,如下:

2.png
  1. 指定用于喚醒的mach_port接口
  2. 調用mach_msg監聽喚醒端口,被喚醒前,系統內核將這個線程掛起,停留在mach_msg_trap狀態
  3. 由另一個線程(或另一個進程中的某個線程)向內核發送這個端口的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

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

推薦閱讀更多精彩內容