NSRunLoop 是基于 CFRunLoopRef 的OC封裝,提供了面向對象的 API,但不是線程安全的,CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,是線程安全的,CoreFoundation是開源的(CoreFoundation 源碼地址)
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跟線程是密不可分的。
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,而子線程在獲取的時候才會創建。
子線程什么時候創建RunLoop
通過查看 NSThread start]的堆棧 可以看到子線程調用了CFRunLoopGetCurrent 這個時候才創建當前線程的runloop,所以都是通過獲取主線程或者當前線程的函數來創建runloop的
RunLoop運行邏輯
通過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是否為空
先判斷source0是否為空,如果為空退出,然后判斷source1是否為空,如果為空退出,然后判斷是否有timer,所以runloop如果要跑起來,必須有source或者timer的其中一個。Source共在2種類型:Source0和Source1,Source0只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(rlms)方法將這個Source標記為待處理,然后手動調用CFRunLoopWakeUp(rl)方法來喚醒RunLoop,讓其處理這個事件。
Source1包含了一個mach_port和一個回調(函數指針),被用于通過內核和其他線程相互發送消息。這種類型的Source能主動喚醒RunLoop的線程。
上面這張runloop邏輯圖(圖片來源地址)將runloop運行的邏輯流程說的很清晰,相比較看到的大家都引用的這張邏輯圖,對比一下。
運行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兩個函數
當前activities = kCFRunLoopEntry 通過cmp x20,#0x1跳轉到0x1965217b4只會執行_objc_autoreleasePoolPush() 向當前的AutoreleasePoolPage增加一個哨兵對象標志創建自動釋放池
而當activities = kCFRunLoopBeforeWaiting|kCFRunLoopExit時,即將進入休眠時會調用objc_autoreleasePoolPop()最新加入的對象一直往前清理直到遇到哨兵對象 和 objc_autoreleasePoolPush()加入釋放對象 。而在即將退出RunLoop時會調用objc_autoreleasePoolPop() 釋放自動自動釋放池內對象