iOS刨根問底-深刻理解RunLoop

概述

RunLoop做爲iOS中一個基礎組件和線程有着千絲萬縷的關係,同時也是不少常見技術的幕後功臣。儘管在平時多數開發者不多直接使用RunLoop,可是理解RunLoop能夠幫助開發者更好的利用多線程編程模型,同時也能夠幫助開發者解答平常開發中的一些疑惑。本文將從RunLoop源碼着手,結合RunLoop的實際應用來逐步解開它的神祕面紗。php

開源的RunloopRef

一般所說的RunLoop指的是NSRunloop或者CFRunloopRef,CFRunloopRef是純C的函數,而NSRunloop僅僅是CFRunloopRef的OC封裝,並未提供額外的其餘功能,所以下面主要分析CFRunloopRef,蘋果已經開源了CoreFoundation源代碼,所以很容易找到CFRunloop源代碼
從代碼能夠看出CFRunloopRef其實就是**__CFRunloop這個結構體指針(按照OC的思路咱們能夠將RunLoop當作一個對象),這個對象的運行纔是咱們一般意義上說的運行循環,核心方法是__CFRunloopRun()**,爲了便於閱讀就再也不直接貼源代碼,放一段僞代碼方便你們閱讀:html

int32_t __CFRunLoopRun()
{
    // 通知即將進入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    
    do
    {
        // 通知將要處理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        // 處理非延遲的主線程調用
        __CFRunLoopDoBlocks();
        // 處理Source0事件
        __CFRunLoopDoSource0();
        
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks();
         }
        /// 若是有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 而後跳轉去處理消息。
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort();
            if (hasMsg) goto handle_msg;
        }
            
        /// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }
            
        // GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即將進入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待內核mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
        
        // 等待。。。
        
        // 從等待中醒來
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        
        // 處理因timer的喚醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();
        
        // 處理異步方法喚醒,如dispatch_async
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
            
        // 處理Source1
        else
            __CFRunLoopDoSource1();
        
        // 再次確保是否有同步的方法須要調用
        __CFRunLoopDoBlocks();
        
    } while (!stop && !timeout);
    
    // 通知即將退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

源代碼儘管不算太長,可是若是不太熟悉的話面對這麼一堆不知道作什麼的函數調用仍是會給人一種神祕感。可是如今能夠不用逐行閱讀,後面慢慢解開這層神祕面紗。如今只要瞭解上面的僞代碼知道核心的方法**__CFRunLoopRun**內部實際上是一個_do while_循環,這也正是Runloop運行的本質。執行了這個函數之後就一直處於「等待-處理」的循環之中,直到循環結束。只是不一樣於咱們本身寫的循環它在休眠時幾乎不會佔用系統資源,固然這是因爲系統內核負責實現的,也是Runloop精華所在。git

隨着Swift的開源蘋果也維護了一個Swift版本的跨平臺CoreFoundation版本,除了mac平臺它仍是適配了Linux和Windows平臺。可是鑑於目前不少關於Runloop的討論都是以OC版展開的,因此這裏也主要分析OC版本。github

下圖描述了Runloop運行流程(基本描述了上面Runloop的核心流程,固然能夠查看官方The Run Loop Sequence of Events描述):編程

RunLoopswift

整個流程並不複雜(須要注意的就是_黃色_區域的消息處理中並不包含source0,由於它在循環開始之初就會處理),整個流程其實就是一種Event Loop的實現,其餘平臺均有相似的實現,只是這裏叫作Runloop。可是既然RunLoop是一個消息循環,誰來管理和運行Runloop?那麼它接收什麼類型的消息?休眠過程是怎麼樣的?如何保證休眠時不佔用系統資源?如何處理這些消息以及什麼時候退出循環?還有一系列問題須要解開。緩存

注意的是儘管CFRunLoopPerformBlock在上圖中做爲喚醒機制有所體現,但事實上執行CFRunLoopPerformBlock只是入隊,下次RunLoop運行纔會執行,而若是須要當即執行則必須調用CFRunLoopWakeUp。cookie

