iOS刨根問底-深入理解RunLoop

iOS刨根問底-深入理解RunLoop

概述

RunLoop作為iOS中一個基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系搁料,同時也是很多常見技術(shù)的幕后功臣。盡管在平時多數(shù)開發(fā)者很少直接使用RunLoop腻要,但是理解RunLoop可以幫助開發(fā)者更好的利用多線程編程模型,同時也可以幫助開發(fā)者解答日常開發(fā)中的一些疑惑涝登。本文將從RunLoop源碼著手雄家,結(jié)合RunLoop的實際應(yīng)用來逐步解開它的神秘面紗。

開源的RunloopRef

通常所說的RunLoop指的是NSRunloop或者CFRunloopRef胀滚,CFRunloopRef是純C的函數(shù)趟济,而NSRunloop僅僅是CFRunloopRef的OC封裝,并未提供額外的其他功能咽笼,因此下面主要分析CFRunloopRef顷编,蘋果已經(jīng)開源了CoreFoundation源代碼,因此很容易找到CFRunloop源代碼剑刑。
從代碼可以看出CFRunloopRef其實就是__CFRunloop這個結(jié)構(gòu)體指針(按照OC的思路我們可以將RunLoop看成一個對象)媳纬,這個對象的運行才是我們通常意義上說的運行循環(huán),核心方法是__CFRunloopRun()施掏,為了便于閱讀就不再直接貼源代碼钮惠,放一段偽代碼方便大家閱讀:

int32_t __CFRunLoopRun()
{
    // 通知即將進入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);

    do
    {
        // 通知將要處理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

        // 處理非延遲的主線程調(diào)用
        __CFRunLoopDoBlocks();
        // 處理Source0事件
        __CFRunLoopDoSource0();

        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks();
         }
        /// 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息七芭。
        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);

        // 等待內(nèi)核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();

        // 再次確保是否有同步的方法需要調(diào)用
        __CFRunLoopDoBlocks();

    } while (!stop && !timeout);

    // 通知即將退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

源代碼盡管不算太長,但是如果不太熟悉的話面對這么一堆不知道做什么的函數(shù)調(diào)用還是會給人一種神秘感锌历。但是現(xiàn)在可以不用逐行閱讀贮庞,后面慢慢解開這層神秘面紗。現(xiàn)在只要了解上面的偽代碼知道核心的方法__CFRunLoopRun內(nèi)部其實是一個do while循環(huán)究西,這也正是Runloop運行的本質(zhì)窗慎。執(zhí)行了這個函數(shù)以后就一直處于“等待-處理”的循環(huán)之中,直到循環(huán)結(jié)束。只是不同于我們自己寫的循環(huán)它在休眠時幾乎不會占用系統(tǒng)資源遮斥,當(dāng)然這是由于系統(tǒng)內(nèi)核負(fù)責(zé)實現(xiàn)的峦失,也是Runloop精華所在。

隨著Swift的開源蘋果也維護了一個Swift版本的跨平臺CoreFoundation版本术吗,除了mac平臺它還是適配了Linux和Windows平臺尉辑。但是鑒于目前很多關(guān)于Runloop的討論都是以O(shè)C版展開的,所以這里也主要分析OC版本较屿。

下圖描述了Runloop運行流程(基本描述了上面Runloop的核心流程隧魄,當(dāng)然可以查看官方The Run Loop Sequence of Events描述):

RunLoop

整個流程并不復(fù)雜(需要注意的就是黃色區(qū)域的消息處理中并不包含source0,因為它在循環(huán)開始之初就會處理)隘蝎,整個流程其實就是一種Event Loop的實現(xiàn)购啄,其他平臺均有類似的實現(xiàn),只是這里叫做Runloop嘱么。但是既然RunLoop是一個消息循環(huán)狮含,誰來管理和運行Runloop?那么它接收什么類型的消息曼振?休眠過程是怎么樣的几迄?如何保證休眠時不占用系統(tǒng)資源?如何處理這些消息以及何時退出循環(huán)拴测?還有一系列問題需要解開乓旗。

注意的是盡管CFRunLoopPerformBlock在上圖中作為喚醒機制有所體現(xiàn),但事實上執(zhí)行CFRunLoopPerformBlock只是入隊集索,下次RunLoop運行才會執(zhí)行屿愚,而如果需要立即執(zhí)行則必須調(diào)用CFRunLoopWakeUp。

