RunLoop

引導


Apple document
Apple documentphp

上圖內容釋義


  • Run loops 是線程相關底層基礎的一部分。它的本質和字面意思同樣運行着的循環(事件處理的循環),做用:接受循環事件和安排線程的工做。目的:讓線程在有任務的時候忙於工做,而沒任務的時候處於休眠狀態。html

  • Run loop 的管理並不是徹底自動。你仍然須要設置線程代碼在合適的時候啓動 run loop 來幫助你處理輸入事件。iOS 中 Cocoa 和 CoreFoundation 框架中各有完整的一套關於 runloop對象的操做api,在主線程中 run loop 是自動建立並運行(在子線程開啓RunLoop 須要手動建立且手動開啓)。git

譯文&源碼


Runloop 儘管在平時多數開發者不多直接使用,可是理解 RunLoop 能夠幫助開發者更好的利用多線程編程模型,同時也能夠幫助開發者解答面試套路的一些疑惑,對於 iOS 編程 熟知它是必不可少的,下面是我對 Runloop 的整理,將以一勞永逸的心態,漸進式學習的目地,而且帶有幾個實戰開發場景。 --> 大神可選擇性路過「思想」。github

RunLoop 該模塊學習將續更 ~面試

在「時間 & 知識 」有限內,總結的文章不免有「未全、不足 」的地方,還望各位好友指出,可留言指正或是補充,以提升文章質量@白開水ln原著;編程

目錄api

  1. Runloop 概念
  2. Runloop 做用
  3. Runloop 開啓&退出
  4. Runloop 和線程關係
    1.如何建立子線程對應的 Runloop ?
  5. Runloop 獲取
  6. Runloop 源碼
  7. Runloop 相關5個類
  8. Runloop 相關類(Mode)
  9. Runloop 相關類(Source)
  10. Runloop 相關類(Timer)
  11. Runloop 相關類(Observer)
  12. Runloop 相關5個類代碼示例
  13. Runloop 應用\場景
    1.Runloop 經典應用:常駐線程
    2.AutoreleasePool 自動釋放池
    3.UI更新
    4.UIImageView 延遲加載圖片
    5.UITableView 與 NSTimer 衝突
  14. Runtime & Runloop 面試最常問到的題整理【建議看】
  15. Runloop 模塊博文推薦(❤️數量較多)
  16. Demo 重要的部分代碼中都有相應的註解和文字打印,運行程序能夠很直觀的表現。
  17. SourceCode 、 ToolsClassWechatPublic-Codeidea
  18. Runtime 模塊詳解「面試、工做」看我就 🐒 了 _. 

Runloop 概念


  • 【Runloop 釋義】:"運行循環"、"跑圈"
  • 【註解1】:iOS 中一般所說的 RunLoop 指的是 NSRunloop (Foundation框架) 或者 CFRunloopRef (CoreFoundation 框架) ,CFRunloopRef 是純C的函數,而 NSRunloop 僅僅是 CFRunloopRef 的一層OC封裝,並未提供額外的其餘功能,所以要了解 RunLoop 內部結構,須要多研究 CFRunLoopRef API(Core Foundation \ 更底層)。
  • 【註解2】:CFRunloopRef 其實就是 __CFRunloop 這個結構體指針(按照OC的思路咱們能夠將RunLoop當作一個對象),這個對象的運行纔是咱們一般意義上說的運行循環,核心方法是 __CFRunloopRun() 查看下(附:源碼)

Runloop 做用


  • 一、保持程序的持續運行(如:程序一啓動就會開啓一個主線程(中的 runloop 是自動建立並運行),runloop 保證主線程不會被銷燬,也就保證了程序的持續運行)。
  • 二、處理App中的各類事件(如:touches 觸摸事件、NSTimer 定時器事件、Selector事件(選擇器 performSelector))。
  • 三、節省CPU資源,提升程序性能(有事情就作事情,沒事情就休息 (其資源釋放))。
  • 四、負責渲染屏幕上的全部UI。

附:CFRunLoop.c 源碼緩存

