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() 釋放自動自動釋放池內對象

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容

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