RunLoop的定義與概念html
RunLoop的主要做用objective-c
main函數中的RunLoop編程
RunLoop與線程的關係swift
RunLoop的消息種類數組
RunLoop的對外接口安全
RunLoop的modebash
RunLoop的內部邏輯網絡
RunLoop在iOS中內部的應用app
RunLoop在iOS開發中的實踐框架
總結
參考文獻
RunLoop
, 就是一個在Run的loop,就是一個一直在跑的圈。其本質就是無休止的while循環
。通常的程序都是執行完任務後便結束。但因爲手機應用的特殊性,在其不執行任務時,也不能將其殺死,而是暫時休眠狀態,直到有外部或內部因素將其喚醒,繼續run。直到用戶手動將該程序完全關閉。// 無Runloop,程序執行完後,直接返回
int main(int argc,char * argv[]){
NSLog(@"execute main function");---->程序開始
return 0; ------------------------->程序結束
}
// 有Runloop
int main(int argc,char * argv[]){
BOOL running = YES; -------->程序開始
do {------------------------------
// 執行各類任務,處理各類事件------持續運行
}while(running);---------------------
return 0;
}
複製代碼
NSRunLoop
和 CFRunLoopRef
。
CFRunLoopRef
是在 CoreFoundation
框架內的,它提供了純 C 函數的 API,全部這些 API 都是線程安全的
。CFRunLoopRef
的封裝,提供了面向對象的 API,可是這些 API 不是線程安全的
。main
函數內部也啓動了一個RunLoopint main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製代碼
UIApplicationMain
函數內部幫咱們開啓了主線程的 RunLoop
, UIApplicationMain
內部擁有一個無線循環的代碼。這個 UIApplicationMain
函數幫咱們啓動的 RunLoop 屬於程序的主線程全部,因此咱們不須要再爲主線程開啓 RunLoop。因爲咱們的程序在主線程上擁有一個 RunLoop ,因此咱們將程序打開後,執行完它所須要的任務後,不必定非要退出程序,而是能夠選擇將其後臺掛起。一切都是由於 主線程上RunLoop 的關係,咱們的程序才能夠長時間持續運行。獲取
方法,就不會得到 RunLoop 的緣由。在 Core Foundation 中
在 Cocoa 中
CFRunLoopRef
和 NSRunLoop
能夠轉化, NSRunLoop
使用 getCFRunLoop
方法就能夠獲得 CFRunLoopRef
對象
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時,初始化全局Dic,並先爲主線程建立一個 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 裏獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,建立一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 註冊一個回調,當線程銷燬時,順便也銷燬其對應的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
複製代碼
這是apple官方文檔的一張圖, 表示了Runloop的消息種類。
這張圖我也沒有特別看懂,主要是講的Runloop的兩種輸入源。官方文檔的解釋是
CFRunLoopSourceSignal(source)
,將這個 Source 標記爲待處理,而後手動調用 CFRunLoopWakeUp(runloop)
來喚醒 RunLoop,讓其處理這個事件。typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠 32
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒 64
kCFRunLoopExit = (1UL << 7), // 即將退出Loop 128
};
複製代碼
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
複製代碼
系統默認註冊了5個Mode:
咱們平時主要應用的 mode 有 kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
兩種, 其中 kCFRunLoopDefaultMode(在cocoa中也叫NSDefaultRunLoopMode)
是咱們開啓一個 RunLoop 時默認的 mode 方式。 而 UITrackingRunLoopMode
主要是追蹤 ScrollView 滑動時的狀態。
這裏有個概念叫 「CommonModes」:一個 Mode 能夠將本身標記爲」Common」屬性(經過將其 ModeName 添加到 RunLoop 的 「commonModes」 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 裏的 Source/Observer/Timer 同步到具備 「Common」 標記的全部Mode裏。須要注意的是: 「CommonModes」並非一個真正的 mode , 它其實是一個 數組 裏面放入了 kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
應用場景舉例:主線程的 RunLoop 裏有兩個預置的 Mode:kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
。這兩個 Mode 都已經被標記爲」Common」屬性。DefaultMode
是 App 平時所處的狀態,UITrackingRunLoopMode
是追蹤 ScrollView 滑動時的狀態。當你建立一個 Timer 並加到 DefaultMode
時,Timer 會獲得重複回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換爲 UITrackingRunLoopMode
,這時 Timer 就不會被回調,而且也不會影響到滑動操做。有時你須要一個 Timer,在兩個 Mode 中都能獲得回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 「commonModeItems」 中。」commonModeItems」 被 RunLoop 自動更新到全部具備」Common」屬性的 Mode 裏去。
CFRunLoop對外暴露的管理 Mode 接口只有下面2個:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
複製代碼
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
複製代碼
先放一張我認爲很是好的圖,其中仍是有些錯誤之處,根據官方文檔,第7步:休眠,等待喚醒的source種類應該是source1,而不是source0。
內部代碼實現以下 若是嫌太長,直接看註釋便可。
/// 用DefaultMode啓動
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode啓動,容許設置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根據modeName找到對應mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 若是mode裏沒有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即將進入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 內部函數,進入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即將觸發 Timer 回調。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 執行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 觸發 Source0 (非port) 回調。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 執行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 若是有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 而後跳轉去處理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 調用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
/// • 一個基於 port 的Source 的事件。
/// • 一個 Timer 到時間了
/// • RunLoop 自身的超時時間到了
/// • 被其餘什麼調用者手動喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,處理消息。
handle_msg:
/// 9.1 若是一個 Timer 到時間了,觸發這個Timer的回調。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 若是有dispatch到main_queue的block,執行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 若是一個 Source1 (基於port) 發出事件了,處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 執行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進入loop時參數說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入參數標記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部調用者強制中止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 若是沒超時,mode裏沒空,loop也沒被中止,那繼續loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即將退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
複製代碼
{
/// 1. 通知Observers,即將進入RunLoop
/// 此處有Observer會建立AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發 Timer 回調。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回調。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發 Source0 (非基於port的) 回調。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即將進入休眠
/// 此處有Observer釋放並新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,線程被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 若是是被Timer喚醒的,回調Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 若是是被dispatch喚醒的,執行全部調用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 若是若是Runloop是被 Source1 (基於port的) 的事件喚醒了,處理這個事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即將退出RunLoop
/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
複製代碼
__IOHIDEventSystemClientQueueCallback()
。_UIApplicationHandleEventQueue()
進行應用內部的分發。_UIApplicationHandleEventQueue()
會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。一般事件好比 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。__IOHIDEventSystemClientQueueCallback()
內觸發的 Source0,Source0 再觸發的 _UIApplicationHandleEventQueue()
。因此 UIButton 事件看到是在 Source0 內的。_UIApplicationHandleEventQueue()
識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記爲待處理。 蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver()
,其內部會獲取全部剛被標記爲待處理的 GestureRecognizer,並執行GestureRecognizer的回調。 當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
。這個函數裏會遍歷全部待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面。__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
裏執行這個 block。但這個邏輯僅限於 dispatch 到主線程,dispatch 到其餘線程仍然是由 libDispatch 處理的。+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
複製代碼
利用PerformSelector設置當前線程的RunLoop的運行模式
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:NSDefaultRunLoopMode];
複製代碼
解釋上述 AFNetworking 長時間鏈接問題 。
方法1即 AFNetworking 中使用的方法,它使得 thread 在打開後,使用結束方法CFRunLoopStop(CFRunLoopGetCurrent())
並無正常結束,該結束方法對這個建立 Thread 的方式無效。因此致使了內存暴增,圖以下
方法2 如圖,因爲它是一個非線程阻塞的方法,因此有時在線程還已經退出後,纔開始暫停這個線程,天然會使得程序崩潰。
方法3 纔是我推薦的方法 ,它能夠正確的建立一個後臺常駐線程。
下面解釋一下原理吧
CFRunLoopStop()
方法只會結束當前的 runMode:beforeDate: 調用,而不會結束後續的調用。run()
方法,實際上就是在內部不斷地調用runMode:beforeDate:
方法,而所謂的runUntilDate:
方法,也是有限的調用runMode:beforeDate:
方法。只是它們所傳的參數都是DefaultMode
罷了。CFRunLoopStop()
方法手動將 RunLoop 結束。CFRunLoopStop()
方法手動將 RunLoop 結束。CFRunLoopStop()
方法的定義中說了。The difference is that you can use this technique on run loops you started unconditionally.
If you want the run loop to terminate, you shouldn't use this method
runMode:beforeDate:
調用,而不會結束後續的調用。這也就是爲何 Runloop 的文檔中說CFRunLoopStop()
能夠 exit(退出) 一個 RunLoop,而在 run 等方法的文檔中又說這樣會致使 RunLoop 沒法 terminate(終結)。