#【用DefaultMode啓動,具體實現查看 CFRunLoopRunSpecific Line2704】
#【RunLoop的主函數,是一個死循環 dowhile】
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        /*
         參數一:CFRunLoopRunSpecific   具體處理runloop的運行狀況
         參數二:CFRunLoopGetCurrent()  當前runloop對象
         參數三:kCFRunLoopDefaultMode  runloop的運行模式的名稱
         參數四:1.0e10                 runloop默認的運行時間,即超時爲10的九次方
         參數五:returnAfterSourceHandled 回調處理
         */
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
        
        //【判斷】:若是runloop沒有中止 且 沒有結束則繼續循環,相反側退出。
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

#【直觀表現】
RunLoop 其實內部就是do-while循環,在這個循環內部不斷地處理各類任務(`好比Source、Timer、Observer`),
經過判斷result的值實現的。因此 能夠當作是一個死循環。
若是沒有RunLoop,UIApplicationMain 函數執行完畢以後將直接返回,就是說程序一啓動而後就結束;

Runloop 開啓&退出


咱們來驗證 Runloop 是在那開啓的?答案:UIApplicationMain 中開啓;多線程

#【驗證 Runloop 的開啓】。

# int 類型返回值
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"開始");
        int number = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        NSLog(@"結束");
        return number;
    }
}

#【驗證結果】:只會打印開始,並不會打印結束。

----
#【Runloop 的退出條件】。
App退出;線程關閉;設置最大時間到期;

【註解】:說明在UIApplicationMain函數內部開啓了一個和主線程相關的RunLoop (保證主線程不會被銷燬),致使 UIApplicationMain 不會返回,一直在運行中,也就保證了程序的持續運行。app

Runloop和線程關係


【附】:CFRunLoop.c 源碼

# NOTE: 得到runloop實現 (建立runloop)

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {// ✔️【主線程相關聯的RunLoop建立】,若是爲空,默認是主線程
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) { // 若是 RunLoop 不存在
        __CFUnlock(&loopsLock);
        // 建立字典
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        // 建立主線程對應的runloop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        // 使用字典保存(KEY:線程 -- Value:線程對應的runloop), 以保證一一對應關係。
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // ✔️【建立與子線程相關聯的RunLoop】,從字典中獲取 子線程的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        // 若是子線程的runloop不存在,那麼就爲該線程建立一個對應的runloop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        // 把當前子線程和對應的runloop保存到字典中
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
        CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}
  • 【由上源碼可得】:RunLoop 和 線程關係
    • 1.每條線程都有惟一的一個與之對應的RunLoop對象。
    • 2.主線程的RunLoop已經自動建立,子線程的RunLoop須要主動建立。
    • 3.RunLoop在第一次獲取時建立,在線程結束時銷燬。
  • 【註解】:Runloop 對象是利用字典來進行存儲,並且 Key:線程 -- Value:線程對應的 runloop。
  • iOS開發過程當中對於開發者而言更多的使用的是NSRunloop,它默認提供了三個經常使用的run方法 

如何建立子線程對應的 Runloop ?

  • 【解決】:開一個子線程建立 runloop ,不是經過[alloc init]方法建立,而是直接經過調用currentRunLoop 方法來建立。
  • 【緣由】:currentRunLoop 自己是懶加載的,當第一次調用currentRunLoop 方法得到該子線程對應的 Runloop 的時候,它會先去判斷(去字典中查找)這個線程的Runloop 是否存在,若是不存在就會本身建立而且返回,若是存在直接返回。

Runloop 獲取


// Foundation框架
    NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop]; // 得到主線程對應的 runloop對象
    NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop]; // 得到當前線程對應的runloop對象
    
    // Core Foundation框架
    CFRunLoopRef maiRunloop = CFRunLoopGetMain(); // 得到主線程對應的 runloop對象
    CFRunLoopRef maiRunloop = CFRunLoopGetCurrent(); // 得到當前線程對應的runloop對象

    // NSRunLoop <--> CFRunLoopRef 相互轉化
    NSLog(@"NSRunLoop <--> CFRunloop == %p--%p",CFRunLoopGetMain() , [NSRunLoop mainRunLoop].getCFRunLoop);