Runloop Mode

從源碼很容易看出,Runloop老是運行在某種特定的CFRunLoopModeRef下(每次運行**__CFRunLoopRun()函數時必須指定Mode)。而經過CFRunloopRef對應結構體的定義能夠很容易知道每種Runloop均可以包含若干個Mode,每一個Mode又包含Source/Timer/Observer。每次調用Runloop的主函數__CFRunLoopRun()時必須指定一種Mode,這個Mode稱爲 _currentMode**,當切換Mode時必須退出當前Mode,而後從新進入Runloop以保證不一樣Mode的Source/Timer/Observer互不影響。網絡

struct __CFRunLoop {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;          /* locked for accessing mode list */
        __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
        Boolean _unused;
        volatile _per_run_data *_perRunData;              // reset for runs of the run loop
        pthread_t _pthread;
        uint32_t _winthread;
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;
        CFMutableSetRef _modes;
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFAbsoluteTime _runTime;
        CFAbsoluteTime _sleepTime;
        CFTypeRef _counterpart;
    };
    
    struct __CFRunLoopMode {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
        CFStringRef _name;
        Boolean _stopped;
        char _padding[3];
        CFMutableSetRef _sources0;
        CFMutableSetRef _sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
        CFMutableDictionaryRef _portToV1SourceMap;
        __CFPortSet _portSet;
        CFIndex _observerMask;
    #if USE_DISPATCH_SOURCE_FOR_TIMERS
        dispatch_source_t _timerSource;
        dispatch_queue_t _queue;
        Boolean _timerFired; // set to true by the source when a timer has fired
        Boolean _dispatchTimerArmed;
    #endif
    #if USE_MK_TIMER_TOO
        mach_port_t _timerPort;
        Boolean _mkTimerArmed;
    #endif
    #if DEPLOYMENT_TARGET_WINDOWS
        DWORD _msgQMask;
        void (*_msgPump)(void);
    #endif
        uint64_t _timerSoftDeadline; /* TSR */
        uint64_t _timerHardDeadline; /* TSR */
    };

系統默認提供的Run Loop Modes有kCFRunLoopDefaultMode(NSDefaultRunLoopMode)UITrackingRunLoopMode,須要切換到對應的Mode時只須要傳入對應的名稱便可。前者是系統默認的Runloop Mode,例如進入iOS程序默認不作任何操做就處於這種Mode中,此時滑動UIScrollView,主線程就切換Runloop到到UITrackingRunLoopMode,再也不接受其餘事件操做(除非你將其餘Source/Timer設置到UITrackingRunLoopMode下)。
可是對於開發者而言常常用到的Mode還有一個kCFRunLoopCommonModes(NSRunLoopCommonModes),其實這個並非某種具體的Mode,而是一種模式組合,在iOS系統中默認包含了
 NSDefaultRunLoopMode UITrackingRunLoopMode(注意:並非說Runloop會運行在kCFRunLoopCommonModes這種模式下,而是至關於分別註冊了 NSDefaultRunLoopMode UITrackingRunLoopMode。固然你也能夠經過調用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes組合)。多線程

注意:咱們經常還會碰到一些系統框架自定義Mode,例如Foundation中NSConnectionReplyMode。還有一些系統私有Mode,例如:GSEventReceiveRunLoopMode接受系統事件,UIInitializationRunLoopMode App啓動過程當中初始化Mode。更多系統或框架Mode查看這裏

CFRunLoopRef和CFRunloopMode、CFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef關係以下圖:

RunLoopMode

那麼CFRunLoopSourceRef、CFRunLoopTimerRef和CFRunLoopObserverRef到底是什麼?它們在Runloop運行流程中起到什麼做用呢?

Source

首先看一下官方Runloop結構圖(注意下圖的Input Source Port和前面流程圖中的Source0並不對應,而是對應Source1。Source1和Timer都屬於端口事件源,不一樣的是全部的Timer都共用一個端口「Mode Timer Port」,而每一個Source1都有不一樣的對應端口):