Runloop Mode

從源碼很容易看出务荆,Runloop總是運行在某種特定的CFRunLoopModeRef下(每次運行__CFRunLoopRun()函數(shù)時必須指定Mode)妆距。而通過CFRunloopRef對應(yīng)結(jié)構(gòu)體的定義可以很容易知道每種Runloop都可以包含若干個Mode,每個Mode又包含Source/Timer/Observer函匕。每次調(diào)用Runloop的主函數(shù)__CFRunLoopRun()時必須指定一種Mode娱据,這個Mode稱為** _currentMode**,當(dāng)切換Mode時必須退出當(dāng)前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 */
    };

系統(tǒng)默認(rèn)提供的Run Loop Modes有kCFRunLoopDefaultMode(NSDefaultRunLoopMode)UITrackingRunLoopMode,需要切換到對應(yīng)的Mode時只需要傳入對應(yīng)的名稱即可抒寂。前者是系統(tǒng)默認(rèn)的Runloop Mode结啼,例如進入iOS程序默認(rèn)不做任何操作就處于這種Mode中,此時滑動UIScrollView屈芜,主線程就切換Runloop到到UITrackingRunLoopMode郊愧,不再接受其他事件操作(除非你將其他Source/Timer設(shè)置到UITrackingRunLoopMode下)朴译。
但是對于開發(fā)者而言經(jīng)常用到的Mode還有一個kCFRunLoopCommonModes(NSRunLoopCommonModes),其實這個并不是某種具體的Mode,而是一種模式組合属铁,在iOS系統(tǒng)中默認(rèn)包含了
** NSDefaultRunLoopMode UITrackingRunLoopMode(注意:并不是說Runloop會運行在kCFRunLoopCommonModes這種模式下眠寿,而是相當(dāng)于分別注冊了 NSDefaultRunLoopMode UITrackingRunLoopMode。當(dāng)然你也可以通過調(diào)用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes**組合)焦蘑。

注意:我們常常還會碰到一些系統(tǒng)框架自定義Mode盯拱,例如Foundation中NSConnectionReplyMode。還有一些系統(tǒng)私有Mode例嘱,例如:GSEventReceiveRunLoopMode接受系統(tǒng)事件坟乾,UIInitializationRunLoopMode App啟動過程中初始化Mode。更多系統(tǒng)或框架Mode查看這里

CFRunLoopRef和CFRunloopMode蝶防、CFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef關(guān)系如下圖:

RunLoopMode

那么CFRunLoopSourceRef、CFRunLoopTimerRef和CFRunLoopObserverRef究竟是什么明吩?它們在Runloop運行流程中起到什么作用呢间学?

Source

首先看一下官方Runloop結(jié)構(gòu)圖(注意下圖的Input Source Port和前面流程圖中的Source0并不對應(yīng),而是對應(yīng)Source1印荔。Source1和Timer都屬于端口事件源低葫,不同的是所有的Timer都共用一個端口“Mode Timer Port”,而每個Source1都有不同的對應(yīng)端口):

RunLoopSource