#【打印結果】:內存地址相同
0000-00-13 00:30:16.527 MultiThreading[57703:1217113] NSRunLoop <--> CFRunloop == 0x60000016a680--0x60000016a680

Runloop 源碼


Runloop 相關內部實現源碼,代碼量甚多,其核心方法是 【__CFRunLoopRun】 ,爲了避免影響文章的可讀性,這裏就再也不直接貼源代碼,放一段僞代碼方便你們閱讀【轉】:

int32_t __CFRunLoopRun()
{
    // 通知即將進入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    
    do
    {
        // 通知將要處理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        // 執行被加入的Block(處理非延遲的主線程調用)
        __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) 後,就會一直處於這個函數內部 "接受消息->等待->處理" 的循環中,直到這個循環結束函數才返回,固然Runloop精華在於在休眠時幾乎不會佔用系統資源(系統內核負責)。

面對上面的一段僞代碼不知道作什麼的函數調用 , 這裏你若是想結合上段的僞代碼看源碼的話,能夠 CFRunLoop.c 源碼 (c) 2015 上面的每一步都有相應的註解,打開對照查看能夠很直觀的表現。

【NOTE】: 固然我 也整理了一張圖,描述了 Runloop 內部實現流程(版1 & 版2 基本描述了 Runloop 的核心流程,固然可仍是對照查看官方文檔或源碼)。

runloop內部實現流程.png
runloop內部實現流程.png

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

Runloop 相關類


Core Foundation 中關於 RunLoop 的5個類

  • 一、CFRunloopRef【RunLoop自己】
  • 二、CFRunloopModeRef【Runloop的運行模式】
  • 三、CFRunloopSourceRef【Runloop要處理的事件源】
  • 四、CFRunloopTimerRef【Timer事件】
  • 五、CFRunloopObserverRef【Runloop的觀察者(監聽者)】

CFRunLoop 的5個相關類關係圖解:

相關類關係圖解.gif
相關類關係圖解.gif

【圖解直觀得知】:

  • 一條線程 對應一個 Runloop,Runloop 老是運行在某種特定的CFRunLoopModeRef(運行模式)下。
  • 每一個 Runloop 均可以包含若干個 Mode ,每一個 Mode 又包含Source源 / Timer事件 / Observer觀察者。
  • 在 Runloop 中有多個運行模式,每次調用 RunLoop 的主函數【__CFRunloopRun()】時,只能指定其中一個 Mode(稱 CurrentMode)運行, 若是須要切換 Mode,只能是退出 CurrentMode 切換到指定的 Mode 進入,目的以保證不一樣 Mode 下的 Source / Timer / Observer 互不影響。
  • Runloop 有效,mode 裏面 至少 要有一個timer(定時器事件) 或者是source(源);
  • 附:源碼
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; // mode名
        Boolean _stopped;
        char _padding[3];
        CFMutableSetRef _sources0; // source0 源
        CFMutableSetRef _sources1; // source1 源
        CFMutableArrayRef _observers; // observer 源
        CFMutableArrayRef _timers; // timer 源
        CFMutableDictionaryRef _portToV1SourceMap;// mach port 到 mode的映射,爲了在runloop主邏輯中過濾runloop本身的port消息。
        __CFPortSet _portSet;// 記錄了全部當前mode中須要監聽的port,做爲調用監聽消息函數的參數。
        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;// 使用 mk timer, 用到的mach port,和source1相似,都依賴於mach port
        Boolean _mkTimerArmed;
    #endif
    #if DEPLOYMENT_TARGET_WINDOWS
        DWORD _msgQMask;
        void (*_msgPump)(void);
    #endif
        uint64_t _timerSoftDeadline; /* TSR timer觸發的理想時間*/
        uint64_t _timerHardDeadline; /* TSR timer觸發的實際時間,理想時間加上tolerance(誤差*/
    };

Runloop 相關類(Mode)