RunLoopSource

再結合前面RunLoop核心運行流程能夠看出Source0(負責App內部事件,由App負責管理觸發,例如UITouch事件)和Timer(又叫Timer Source,基於時間的觸發器,上層對應NSTimer)是兩個不一樣的Runloop事件源(固然Source0是Input Source中的一類,Input Source還包括Custom Input Source,由其餘線程手動發出),RunLoop被這些事件喚醒以後就會處理並調用事件處理方法(CFRunLoopTimerRef的回調指針和CFRunLoopSourceRef均包含對應的回調指針)。
可是對於CFRunLoopSourceRef除了Source0以外還有另外一個版本就是Source1,Source1除了包含回調指針外包含一個mach port,和Source0須要手動觸發不一樣,Source1能夠監聽系統端口和其餘線程相互發送消息,它可以主動喚醒RunLoop(由操做系統內核進行管理,例如CFMessagePort消息)。官方也指出能夠自定義Source,所以對於CFRunLoopSourceRef來講它更像一種協議,框架已經默認定義了兩種實現,若是有必要開發人員也能夠自定義,詳細狀況能夠查看官方文檔

Observer

struct __CFRunLoopObserver {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;
        CFRunLoopRef _runLoop;
        CFIndex _rlCount;
        CFOptionFlags _activities;      /* immutable */
        CFIndex _order;         /* immutable */
        CFRunLoopObserverCallBack _callout; /* immutable */
        CFRunLoopObserverContext _context;  /* immutable, except invalidation */
    };

相對來講CFRunloopObserverRef理解起來並不複雜,它至關於消息循環中的一個監聽器,隨時通知外部當前RunLoop的運行狀態(它包含一個函數指針_callout_將當前狀態及時告訴觀察者)。具體的Observer狀態以下:

/* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0), // 進入RunLoop 
        kCFRunLoopBeforeTimers = (1UL << 1), // 即將開始Timer處理
        kCFRunLoopBeforeSources = (1UL << 2), // 即將開始Source處理
        kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
        kCFRunLoopAfterWaiting = (1UL << 6), //從休眠狀態喚醒
        kCFRunLoopExit = (1UL << 7), //退出RunLoop
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

Call out

在開發過程當中幾乎全部的操做都是經過Call out進行回調的(不管是Observer的狀態通知仍是Timer、Source的處理),而系統在回調時一般使用以下幾個函數進行回調(換句話說你的代碼其實最終都是經過下面幾個函數來負責調用的,即便你本身監聽Observer也會先調用下面的函數而後間接通知你,因此在調用堆棧中常常看到這些函數):

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__();

例如在控制器的touchBegin中打入斷點查看堆棧(因爲UIEvent是Source0,因此能夠看到一個Source0的Call out函數CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION調用):

RunLoop_Source0_UITouch

RunLoop休眠

其實對於Event Loop而言RunLoop最核心的事情就是保證線程在沒有消息時休眠以免佔用系統資源,有消息時可以及時喚醒。RunLoop的這個機制徹底依靠系統內核來完成,具體來講是蘋果操做系統核心組件Darwin中的Mach來完成的(Darwin是開源的)。能夠從下圖最底層Kernel中找到Mach:

osx_architecture-kernels_drivers

Mach是Darwin的核心,能夠說是內核的核心,提供了進程間通訊(IPC)、處理器調度等基礎服務。在Mach中,進程、線程間的通訊是以消息的方式來完成的,消息在兩個Port之間進行傳遞(這也正是Source1之因此稱之爲Port-based Source的緣由,由於它就是依靠系統發送消息到指定的Port來觸發的)。消息的發送和接收使用<mach/message.h>中的mach_msg()函數(事實上蘋果提供的Mach API不多,並不鼓勵咱們直接調用這些API):

/*
     *  Routine:    mach_msg
     *  Purpose:
     *      Send and/or receive a message.  If the message operation
     *      is interrupted, and the user did not request an indication
     *      of that fact, then restart the appropriate parts of the
     *      operation silently (trap version does not restart).
     */
    __WATCHOS_PROHIBITED __TVOS_PROHIBITED
    extern mach_msg_return_t    mach_msg(
                        mach_msg_header_t *msg,
                        mach_msg_option_t option,
                        mach_msg_size_t send_size,
                        mach_msg_size_t rcv_size,
                        mach_port_name_t rcv_name,
                        mach_msg_timeout_t timeout,
                        mach_port_name_t notify);