再結(jié)合前面RunLoop核心運行流程可以看出Source0(負(fù)責(zé)App內(nèi)部事件仍律,由App負(fù)責(zé)管理觸發(fā)僵腺,例如UITouch事件)和Timer(又叫Timer Source巩螃,基于時間的觸發(fā)器,上層對應(yīng)NSTimer)是兩個不同的Runloop事件源(當(dāng)然Source0是Input Source中的一類,Input Source還包括Custom Input Source廊散,由其他線程手動發(fā)出),RunLoop被這些事件喚醒之后就會處理并調(diào)用事件處理方法(CFRunLoopTimerRef的回調(diào)指針和CFRunLoopSourceRef均包含對應(yīng)的回調(diào)指針)扯旷。
但是對于CFRunLoopSourceRef除了Source0之外還有另一個版本就是Source1雄妥,Source1除了包含回調(diào)指針外包含一個mach port,和Source0需要手動觸發(fā)不同炕横,Source1可以監(jiān)聽系統(tǒng)端口和其他線程相互發(fā)送消息源内,它能夠主動喚醒RunLoop(由操作系統(tǒng)內(nèi)核進行管理,例如CFMessagePort消息)份殿。官方也指出可以自定義Source膜钓,因此對于CFRunLoopSourceRef來說它更像一種協(xié)議,框架已經(jīng)默認(rèn)定義了兩種實現(xiàn)卿嘲,如果有必要開發(fā)人員也可以自定義颂斜,詳細(xì)情況可以查看官方文檔

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理解起來并不復(fù)雜腔寡,它相當(dāng)于消息循環(huán)中的一個監(jiān)聽器焚鲜,隨時通知外部當(dāng)前RunLoop的運行狀態(tài)(它包含一個函數(shù)指針callout將當(dāng)前狀態(tài)及時告訴觀察者)。具體的Observer狀態(tài)如下:

    /* 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), //從休眠狀態(tài)喚醒
        kCFRunLoopExit = (1UL << 7), //退出RunLoop
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

Call out

在開發(fā)過程中幾乎所有的操作都是通過Call out進行回調(diào)的(無論是Observer的狀態(tài)通知還是Timer、Source的處理)忿磅,而系統(tǒng)在回調(diào)時通常使用如下幾個函數(shù)進行回調(diào)(換句話說你的代碼其實最終都是通過下面幾個函數(shù)來負(fù)責(zé)調(diào)用的糯彬,即使你自己監(jiān)聽Observer也會先調(diào)用下面的函數(shù)然后間接通知你,所以在調(diào)用堆棧中經(jīng)炒兴看到這些函數(shù)):

    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函數(shù)****CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION****調(diào)用):

RunLoop_Source0_UITouch

RunLoop休眠

其實對于Event Loop而言RunLoop最核心的事情就是保證線程在沒有消息時休眠以避免占用系統(tǒng)資源,有消息時能夠及時喚醒吨些。RunLoop的這個機制完全依靠系統(tǒng)內(nèi)核來完成搓谆,具體來說是蘋果操作系統(tǒng)核心組件Darwin中的Mach來完成的(Darwin是開源的)『朗可以從下圖最底層Kernel中找到Mach:

osx_architecture-kernels_drivers

Mach是Darwin的核心泉手,可以說是內(nèi)核的核心,提供了進程間通信(IPC)偶器、處理器調(diào)度等基礎(chǔ)服務(wù)斩萌。在Mach中,進程屏轰、線程間的通信是以消息的方式來完成的颊郎,消息在兩個Port之間進行傳遞(這也正是Source1之所以稱之為Port-based Source的原因,因為它就是依靠系統(tǒng)發(fā)送消息到指定的Port來觸發(fā)的)霎苗。消息的發(fā)送和接收使用<mach/message.h>中的mach_msg()函數(shù)(事實上蘋果提供的Mach API很少姆吭,并不鼓勵我們直接調(diào)用這些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()的本質(zhì)是一個調(diào)用mach_msg_trap(),這相當(dāng)于一個系統(tǒng)調(diào)用,會觸發(fā)內(nèi)核狀態(tài)切換唁盏。當(dāng)程序靜止時内狸,RunLoop停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而這個函數(shù)內(nèi)部就是調(diào)用了mach_msg讓程序處于休眠狀態(tài)。

Runloop和線程的關(guān)系

Runloop是基于pthread進行管理的升敲,pthread是基于c的跨平臺多線程操作底層API答倡。它是mach thread的上層封裝(可以參見Kernel Programming Guide),和NSThread一一對應(yīng)(而NSThread是一套面向?qū)ο蟮腁PI驴党,所以在iOS開發(fā)中我們也幾乎不用直接使用pthread)瘪撇。

pthread

蘋果開發(fā)的接口中并沒有直接創(chuàng)建Runloop的接口,如果需要使用Runloop通常CFRunLoopGetMain()CFRunLoopGetCurrent()兩個方法來獲雀圩(通過上面的源代碼也可以看到倔既,核心邏輯在CFRunLoopGet當(dāng)中),通過代碼并不難發(fā)現(xiàn)其實只有當(dāng)我們使用線程的方法主動get Runloop時才會在第一次創(chuàng)建該線程的Runloop,同時將它保存在全局的Dictionary中(線程和Runloop二者一一對應(yīng))鹏氧,默認(rèn)情況下線程并不會創(chuàng)建Runloop(主線程的Runloop比較特殊渤涌,任何線程創(chuàng)建之前都會保證主線程已經(jīng)存在Runloop),同時在線程結(jié)束的時候也會銷毀對應(yīng)的Runloop把还。

iOS開發(fā)過程中對于開發(fā)者而言更多的使用的是NSRunloop,它默認(rèn)提供了三個常用的run方法:

     - (void)run; 
     - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
     - (void)runUntilDate:(NSDate *)limitDate;
  • run方法對應(yīng)上面CFRunloopRef中的CFRunLoopRun并不會退出实蓬,除非調(diào)用CFRunLoopStop();通常如果想要永遠(yuǎn)不會退出RunLoop才會使用此方法茸俭,否則可以使用runUntilDate。
  • runMode:beforeDate:則對應(yīng)CFRunLoopRunInMode(mode,limiteDate,true)方法,只執(zhí)行一次安皱,執(zhí)行完就退出调鬓;通常用于手動控制RunLoop(例如在while循環(huán)中)。
  • runUntilDate:方法其實是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false)酌伊,執(zhí)行完并不會退出腾窝,繼續(xù)下一次RunLoop直到timeout。

RunLoop應(yīng)用

NSTimer

前面一直提到Timer Source作為事件源居砖,事實上它的上層對應(yīng)就是NSTimer(其實就是CFRunloopTimerRef)這個開發(fā)者經(jīng)常用到的定時器(底層基于使用mk_timer實現(xiàn))虹脯,甚至很多開發(fā)者接觸RunLoop還是從NSTimer開始的。其實NSTimer定時器的觸發(fā)正是基于RunLoop運行的奏候,所以使用NSTimer之前必須注冊到RunLoop循集,但是RunLoop為了節(jié)省資源并不會在非常準(zhǔn)確的時間點調(diào)用定時器,如果一個任務(wù)執(zhí)行時間較長蔗草,那么當(dāng)錯過一個時間點后只能等到下一個時間點執(zhí)行暇榴,并不會延后執(zhí)行(NSTimer提供了一個tolerance屬性用于設(shè)置寬容度,如果確實想要使用NSTimer并且希望盡可能的準(zhǔn)確蕉世,則可以設(shè)置此屬性)。

NSTimer的創(chuàng)建通常有兩種方式婆硬,盡管都是類方法狠轻,一種是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

二者最大的區(qū)別就是后者除了創(chuàng)建一個定時器外會自動以NSDefaultRunLoopModeMode添加到當(dāng)前線程RunLoop中向楼,不添加到RunLoop中的NSTimer是無法正常工作的。例如下面的代碼中如果timer2不加入到RunLoop中是無法正常工作的谐区。同時注意如果滾動UIScrollView(UITableView湖蜕、UICollectionview是類似的)二者是無法正常工作的,但是如果將NSDefaultRunLoopMode改為NSRunLoopCommonModes則可以正常工作宋列,這也解釋了前面介紹的Mode內(nèi)容昭抒。

    #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創(chuàng)建后會自動以NSDefaultRunLoopMode默認(rèn)模式添加到當(dāng)前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并沒有強引用炼杖,對于普通的對象而言灭返,執(zhí)行完viewDidLoad方法之后(準(zhǔn)確的說應(yīng)該是執(zhí)行完viewDidLoad方法后的的一個RunLoop運行結(jié)束)二者應(yīng)該會被釋放,但事實上二者并沒有被釋放坤邪。原因是:為了確保定時器正常運轉(zhuǎn)熙含,當(dāng)加入到RunLoop以后系統(tǒng)會對NSTimer執(zhí)行一次retain操作(特別注意:timer2創(chuàng)建時并沒直接賦值給timer2,原因是timer2是weak屬性艇纺,如果直接賦值給timer2會被立即釋放怎静,因為timerWithXXX方法創(chuàng)建的NSTimer默認(rèn)并沒有加入RunLoop邮弹,只有后面加入RunLoop以后才可以將引用指向timer2)。
但是即使使用了弱引用蚓聘,上面的代碼中ViewController1也無法正常釋放腌乡,原因是在創(chuàng)建NSTimer2時指定了target為self,這樣一來造成了timer1和timer2對ViewController1有一個強引用或粮。解決這個問題的方法通常有兩種:一種是將target分離出來獨立成一個對象(在這個對象中創(chuàng)建NSTimer并將對象本身作為NSTimer的target)导饲,控制器通過這個對象間接使用NSTimer;另一種方式的思路仍然是轉(zhuǎn)移target氯材,只是可以直接增加NSTimer擴展(分類)渣锦,讓NSTimer自身做為target,同時可以將操作selector封裝到block中氢哮。后者相對優(yōu)雅袋毙,也是目前使用較多的方案(目前有大量類似的封裝,例如:NSTimer+Block)冗尤。顯然Apple也認(rèn)識到了這個問題听盖,如果你可以確保代碼只在iOS 10下運行就可以使用iOS 10新增的系統(tǒng)級block方案(上面的代碼中已經(jīng)貼出這種方法)。
當(dāng)然使用上面第二種方法可以解決控制器無法釋放的問題裂七,但是會發(fā)現(xiàn)即使控制器被釋放了兩個定時器仍然正常運行皆看,要解決這個問題就需要調(diào)用NSTimer的invalidate方法(注意:無論是重復(fù)執(zhí)行的定時器還是一次性的定時器只要調(diào)用invalidate方法則會變得無效,只是一次性的定時器執(zhí)行完操作后會自動調(diào)用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

其實和定時器相關(guān)的另一個問題大家也經(jīng)常碰到腰吟,那就是NSTimer不是一種實時機制,官方文檔明確說明在一個循環(huán)中如果RunLoop沒有被識別(這個時間大概在50-100ms)或者說當(dāng)前RunLoop在執(zhí)行一個長的call out(例如執(zhí)行某個循環(huán)操作)則NSTimer可能就會存在誤差徙瓶,RunLoop在下一次循環(huán)中繼續(xù)檢查并根據(jù)情況確定是否執(zhí)行(NSTimer的執(zhí)行時間總是固定在一定的時間間隔毛雇,例如1:00:00、1:00:01侦镇、1:00:02灵疮、1:00:05則跳過了第4、5次運行循環(huán))壳繁。
要演示這個問題請看下面的例子(注意:有些示例中可能會讓一個線程中啟動一個定時器震捣,再在主線程啟動一個耗時任務(wù)來演示這個問,如果實際測試可能效果不會太明顯闹炉,因為現(xiàn)在的iPhone都是多核運算的伍派,這樣一來這個問題會變得相對復(fù)雜,因此下面的例子選擇在同一個RunLoop中即加入定時器和執(zhí)行耗時任務(wù))

    #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的引用剩胁,也就無法控制線程的狀態(tài)
        //[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 {
        // 使用下面的方式創(chuàng)建定時器雖然會自動加入到當(dāng)前線程的RunLoop中诉植,但是除了主線程外其他線程的RunLoop默認(rèn)是不會運行的,必須手動調(diào)用
        __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]);

        // 區(qū)分直接調(diào)用和「performSelector:withObject:afterDelay:」區(qū)別,下面的直接調(diào)用無論是否運行RunLoop一樣可以執(zhí)行昵观,但是后者則不行晾腔。
        //[self caculate];
        [self performSelector:@selector(caculate) withObject:nil afterDelay:2.0];

        // 取消當(dāng)前RunLoop中注冊測selector(注意:只是當(dāng)前RunLoop舌稀,所以也只能在當(dāng)前RunLoop中取消)
        // [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(caculate) object:nil];
        NSLog(@"runloop after performSelector:%@",[NSRunLoop currentRunLoop]);

        // 非主線程RunLoop必須手動調(diào)用
        [[NSRunLoop currentRunLoop] run];

        NSLog(@"注意:如果RunLoop不退出(運行中),這里的代碼并不會執(zhí)行灼擂,RunLoop本身就是一個循環(huán).");

    }

    - (void)caculate {
        for (int i = 0;i < 9999;++i) {
            NSLog(@"%i,%@",i,[NSThread currentThread]);
            if ([NSThread currentThread].isCancelled) {
                return;
            }
        }
    }

    @end

如果運行并且不退出上面的程序會發(fā)現(xiàn)壁查,前兩秒NSTimer可以正常執(zhí)行,但是兩秒后由于同一個RunLoop中循環(huán)操作的執(zhí)行造成定時器跳過了中間執(zhí)行的機會一直到caculator循環(huán)完畢剔应,這也正說明了NSTimer不是實時系統(tǒng)機制的原因睡腿。

但是以上程序還有幾點需要說明一下:

  1. NSTimer會對Target進行強引用直到任務(wù)結(jié)束或exit之后才會釋放。如果上面的程序沒有進行線程cancel而終止任務(wù)則及時關(guān)閉控制器也無法正確釋放峻贮。
  2. 非主線程的RunLoop并不會自動運行(同時注意默認(rèn)情況下非主線程的RunLoop并不會自動創(chuàng)建席怪,直到第一次使用),RunLoop運行必須要在加入NSTimer或Source0纤控、Sourc1挂捻、Observer輸入后運行否則會直接退出。例如上面代碼如果run放到NSTimer創(chuàng)建之前則既不會執(zhí)行定時任務(wù)也不會執(zhí)行循環(huán)運算船万。
  3. performSelector:withObject:afterDelay:執(zhí)行的本質(zhì)還是通過創(chuàng)建一個NSTimer然后加入到當(dāng)前線程RunLoop(通而過前后兩次打印RunLoop信息可以看到此方法執(zhí)行之后RunLoop的timer會增加1個刻撒。類似的還有performSelector:onThread:withObject:afterDelay:,只是它會在另一個線程的RunLoop中創(chuàng)建一個Timer)耿导,所以此方法事實上在任務(wù)執(zhí)行完之前會對觸發(fā)對象形成引用声怔,任務(wù)執(zhí)行完進行釋放(例如上面會對ViewController形成引用,注意:performSelector: withObject:等方法則等同于直接調(diào)用舱呻,原理與此不同)捧搞。
  4. 同時上面的代碼也充分說明了RunLoop是一個循環(huán)事實,run方法之后的代碼不會立即執(zhí)行狮荔,直到RunLoop退出。
  5. 上面程序的運行過程中如果突然dismiss介粘,則程序的實際執(zhí)行過程要分為兩種情況考慮:如果循環(huán)任務(wù)caculate還沒有開始則會在timer1中停止timer1運行(停止了線程中第一個任務(wù))殖氏,然后等待caculate執(zhí)行并break(停止線程中第二個任務(wù))后線程任務(wù)執(zhí)行結(jié)束釋放對控制器的引用;如果循環(huán)任務(wù)caculate執(zhí)行過程中dismiss則caculate任務(wù)執(zhí)行結(jié)束姻采,等待timer1下個周期運行(因為當(dāng)前線程的RunLoop并沒有退出雅采,timer1引用計數(shù)器并不為0)時檢測到線程取消狀態(tài)則執(zhí)行invalidate方法(第二個任務(wù)也結(jié)束了),此時線程釋放對于控制器的引用慨亲。

CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時器婚瓜,它也需要加入到RunLoop才能執(zhí)行。與NSTimer類似刑棵,CADisplayLink同樣是基于CFRunloopTimerRef實現(xiàn)巴刻,底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度)蛉签,不過和NStimer類似的是如果遇到大任務(wù)它仍然存在丟幀現(xiàn)象胡陪。通常情況下CADisaplayLink用于構(gòu)建幀動畫沥寥,看起來相對更加流暢,而NSTimer則有更廣泛的用處柠座。

AutoreleasePool

AutoreleasePool是另一個與RunLoop相關(guān)討論較多的話題邑雅。其實從RunLoop源代碼分析,AutoreleasePool與RunLoop并沒有直接的關(guān)系妈经,之所以將兩個話題放到一起討論最主要的原因是因為在iOS應(yīng)用啟動后會注冊兩個Observer管理和維護AutoreleasePool淮野。不妨在應(yīng)用程序剛剛啟動時打印currentRunLoop可以看到系統(tǒng)默認(rèn)注冊了很多個Observer,其中有兩個Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**吹泡,這兩個是和自動釋放池相關(guān)的兩個監(jiān)聽骤星。

    <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會監(jiān)聽RunLoop的進入,它會回調(diào)objc_autoreleasePoolPush()向當(dāng)前的AutoreleasePoolPage增加一個哨兵對象標(biāo)志創(chuàng)建自動釋放池荞胡。這個Observer的order是-2147483647優(yōu)先級最高妈踊,確保發(fā)生在所有回調(diào)操作之前。
第二個Observer會監(jiān)聽RunLoop的進入休眠和即將退出RunLoop兩種狀態(tài)泪漂,在即將進入休眠時會調(diào)用**objc_autoreleasePoolPop() **和 **objc_autoreleasePoolPush() 根據(jù)情況從最新加入的對象一直往前清理直到遇到哨兵對象廊营。而在即將退出RunLoop時會調(diào)用objc_autoreleasePoolPop() **釋放自動自動釋放池內(nèi)對象。這個Observer的order是2147483647萝勤,優(yōu)先級最低露筒,確保發(fā)生在所有回調(diào)操作之后。
主線程的其他操作通常均在這個AutoreleasePool之內(nèi)(main函數(shù)中)敌卓,以盡可能減少內(nèi)存維護操作(當(dāng)然你如果需要顯式釋放【例如循環(huán)】時可以自己創(chuàng)建AutoreleasePool否則一般不需要自己創(chuàng)建)慎式。
其實在應(yīng)用程序啟動后系統(tǒng)還注冊了其他Observer(例如即將進入休眠時執(zhí)行注冊回調(diào)_UIGestureRecognizerUpdateObserver用于手勢處理、回調(diào)為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用于界面實時繪制更新)和多個Source1(例如context為CFMachPort的Source1用于接收硬件事件響應(yīng)進而分發(fā)到應(yīng)用程序一直到UIEvent)趟径,這里不再一一詳述瘪吏。

UI更新

如果打印App啟動之后的主線程RunLoop可以發(fā)現(xiàn)另外一個callout為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer,這個監(jiān)聽專門負(fù)責(zé)UI變化后的更新蜗巧,比如修改了frame掌眠、調(diào)整了UI層級(UIView/CALayer)或者手動設(shè)置了setNeedsDisplay/setNeedsLayout之后就會將這些操作提交到全局容器。而這個Observer監(jiān)聽了主線程RunLoop的即將進入休眠和退出狀態(tài)幕屹,一旦進入這兩種狀態(tài)則會遍歷所有的UI更新并提交進行實際繪制更新蓝丙。
通常情況下這種方式是完美的,因為除了系統(tǒng)的更新望拖,還可以利用setNeedsDisplay等方法手動觸發(fā)下一次RunLoop運行的更新渺尘。但是如果當(dāng)前正在執(zhí)行大量的邏輯運算可能UI的更新就會比較卡,因此facebook推出了AsyncDisplayKit來解決這個問題说敏。AsyncDisplayKit其實是將UI排版和繪制運算盡可能放到后臺鸥跟,將UI的最終更新操作放到主線程(這一步也必須在主線程完成),同時提供一套類UIView或CALayer的相關(guān)屬性盔沫,盡可能保證開發(fā)者的開發(fā)習(xí)慣锌雀。這個過程中AsyncDisplayKit在主線程RunLoop中增加了一個Observer監(jiān)聽即將進入休眠和退出RunLoop兩種狀態(tài),收到回調(diào)時遍歷隊列中的待處理任務(wù)一一執(zhí)行蚂夕。

NSURLConnection

在前面的網(wǎng)絡(luò)開發(fā)的文章中已經(jīng)介紹過NSURLConnection的使用,一旦啟動NSURLConnection以后就會不斷調(diào)用delegate方法接收數(shù)據(jù)腋逆,這樣一個連續(xù)的的動作正是基于RunLoop來運行婿牍。
一旦NSURLConnection設(shè)置了delegate會立即創(chuàng)建一個線程com.apple.NSURLConnectionLoader,同時內(nèi)部啟動RunLoop并在NSDefaultMode模式下添加4個Source0惩歉。其中CFHTTPCookieStorage用于處理cookie ;CFMultiplexerSource負(fù)責(zé)各種delegate回調(diào)并在回調(diào)中喚醒delegate內(nèi)部的RunLoop(通常是主線程)來執(zhí)行實際操作等脂。
早期版本的AFNetworking庫也是基于NSURLConnection實現(xiàn),為了能夠在后臺接收delegate回調(diào)AFNetworking內(nèi)部創(chuàng)建了一個空的線程并啟動了RunLoop撑蚌,當(dāng)需要使用這個后臺線程執(zhí)行任務(wù)時AFNetworking通過**performSelector: onThread: **將這個任務(wù)放到后臺線程的RunLoop中上遥。

GCD和RunLoop的關(guān)系

在RunLoop的源代碼中可以看到用到了GCD的相關(guān)內(nèi)容,但是RunLoop本身和GCD并沒有直接的關(guān)系争涌。當(dāng)調(diào)用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)時libDispatch會向主線程RunLoop發(fā)送消息喚醒RunLoop粉楚,RunLoop從消息中獲取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回調(diào)里執(zhí)行這個block亮垫。不過這個操作僅限于主線程模软,其他線程dispatch操作是全部由libDispatch驅(qū)動的。

更多RunLoop使用

前面看了很多RunLoop的系統(tǒng)應(yīng)用和一些知名第三方庫使用饮潦,那么除了這些究竟在實際開發(fā)過程中我們自己能不能適當(dāng)?shù)氖褂肦unLoop幫我們做一些事情呢燃异?

思考這個問題其實只要看RunLoopRef的包含關(guān)系就知道了,RunLoop包含多個Mode继蜡,而它的Mode又是可以自定義的回俐,這么推斷下來其實無論是Source1、Timer還是Observer開發(fā)者都可以利用稀并,但是通常情況下不會自定義Timer仅颇,更不會自定義一個完整的Mode,利用更多的其實是Observer和Mode的切換碘举。
例如很多人都熟悉的使用perfromSelector在默認(rèn)模式下設(shè)置圖片忘瓦,防止UITableView滾動卡頓([[UIImageView allocinitWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。還有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空閑狀態(tài)下計算出UITableViewCell的高度并進行緩存殴俱。再有老譚的PerformanceMonitor關(guān)于iOS實時卡頓監(jiān)控,同樣是利用Observer對RunLoop進行監(jiān)視枚抵。

關(guān)于如何自定義一個Custom Input Source官網(wǎng)給出了詳細(xì)的流程线欲。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市汽摹,隨后出現(xiàn)的幾起案子李丰,更是在濱河造成了極大的恐慌,老刑警劉巖逼泣,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件趴泌,死亡現(xiàn)場離奇詭異舟舒,居然都是意外死亡,警方通過查閱死者的電腦和手機嗜憔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門秃励,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吉捶,你說我怎么就攤上這事夺鲜。” “怎么了呐舔?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵币励,是天一觀的道長。 經(jīng)常有香客問我珊拼,道長食呻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任澎现,我火速辦了婚禮仅胞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘昔头。我一直安慰自己饼问,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布揭斧。 她就那樣靜靜地躺著莱革,像睡著了一般。 火紅的嫁衣襯著肌膚如雪讹开。 梳的紋絲不亂的頭發(fā)上盅视,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音旦万,去河邊找鬼闹击。 笑死,一個胖子當(dāng)著我的面吹牛成艘,可吹牛的內(nèi)容都是我干的赏半。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼淆两,長吁一口氣:“原來是場噩夢啊……” “哼断箫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起秋冰,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤仲义,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體埃撵,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡赵颅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了暂刘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饺谬。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鸳惯,靈堂內(nèi)的尸體忽然破棺而出商蕴,到底是詐尸還是另有隱情,我是刑警寧澤芝发,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布绪商,位于F島的核電站,受9級特大地震影響辅鲸,放射性物質(zhì)發(fā)生泄漏格郁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一独悴、第九天 我趴在偏房一處隱蔽的房頂上張望例书。 院中可真熱鬧,春花似錦刻炒、人聲如沸决采。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽树瞭。三九已至,卻和暖如春爱谁,著一層夾襖步出監(jiān)牢的瞬間晒喷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工访敌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凉敲,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓寺旺,卻偏偏與公主長得像爷抓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子阻塑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容

  • RunLoop 的概念 一般來講蓝撇,一個線程一次只能執(zhí)行一個任務(wù),執(zhí)行完成后線程就會退出叮姑。如果我們需要一個機制唉地,讓線...
    Mirsiter_魏閱讀 618評論 0 2
  • 轉(zhuǎn)自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飄金閱讀 983評論 0 4
  • 轉(zhuǎn)載:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling閱讀 1,438評論 0 13
  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技術(shù) RunLoop 是 iOS 和 ...
    橙娃閱讀 854評論 1 2
  • 澎湃新聞(www.thepaper.cn)8月7日從浙江青田縣人民檢察院獲悉耘沼,考慮到嫌疑人交通肇事情節(jié)輕微,受害人...
    百事可愛喲閱讀 110評論 0 0