CFRunLoopModeRef 表明 RunLoop 的運行模式;系統默認提供了5個 Mode 。

  • 1.【kCFRunLoopDefaultMode (NSDefaultRunLoopMode)】: App的默認Mode,一般主線程是在這個Mode下運行。

  • 2.【UITrackingRunLoopMode】: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響。

  • 3.【UIInitializationRunLoopMode】: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用。

  • 4.【GSEventReceiveRunLoopMode】: 接受系統事件的內部 Mode,一般用不到。

  • 5.【kCFRunLoopCommonModes (NSRunLoopCommonModes)】: 這個並非某種具體的 Mode, 能夠說是一個佔位用的Mode(一種模式組合)。

  • CFRunLoop 對外暴露的管理 Mode 接口:

# CFRunLoop
CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);
CF_EXPORT CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

# NSRunLoop.h
FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode;// (默認):同一時間只能執行一個任務
FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes NS_AVAILABLE(10_5, 2_0); // (公用):能夠分配必定的時間處理定時器

【注】:對照上面貼的源碼,關於 CommonModes ;

  • 【關於 _commonModes】:一個 mode 能夠標記爲 common 屬性(用於 CFRunLoopAddCommonMode函數),而後它就會保存在_commonModes。主線程 CommonModes默認已有兩個modek CFRunLoopDefaultMode 和 UITrackingRunLoopMode,固然你也能夠經過調用 CFRunLoopAddCommonMode() 方法將自定義mode 放到 kCFRunLoopCommonModes組合)。

  • 【關於 _commonModeItems】:_commonModeItems 裏面存放的source, observer, timer等,在每次 runLoop 運行的時候都會被同步到具備 Common 標記的 Modes 裏。如:[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];就是把timer放到commonModeItems 裏。

  • 更多系統或框架 Mode查看這裏

Runloop 相關類(Source)


CFRunloopSourceRef 事件源 \ 輸入源,有兩種分類模式

  • 【官方版】:


    官方Runloop圖解.jpg
    官方Runloop圖解.jpg
  • 【Port-Based Sources】: 基於端口的源 (對應的是source1):與內核端口相關,只須要簡單的建立端口對象,並使用 NSPort 的方法將端口對象加入到runloop,端口對象會處理建立以及配置輸入源;。

  • 【Custom Input Sources】:自定義源:使用CFRunLoopSourceRef 類型相關的函數 (線程) 來建立自定義輸入源。

  • 【Perform Selector Sources】:performSelector:OnThread:delay:

  • 【源碼版】:按照函數調用棧的分類 source0 和 source1
    • Source0:非基於端口Port的事件;(用於用戶主動觸發的事件,如:點擊按鈕 或點擊屏幕)。
    • Source1:基於端口Port的事件;(經過內核和其餘線程相互發送消息,與內核相關)。
    • 補充:Source1 事件在處理時會分發一些操做給 Source0 去處理。


Runloop 相關類(Timer)


  • CFRunLoopTimerRef是基於時間的觸發器。

  • 基本上說的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影響。

  • 而與NSTimer相比,GCD定時器不會受Runloop影響。

Runloop 相關類(Observer)


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