mach_msg()的本質是一個調用mach_msg_trap(),這至關於一個系統調用,會觸發內核狀態切換。當程序靜止時,RunLoop停留在**__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而這個函數內部就是調用了mach_msg**讓程序處於休眠狀態。

Runloop和線程的關係

Runloop是基於pthread進行管理的,pthread是基於c的跨平臺多線程操做底層API。它是mach thread的上層封裝(能夠參見Kernel Programming Guide),和NSThread一一對應(而NSThread是一套面向對象的API,因此在iOS開發中咱們也幾乎不用直接使用pthread)。

pthread

蘋果開發的接口中並無直接建立Runloop的接口,若是須要使用Runloop一般CFRunLoopGetMain()CFRunLoopGetCurrent()兩個方法來獲取(經過上面的源代碼也能夠看到,核心邏輯在_CFRunLoopGet_當中),經過代碼並不難發現其實只有當咱們使用線程的方法主動get Runloop時纔會在第一次建立該線程的Runloop,同時將它保存在全局的Dictionary中(線程和Runloop兩者一一對應),默認狀況下線程並不會建立Runloop(主線程的Runloop比較特殊,任何線程建立以前都會保證主線程已經存在Runloop),同時在線程結束的時候也會銷燬對應的Runloop。

iOS開發過程當中對於開發者而言更多的使用的是NSRunloop,它默認提供了三個經常使用的run方法:

- (void)run; 
     - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
     - (void)runUntilDate:(NSDate *)limitDate;
  • run方法對應上面CFRunloopRef中的CFRunLoopRun並不會退出,除非調用CFRunLoopStop();一般若是想要永遠不會退出RunLoop纔會使用此方法,不然可使用runUntilDate。
  • runMode:beforeDate:則對應CFRunLoopRunInMode(mode,limiteDate,true)方法,只執行一次,執行完就退出;一般用於手動控制RunLoop(例如在while循環中)。
  • runUntilDate:方法實際上是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false),執行完並不會退出,繼續下一次RunLoop直到timeout。

RunLoop應用

NSTimer

前面一直提到Timer Source做爲事件源,事實上它的上層對應就是NSTimer(其實就是CFRunloopTimerRef)這個開發者常常用到的定時器(底層基於使用mk_timer實現),甚至不少開發者接觸RunLoop仍是從NSTimer開始的。其實NSTimer定時器的觸發正是基於RunLoop運行的,因此使用NSTimer以前必須註冊到RunLoop,可是RunLoop爲了節省資源並不會在很是準確的時間點調用定時器,若是一個任務執行時間較長,那麼當錯過一個時間點後只能等到下一個時間點執行,並不會延後執行(NSTimer提供了一個tolerance屬性用於設置寬容度,若是確實想要使用NSTimer而且但願儘量的準確,則能夠設置此屬性)。

NSTimer的建立一般有兩種方式,儘管都是類方法,一種是timerWithXXX,另外一種scheduedTimerWithXXX。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

兩者最大的區別就是後者除了建立一個定時器外會自動以NSDefaultRunLoopModeMode添加到當前線程RunLoop中,不添加到RunLoop中的NSTimer是沒法正常工做的。例以下面的代碼中若是timer2不加入到RunLoop中是沒法正常工做的。同時注意若是滾動UIScrollView(UITableView、UICollectionview是相似的)兩者是沒法正常工做的,可是若是將NSDefaultRunLoopMode改成NSRunLoopCommonModes則能夠正常工做,這也解釋了前面介紹的Mode內容。

