iOS源碼分析(1)——RunLoop

NSRunLoop 是基于 CFRunLoopRef 的OC封裝,提供了面向對象的 API,但不是線程安全的,CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,是線程安全的,CoreFoundation是開源的(CoreFoundation 源碼地址)

image.png

Runloop的創建

typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
 用于手動將當前runloop線程喚醒,通過調用CFRunLoopWakeUp完成,CFRunLoopWakeUp會向_wakeUpPort發送一條消息
    __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;
 // 添加到runloop中的block,通過CFRunLoopPerformBlock可向runloop中添加block任務。
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

一個RunLoop對象,主要包含了對應的一個線程,若干個 modes,若干個集合的 commonMode和commonModeItems,還有一個當前運行的 mode。
  創建RunLoop的函數__CFRunLoopCreate需要傳入的參數是線程,說明runloop跟線程是密不可分的。

image.png

CF沒有對外提供創建runloop的函數,主要通過獲取主線程或當前線程創建的 RunLoop,

CFRunLoopRef CFRunLoopGetMain(void) {
     static CFRunLoopRef __main = NULL; // no retain needed
     // pthread_main_thread_np() 主線程
     if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
     return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
     CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
     if (rl) return rl;
     // pthread_self() 當前線程
     return _CFRunLoopGet0(pthread_self());
 }

都是調用_CFRunLoopGet0函數,通過源碼可知runloop的存儲方式是鍵值對,key是當前線程,value是runloop,線程和runloop是一對一的關系,當字典為空的時候會默認創建主線程的runloop,而子線程在獲取的時候才會創建。


_CFRunLoopGet0函數

子線程什么時候創建RunLoop

通過查看 NSThread start]的堆棧 可以看到子線程調用了CFRunLoopGetCurrent 這個時候才創建當前線程的runloop,所以都是通過獲取主線程或者當前線程的函數來創建runloop的

__NSThread_start_堆棧

RunLoop運行邏輯

image.png

  通過CFRunLoopRun 函數我們直觀的感知到runloop 就是一個do..while的循環。只要result 沒有stop或者返回finish就只周而復始的一直執行CFRunLoopRunSpecific函數。

什么情況會退出循環

app停止運行;線程一次性執行;設置最大時間到期;mode為空;

          //  如果處理事件完畢,啟動Runloop時設置參數為一次性執行
            if (sourceHandledThisLoop && stopAfterHandle) {  
                retVal = kCFRunLoopRunHandledSource;  
            //  如果啟動Runloop時設置的最大運轉時間到期
            } else if (timeout) {  
                retVal = kCFRunLoopRunTimedOut;  
            //  如果啟動Runloop被外部調用強制停止,
            } else if (__CFRunLoopIsStopped(runloop)) {  
                retVal = kCFRunLoopRunStopped;  
            //  如果啟動Runloop的modeI為空,
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {  
                retVal = kCFRunLoopRunFinished;  
            }  

其中接觸到最多的是Mode,每次調用 RunLoop 的主函數時,都需要指定一個 Mode,主要是kCFRunLoopDefaultMode和UITrackingRunLoopMode,如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。前者是系統默認的Runloop Mode,例如進入iOS程序默認不做任何操作就處于這種Mode中,滑動UIScrollView類型的View是,主線程就切換Runloop到到UITrackingRunLoopMode,還有個 UIInitializationRunLoopMode 是程序啟動時進入的 mode,一般用很少用。

struct __CFRunLoopMode {
    ...
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */ //對象鎖,保證線程安全
    ...
    CFMutableSetRef _sources0; //source0類型的CFRunLoopSource的set
    CFMutableSetRef _sources1; //source1類型的CFRunLoopSource的set
    CFMutableArrayRef _observers; //observer數組
    CFMutableArrayRef _timers; //timer數組
    ...
};

CFRunLoop 還定義了一個偽 mode 叫kCFRunLoopCommonModes,它不是一個真正的 mode,而是若干個 mode 的集合,加到 CommonMode 的 source/timer/observer 相當于添加到了它里面所有的 mode 中。我們可以通過lldp po [NSRunLoop currentRunLoop]) 從打印結果看到 CommonMode 包含了上面的 DefaultMode 和 TrackingRunLoopMode:

common modes = <CFBasicHash 0x7fdaa0d00ae0 [0x1084b57b0]>{type = mutable set, count = 2,
entries =>
0 : <CFString 0x10939f950 [0x1084b57b0]>{contents = "UITrackingRunLoopMode"}
2 : <CFString 0x1084d5b40 [0x1084b57b0]>{contents = "kCFRunLoopDefaultMode"}
}

timer 在滑動 UIScrollView類型的View 的時候仍能正常工作,則需要用把 timer 加進 CommonMode 中,這樣就可以在 DefaultMode 或 TrackingRunLoopMode都能執行。

如何判斷mode是否為空

判斷mode是否為空

  先判斷source0是否為空,如果為空退出,然后判斷source1是否為空,如果為空退出,然后判斷是否有timer,所以runloop如果要跑起來,必須有source或者timer的其中一個。Source共在2種類型:Source0和Source1,Source0只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(rlms)方法將這個Source標記為待處理,然后手動調用CFRunLoopWakeUp(rl)方法來喚醒RunLoop,讓其處理這個事件。
Source1包含了一個mach_port和一個回調(函數指針),被用于通過內核和其他線程相互發送消息。這種類型的Source能主動喚醒RunLoop的線程。

runloop的運行邏輯