/* jianshu:白開水ln Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),           //即將進入Runloop
    kCFRunLoopBeforeTimers = (1UL << 1),    //即將處理NSTimer
    kCFRunLoopBeforeSources = (1UL << 2),   //即將處理Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),   //即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),    //從休眠裝填中喚醒
    kCFRunLoopExit = (1UL << 7),            //退出runloop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   //全部狀態改變
};

Runloop 休眠


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

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 相關5個類代碼示例


Mode-Runloop的運行模式、Source-Runloop要處理的事件源、Timer-定時器事件

[NSTimer timerWithTimeInterval:   ].gif
[NSTimer timerWithTimeInterval: ].gif  [NSTimer scheduledTimerWithTimeInterval:   ].gif
[NSTimer scheduledTimerWithTimeInterval: ].gif GCD定時器不會受Runloop影響.png
GCD定時器不會受Runloop影響.png

Runloop 應用\場景


一、NSTimer
二、ImageView顯示:控制方法在特定的模式下可用
三、PerformSelector
四、常駐線程:在子線程中開啓一個runloop
五、AutoreleasePool 自動釋放池
六、UI更新

Runloop 經典應用:常駐線程

【註解】:常駐線程:線程建立出來就處於等待狀態(有或無任務),想用它的時候就用它執行任務,不想用的時候就處於等待狀態。

【場景】:如:1.聊天發送語音消息,可能會專門開一個子線程來處理;2.在後臺記錄用戶的停留時間或某個按鈕點擊次數,這些用主線程作可能不太方便,可能會開啓一個子線程後臺默默收集;

【需求】:讓線程持續存在,能夠切換執行其餘任務。

【解決】:開啓 Runloop循環。

Demo & 效果圖:

常駐線程:讓線程持續存在,能夠切換執行其餘任務.gif
常駐線程:讓線程持續存在,能夠切換執行其餘任務.gif

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 不然通常不須要本身建立)。

AutoreleasePool 自動釋放池.png
AutoreleasePool 自動釋放池.png

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 兩種狀態,收到回調時遍歷隊列中的待處理任務一一執行。

UIImageView 延遲加載圖片

Demo & 效果圖

.gif
.gif

UITableView 與 NSTimer 衝突

【描述】:因爲 UItabelView 在滑動的時候,會從當前的 RunLoop 默認的模式 kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 自動切換到 UITrackingRunLoopMode界面追蹤模式。這個時候,處於 NSDefaultRunLoopMode 裏面的 NSTimer 因爲切換了模式形成計時器沒法繼續運行。

【解決】:

  • 一、更改RunLoop運行Mode(NSRunLoopCommonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  • 二、將NSTimer放到新的線程中
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
    [thread start];

- (void)newThread{
    @autoreleasepool{
        //在當前Run Loop中添加timer,模式是默認的NSDefaultRunLoopMode
        timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(incrementCounter:) userInfo: nil repeats:YES];
        //開始執行新線程的Run Loop,若是不啓動run loop,timer的事件是不會響應的
        [[NSRunLoop currentRunLoop] run];
    }  
}

Runtime & Runloop 面試最常問到的題整理【建議看】

說明:此面試題針對性的摘錄整理,只爲方便 在面試路上準備的你 ,會注有原文。


一、整理原文:2017年5月iOS招人心得(附面試題)

Runtime
  1. objc在向一個對象發送消息時,發生了什麼?
  2. 何時會報unrecognized selector錯誤?iOS有哪些機制來避免走到這一步?
  3. 可否向編譯後獲得的類中增長實例變量?可否向運行時建立的類中添加實例變量?爲何?
  4. runtime如何實現weak變量的自動置nil?
  5. 給類添加一個屬性後,在類結構體裏哪些元素會發生變化?
RunLoop
  1. runloop是來作什麼的?runloop和線程有什麼關係?主線程默認開啓了runloop麼?子線程呢?
  2. runloop的mode是用來作什麼的?有幾種mode
  3. 爲何把NSTimer對象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主運行循環之後,滑動scrollview的時候NSTimer卻不動了?
  4. 蘋果是如何實現Autorelease Pool的?

//-------------------- 【我是分割線】 ---------------------//

整理原文:2017年iOS面試題總結,附上答案

Runtime
01
問題: objc在向一個對象發送消息時,發生了什麼?
解答: 根據對象的 isa 指針找到類對象 id,在查詢類對象裏面的 methodLists 方法函數列表,若是沒有在好到,在沿着 superClass ,尋找父類,再在父類 methodLists 方法列表裏面查詢,最終找到 SEL ,根據 id 和 SEL 確認 IMP(指針函數),在發送消息;
03
問題: 何時會報unrecognized selector錯誤?iOS有哪些機制來避免走到這一步?
解答: 當發送消息的時候,咱們會根據類裏面的 methodLists 列表去查詢咱們要動用的SEL,當查詢不到的時候,咱們會一直沿着父類查詢,當最終查詢不到的時候咱們會報 unrecognized selector 錯誤,當系統查詢不到方法的時候,會調用 +(BOOL)resolveInstanceMethod:(SEL)sel 動態解釋的方法來給我一次機會來添加,調用不到的方法。或者咱們能夠再次使用 -(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法來告訴系統,該調用什麼方法,一來保證不會崩潰。
04
問題: 可否向編譯後獲得的類中增長實例變量?可否向運行時建立的類中添加實例變量?爲何?
解答: 一、不能向編譯後獲得的類增長實例變量 二、能向運行時建立的類中添加實例變量。【解釋】:1. 編譯後的類已經註冊在 runtime 中,類結構體中的 objc_ivar_list 實例變量的鏈表和 instance_size 實例變量的內存大小已經肯定,runtime會調用 class_setvarlayout 或 class_setWeaklvarLayout 來處理strong weak 引用.因此不能向存在的類中添加實例變量。2. 運行時建立的類是能夠添加實例變量,調用class_addIvar函數. 可是的在調用 objc_allocateClassPair 以後,objc_registerClassPair 以前,緣由同上.
05
問題: runtime如何實現weak變量的自動置nil?
解答: runtime 對註冊的類, 會進行佈局,對於 weak 對象會放入一個 hash 表中。 用 weak 指向的對象內存地址做爲 key,當此對象的引用計數爲0的時候會 dealloc,假如 weak 指向的對象內存地址是a,那麼就會以a爲鍵, 在這個 weak 表中搜索,找到全部以a爲鍵的 weak 對象,從而設置爲 nil。
06
問題: 給類添加一個屬性後,在類結構體裏哪些元素會發生變化?
解答: instance_size :實例的內存大小;objc_ivar_list *ivars:屬性列表
RunLoop
01
問題: runloop是來作什麼的?runloop和線程有什麼關係?主線程默認開啓了runloop麼?子線程呢?
解答: runloop: 從字面意思看:運行循環、跑圈,其實它內部就是do-while循環,在這個循環內部不斷地處理各類任務(好比Source、Timer、Observer)事件。runloop和線程的關係:一個線程對應一個RunLoop,主線程的RunLoop默認建立並啓動,子線程的RunLoop需手動建立且手動啓動(調用run方法)。RunLoop只能選擇一個Mode啓動,若是當前Mode中沒有任何Source(Sources0、Sources1)、Timer,那麼就直接退出RunLoop。
02
問題: runloop的mode是用來作什麼的?有幾種mode?
解答: model:是runloop裏面的運行模式,不一樣的模式下的runloop處理的事件和消息有必定的差異。系統默認註冊了5個Mode:(1)kCFRunLoopDefaultMode: App的默認 Mode,一般主線程是在這個 Mode 下運行的。(2)UITrackingRunLoopMode: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響。(3)UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用。(4)GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,一般用不到。(5)kCFRunLoopCommonModes: 這是一個佔位的 Mode,沒有實際做用。注意iOS 對以上5中model進行了封裝 NSDefaultRunLoopMode、NSRunLoopCommonModes
03
問題: 爲何把NSTimer對象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主運行循環之後,滑動scrollview的時候NSTimer卻不動了?
解答: nstime對象是在 NSDefaultRunLoopMode下面調用消息的,可是當咱們滑動scrollview的時候,NSDefaultRunLoopMode模式就自動切換到UITrackingRunLoopMode模式下面,卻不能夠繼續響應nstime發送的消息。因此若是想在滑動scrollview的狀況下面還調用nstime的消息,咱們能夠把nsrunloop的模式更改成NSRunLoopCommonModes.
04
問題: 蘋果是如何實現Autorelease Pool的?
解答: Autorelease Pool做用:緩存池,能夠避免咱們常常寫relase的一種方式。其實就是延遲release,將建立的對象,添加到最近的autoreleasePool中,等到autoreleasePool做用域結束的時候,會將裏面全部的對象的引用計數器 - autorelease.

後續遇到針對 runtime & runloop 常面相關,會及時在這裏補充;

Runloop 模塊博文推薦(❤️數量較多)


分享者 Runloop 模塊推薦閱讀博文
xx_cc 充滿靈性的死循環 www.jianshu.com/p/b9426458f…
WeiHing 原理探究及基本使用 www.jianshu.com/p/911549ae4…
續更 --

參考文章:


效果圖


Runloop&Runtime.gif
Runloop&Runtime.gif

轉載:https://juejin.im/entry/599c13bc6fb9a0248926a77d

相關文章
相關標籤/搜索