#import "ViewController1.h"

    @interface ViewController1 ()
    @property (nonatomic,weak) NSTimer *timer1;
    @property (nonatomic,weak) NSTimer *timer2;
    @end
    
    @implementation ViewController1
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor blueColor];
        // timer1建立後會自動以NSDefaultRunLoopMode默認模式添加到當前RunLoop中,因此能夠正常工做
        self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];
        NSTimer *tempTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];
        // 若是不把timer2添加到RunLoop中是沒法正常工做的(注意若是想要在滾動UIScrollView時timer2能夠正常工做能夠將NSDefaultRunLoopMode改成NSRunLoopCommonModes)
        [[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
        self.timer2 = tempTimer;
        
        CGRect rect = [UIScreen mainScreen].bounds;
        UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
        [self.view addSubview:scrollView];
        
        UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
        contentView.backgroundColor = [UIColor redColor];
        [scrollView addSubview:contentView];
        scrollView.contentSize = contentView.frame.size;
    }
    
    - (void)timeInterval:(NSTimer *)timer {
        if (self.timer1 == timer) {
            NSLog(@"timer1...");
        } else {
            NSLog(@"timer2...");
        }
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self dismissViewControllerAnimated:true completion:nil];
    }
    
    - (void)dealloc {
        NSLog(@"ViewController1 dealloc...");
    }
    @end

注意上面代碼中UIViewController1對timer1和timer2並無強引用,對於普通的對象而言,執行完viewDidLoad方法以後(準確的說應該是執行完viewDidLoad方法後的的一個RunLoop運行結束)兩者應該會被釋放,但事實上兩者並無被釋放。緣由是:爲了確保定時器正常運轉,當加入到RunLoop之後系統會對NSTimer執行一次retain操做(特別注意:timer2建立時並沒直接賦值給timer2,緣由是timer2是weak屬性,若是直接賦值給timer2會被當即釋放,由於timerWithXXX方法建立的NSTimer默認並無加入RunLoop,只有後面加入RunLoop之後才能夠將引用指向timer2)。
可是即便使用了弱引用,上面的代碼中ViewController1也沒法正常釋放,緣由是在建立NSTimer2時指定了target爲self,這樣一來形成了timer1和timer2對ViewController1有一個強引用。解決這個問題的方法一般有兩種:一種是將target分離出來獨立成一個對象(在這個對象中建立NSTimer並將對象自己做爲NSTimer的target),控制器經過這個對象間接使用NSTimer;另外一種方式的思路仍然是轉移target,只是能夠直接增長NSTimer擴展(分類),讓NSTimer自身作爲target,同時能夠將操做selector封裝到block中。後者相對優雅,也是目前使用較多的方案(目前有大量相似的封裝,例如:NSTimer+Block)。顯然Apple也認識到了這個問題,若是你能夠確保代碼只在iOS 10下運行就可使用iOS 10新增的系統級block方案(上面的代碼中已經貼出這種方法)。
固然使用上面第二種方法能夠解決控制器沒法釋放的問題,可是會發現即便控制器被釋放了兩個定時器仍然正常運行,要解決這個問題就須要調用NSTimer的invalidate方法(注意:不管是重複執行的定時器仍是一次性的定時器只要調用invalidate方法則會變得無效,只是一次性的定時器執行完操做後會自動調用invalidate方法)。修改後的代碼以下:

#import "ViewController1.h"
    
    @interface ViewController1 ()
    @property (nonatomic,weak) NSTimer *timer1;
    @property (nonatomic,weak) NSTimer *timer2;
    @end
    
    @implementation ViewController1
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor blueColor];
    
        self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer1...");
        }];
        NSTimer *tempTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer2...");
        }];
        [[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
        self.timer2 = tempTimer;
        
        CGRect rect = [UIScreen mainScreen].bounds;
        UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
        [self.view addSubview:scrollView];
        
        UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
        contentView.backgroundColor = [UIColor redColor];
        [scrollView addSubview:contentView];
        scrollView.contentSize = contentView.frame.size;
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self dismissViewControllerAnimated:true completion:nil];
    }
    
    - (void)dealloc {
        [self.timer1 invalidate];
        [self.timer2 invalidate];
        NSLog(@"ViewController1 dealloc...");
    }
    @end