上面這張runloop邏輯圖(圖片來源地址)將runloop運行的邏輯流程說的很清晰,相比較看到的大家都引用的這張邏輯圖,對比一下。

image.png

運行NSRunloop默認提供了三個常用的run方法:

     - (void)run; 
     - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
     - (void)runUntilDate:(NSDate *)limitDate;

run方法對應上面CFRunloop中的CFRunLoopRun并不會退出,除非調用CFRunLoopStop();通常如果想要永遠不會退出RunLoop才會使用此方法,否則可以使用runUntilDate。
runMode:beforeDate:則對應CFRunLoopRunInMode(mode,limiteDate,true)方法,只執行一次,執行完就退出。
而runUntilDate:方法其實是設置超時時間,并且是否執行一次參數設置的是false等同于CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false),執行完并不會退出,繼續下一次RunLoop直到timeout。

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

RunLoop與AutoreleasePool

@autoreleasepool 通過執行clang -rewrite-objc是一個__AtAutoreleasePool 結構體

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

objc_autoreleasePoolPush對象添加到自動釋放池中,objc_autoreleasePoolPop釋放對象 ,再這里不過多的展開2個函數的具體實現。

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

[NSRunLoop currentRunLoop] 的結果中我們可以看到與自動釋放池相關的CFRunLoopObserver 是:

<CFRunLoopObserver>{activities = 0x1, callout = _wrapRunLoopWithAutoreleasePoolHandler} 
<CFRunLoopObserver>{activities = 0xa0, callout = _wrapRunLoopWithAutoreleasePoolHandler}
####RunLoop 的運行狀態

RunLoop 的狀態的變化有

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    // 即將進入 loop
    kCFRunLoopEntry = (1UL << 0),
    // 即將處理 timer
    kCFRunLoopBeforeTimers = (1UL << 1),
    // 即將處理 source
    kCFRunLoopBeforeSources = (1UL << 2),
    // 即將 sleep
    kCFRunLoopBeforeWaiting = (1UL << 5),
    // 剛被喚醒,退出 sleep
    kCFRunLoopAfterWaiting = (1UL << 6),
    // 即將退出
    kCFRunLoopExit = (1UL << 7),
    // 全部的活動
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

activities = 0x1代表是kCFRunLoopEntry進入 loop ,而activities = 0xa0代表(kCFRunLoopBeforeWaiting | kCFRunLoopExit)準備進入睡眠和即將退出 loop 兩個runloop狀態

我們可以使用 CFRunLoopObserverCreateWithHandler() 來創建 observer,創建時設置要監聽的狀態變化和回調,再用 CFRunLoopAddObserver() 來給當前的 RunLoop 添加 observer,當前 RunLoop 狀態發生變化時,observer 就會執行回調

 CFRunLoopObserverRef observer =
    CFRunLoopObserverCreateWithHandler(
                                       CFAllocatorGetDefault(),
                                       kCFRunLoopAllActivities,
                                       YES,
                                       0,
                                       ^(CFRunLoopObserverRef observer,
                                         CFRunLoopActivity activity) {
                                           if (activity==kCFRunLoopEntry) {
                                               NSLog(@"即將進入Loop:%zd", activity);
                                           }
                                           if (activity==kCFRunLoopBeforeWaiting) {
                                                NSLog(@"即將進入休眠:%zd", activity);
                                           }else{
                                               NSLog(@"RunLoop 的狀態變化:%zd", activity);
                                           }
                                          
                                       });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    CFRelease(observer);

_wrapRunLoopWithAutoreleasePoolHandler 調用邏輯

通過設置對_wrapRunLoopWithAutoreleasePoolHandler設置符號斷點,獲取到的匯編代碼,_wrapRunLoopWithAutoreleasePoolHandler 會調用NSPopAutoreleasePool和NSPushAutoreleasePool,也是objc_autoreleasePoolPush和objc_autoreleasePoolPop兩個函數


_wrapRunLoopWithAutoreleasePoolHandler

  當前activities = kCFRunLoopEntry 通過cmp x20,#0x1跳轉到0x1965217b4只會執行_objc_autoreleasePoolPush() 向當前的AutoreleasePoolPage增加一個哨兵對象標志創建自動釋放池
  而當activities = kCFRunLoopBeforeWaiting|kCFRunLoopExit時,即將進入休眠時會調用objc_autoreleasePoolPop()最新加入的對象一直往前清理直到遇到哨兵對象 和 objc_autoreleasePoolPush()加入釋放對象 。而在即將退出RunLoop時會調用objc_autoreleasePoolPop() 釋放自動自動釋放池內對象

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

推薦閱讀更多精彩內容

  • runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的, 在面試過程中是經常會被問到的, ...
    SOI閱讀 21,863評論 3 63
  • runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的, 在面試過程中是經常會被問到的, ...
    made_China閱讀 1,233評論 0 7
  • Runloop是iOS和OSX開發中非常基礎的一個概念,從概念開始學習。 RunLoop的概念 -般說,一個線程一...
    小貓仔閱讀 1,024評論 0 1
  • 落葉凄涼隨舞纏, 闌珊夜色濕炊煙。 倦啾不出聲還亂, 孤叫嫣然冷匯天。 西宛起,對風言, 旅人不在畫愁眠。 誰人相...
    田萍閱讀 386評論 0 3
  • 一個人感受到自己的堅強時,一定是受了很大的委屈,在乎的人都在那里喋喋不休,不巧的是,他們多說一句都會覺得心煩,有很...
    陳雨啊閱讀 329評論 0 0