其實和定時器相關的另外一個問題你們也常常碰到,那就是NSTimer不是一種實時機制,官方文檔明確說明在一個循環中若是RunLoop沒有被識別(這個時間大概在50-100ms)或者說當前RunLoop在執行一個長的call out(例如執行某個循環操做)則NSTimer可能就會存在偏差,RunLoop在下一次循環中繼續檢查並根據狀況肯定是否執行(NSTimer的執行時間老是固定在必定的時間間隔,例如1:00:00、1:00:0一、1:00:0二、1:00:05則跳過了第四、5次運行循環)。
要演示這個問題請看下面的例子(注意:有些示例中可能會讓一個線程中啓動一個定時器,再在主線程啓動一個耗時任務來演示這個問,若是實際測試可能效果不會太明顯,由於如今的iPhone都是多核運算的,這樣一來這個問題會變得相對複雜,所以下面的例子選擇在同一個RunLoop中即加入定時器和執行耗時任務)

#import "ViewController.h"
    
    @interface ViewController ()
    @property (nonatomic,weak) NSTimer *timer1;
    @property (nonatomic,strong) NSThread *thread1;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor redColor];
        
        // 因爲下面的方法沒法拿到NSThread的引用,也就沒法控制線程的狀態
        //[NSThread detachNewThreadSelector:@selector(performTask) toTarget:self withObject:nil];
        self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(performTask) object:nil];
        [self.thread1 start];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self.thread1 cancel];
        [self dismissViewControllerAnimated:YES completion:nil];
    }
    
    - (void)dealloc {
        [self.timer1 invalidate];
        NSLog(@"ViewController dealloc.");
    }
    
    - (void)performTask {
        // 使用下面的方式建立定時器雖然會自動加入到當前線程的RunLoop中,可是除了主線程外其餘線程的RunLoop默認是不會運行的,必須手動調用
        __weak typeof(self) weakSelf = self;
        self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            if ([NSThread currentThread].isCancelled) {
                //[NSObject cancelPreviousPerformRequestsWithTarget:weakSelf selector:@selector(caculate) object:nil];
                //[NSThread exit];
                [weakSelf.timer1 invalidate];
            }
            NSLog(@"timer1...");
        }];
        
        NSLog(@"runloop before performSelector:%@",[NSRunLoop currentRunLoop]);
        
        // 區分直接調用和「performSelector:withObject:afterDelay:」區別,下面的直接調用不管是否運行RunLoop同樣能夠執行,可是後者則不行。
        //[self caculate];
        [self performSelector:@selector(caculate) withObject:nil afterDelay:2.0];
    
        // 取消當前RunLoop中註冊測selector(注意:只是當前RunLoop,因此也只能在當前RunLoop中取消)
        // [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(caculate) object:nil];
        NSLog(@"runloop after performSelector:%@",[NSRunLoop currentRunLoop]);
        
        // 非主線程RunLoop必須手動調用
        [[NSRunLoop currentRunLoop] run];
        
        NSLog(@"注意:若是RunLoop不退出(運行中),這裏的代碼並不會執行,RunLoop自己就是一個循環.");
        
        
    }
    
    - (void)caculate {
        for (int i = 0;i < 9999;++i) {
            NSLog(@"%i,%@",i,[NSThread currentThread]);
            if ([NSThread currentThread].isCancelled) {
                return;
            }
        }
    }
    
    @end

若是運行而且不退出上面的程序會發現,前兩秒NSTimer能夠正常執行,可是兩秒後因爲同一個RunLoop中循環操做的執行形成定時器跳過了中間執行的機會一直到caculator循環完畢,這也正說明了NSTimer不是實時系統機制的緣由。

可是以上程序還有幾點須要說明一下:

  1. NSTimer會對Target進行強引用直到任務結束或exit以後纔會釋放。若是上面的程序沒有進行線程cancel而終止任務則及時關閉控制器也沒法正確釋放。
  2. 非主線程的RunLoop並不會自動運行(同時注意默認狀況下非主線程的RunLoop並不會自動建立,直到第一次使用),RunLoop運行必需要在加入NSTimer或Source0、Sourc一、Observer輸入後運行不然會直接退出。例如上面代碼若是run放到NSTimer建立以前則既不會執行定時任務也不會執行循環運算。
  3. performSelector:withObject:afterDelay:執行的本質仍是經過建立一個NSTimer而後加入到當前線程RunLoop(通而過先後兩次打印RunLoop信息能夠看到此方法執行以後RunLoop的timer會增長1個。相似的還有performSelector:onThread:withObject:afterDelay:,只是它會在另外一個線程的RunLoop中建立一個Timer),因此此方法事實上在任務執行完以前會對觸發對象造成引用,任務執行完進行釋放(例如上面會對ViewController造成引用,注意:performSelector: withObject:等方法則等同於直接調用,原理與此不一樣)。
  4. 同時上面的代碼也充分說明了RunLoop是一個循環事實,run方法以後的代碼不會當即執行,直到RunLoop退出。
  5. 上面程序的運行過程當中若是忽然dismiss,則程序的實際執行過程要分爲兩種狀況考慮:若是循環任務caculate尚未開始則會在timer1中中止timer1運行(中止了線程中第一個任務),而後等待caculate執行並break(中止線程中第二個任務)後線程任務執行結束釋放對控制器的引用;若是循環任務caculate執行過程當中dismiss則caculate任務執行結束,等待timer1下個週期運行(由於當前線程的RunLoop並無退出,timer1引用計數器並不爲0)時檢測到線程取消狀態則執行invalidate方法(第二個任務也結束了),此時線程釋放對於控制器的引用。

CADisplayLink是一個執行頻率(fps)和屏幕刷新相同(能夠修改preferredFramesPerSecond改變刷新頻率)的定時器,它也須要加入到RunLoop才能執行。與NSTimer相似,CADisplayLink一樣是基於CFRunloopTimerRef實現,底層使用mk_timer(能夠比較加入到RunLoop先後RunLoop中timer的變化)。和NSTimer相比它精度更高(儘管NSTimer也能夠修改精度),不過和NStimer相似的是若是遇到大任務它仍然存在丟幀現象。一般狀況下CADisaplayLink用於構建幀動畫,看起來相對更加流暢,而NSTimer則有更普遍的用處。

AutoreleasePool

AutoreleasePool是另外一個與RunLoop相關討論較多的話題。其實從RunLoop源代碼分析,AutoreleasePool與RunLoop並無直接的關係,之因此將兩個話題放到一塊兒討論最主要的緣由是由於在iOS應用啓動後會註冊兩個Observer管理和維護AutoreleasePool。不妨在應用程序剛剛啓動時打印currentRunLoop能夠看到系統默認註冊了不少個Observer,其中有兩個Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**,這兩個是和自動釋放池相關的兩個監聽。

<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
    '' <CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}

第一個Observer會監聽RunLoop的進入,它會回調objc_autoreleasePoolPush()向當前的AutoreleasePoolPage增長一個哨兵對象標誌建立自動釋放池。這個Observer的order是-2147483647優先級最高,確保發生在全部回調操做以前。
第二個Observer會監聽RunLoop的進入休眠和即將退出RunLoop兩種狀態,在即將進入休眠時會調用objc_autoreleasePoolPop() objc_autoreleasePoolPush() 根據狀況從最新加入的對象一直往前清理直到遇到哨兵對象。而在即將退出RunLoop時會調用objc_autoreleasePoolPop() 釋放自動自動釋放池內對象。這個Observer的order是2147483647,優先級最低,確保發生在全部回調操做以後。
主線程的其餘操做一般均在這個AutoreleasePool以內(main函數中),以儘量減小內存維護操做(固然你若是須要顯式釋放【例如循環】時能夠本身建立AutoreleasePool不然通常不須要本身建立)。
其實在應用程序啓動後系統還註冊了其餘Observer(例如即將進入休眠時執行註冊回調_UIGestureRecognizerUpdateObserver用於手勢處理、回調爲_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用於界面實時繪製更新)和多個Source1(例如context爲CFMachPort的Source1用於接收硬件事件響應進而分發到應用程序一直到UIEvent),這裏再也不一一詳述。

UI更新

若是打印App啓動以後的主線程RunLoop能夠發現另一個callout爲**_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv**的Observer,這個監聽專門負責UI變化後的更新,好比修改了frame、調整了UI層級(UIView/CALayer)或者手動設置了setNeedsDisplay/setNeedsLayout以後就會將這些操做提交到全局容器。而這個Observer監聽了主線程RunLoop的即將進入休眠和退出狀態,一旦進入這兩種狀態則會遍歷全部的UI更新並提交進行實際繪製更新。
一般狀況下這種方式是完美的,由於除了系統的更新,還能夠利用setNeedsDisplay等方法手動觸發下一次RunLoop運行的更新。可是若是當前正在執行大量的邏輯運算可能UI的更新就會比較卡,所以facebook推出了AsyncDisplayKit來解決這個問題。AsyncDisplayKit實際上是將UI排版和繪製運算儘量放到後臺,將UI的最終更新操做放到主線程(這一步也必須在主線程完成),同時提供一套類UIView或CALayer的相關屬性,儘量保證開發者的開發習慣。這個過程當中AsyncDisplayKit在主線程RunLoop中增長了一個Observer監聽即將進入休眠和退出RunLoop兩種狀態,收到回調時遍歷隊列中的待處理任務一一執行。

NSURLConnection

在前面的網絡開發的文章中已經介紹過NSURLConnection的使用,一旦啓動NSURLConnection之後就會不斷調用delegate方法接收數據,這樣一個連續的的動做正是基於RunLoop來運行。
一旦NSURLConnection設置了delegate會當即建立一個線程com.apple.NSURLConnectionLoader,同時內部啓動RunLoop並在NSDefaultMode模式下添加4個Source0。其中CFHTTPCookieStorage用於處理cookie ;CFMultiplexerSource負責各類delegate回調並在回調中喚醒delegate內部的RunLoop(一般是主線程)來執行實際操做。
早期版本的AFNetworking庫也是基於NSURLConnection實現,爲了可以在後臺接收delegate回調AFNetworking內部建立了一個空的線程並啓動了RunLoop,當須要使用這個後臺線程執行任務時AFNetworking經過performSelector: onThread: 將這個任務放到後臺線程的RunLoop中。

GCD和RunLoop的關係

在RunLoop的源代碼中能夠看到用到了GCD的相關內容,可是RunLoop自己和GCD並無直接的關係。當調用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)時libDispatch會向主線程RunLoop發送消息喚醒RunLoop,RunLoop從消息中獲取block,而且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__回調裏執行這個block。不過這個操做僅限於主線程,其餘線程dispatch操做是所有由libDispatch驅動的。

更多RunLoop使用

前面看了不少RunLoop的系統應用和一些知名第三方庫使用,那麼除了這些究竟在實際開發過程當中咱們本身能不能適當的使用RunLoop幫咱們作一些事情呢?

思考這個問題其實只要看RunLoopRef的包含關係就知道了,RunLoop包含多個Mode,而它的Mode又是能夠自定義的,這麼推斷下來其實不管是Source一、Timer仍是Observer開發者均可以利用,可是一般狀況下不會自定義Timer,更不會自定義一個完整的Mode,利用更多的實際上是Observer和Mode的切換。
例如不少人都熟悉的使用perfromSelector在默認模式下設置圖片,防止UITableView滾動卡頓([[UIImageView alloc initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。還有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空閒狀態下計算出UITableViewCell的高度並進行緩存。再有老譚的PerformanceMonitor關於iOS實時卡頓監控,一樣是利用Observer對RunLoop進行監視。

關於如何自定義一個Custom Input Source官網給出了詳細的流程。

相關文章
相關標籤/搜索