對於iOS開發,runLoop機制還是很有必要了解一下的,最近在做一個廣告圖的功能正好需要了解下runtime機制問題,在查看了官方文檔API以及論壇貼吧博客各位大牛的文章後,整理下關於我自己的理解和總結.
OSX/iOS 系統中,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的。
NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的。
Thread包含一個CFRunLoop,一個CFRunLoop包含一種CFRunLoopMode,mode包含CFRunLoopSource,CFRunLoopTimer和CFRunLoopObserver。
RunLoop只能運行在一種mode下,如果要換mode當前的loop也需要停下重啟成新的。利用這個機制,ScrollView過程中NSDefaultRunLoopMode的mode會切換UITrackingRunLoopMode來保證ScrollView的流暢滑動不受只能在NSDefaultRunLoopMode時處理的事件影響滑動。同時mode還是可定制的。
NSDefaultRunLoopMode:默認,空閒狀態
UITrackingRunLoopMode:ScrollView滑動時
UIInitializationRunLoopMode:啟動時
NSRunLoopCommonModes:Mode集合 Timer計時會被scrollView的滑動影響的問題可以通過將timer添加到NSRunLoopCommonModes來解決
//然後再添加到NSRunLoopCommonModes裡 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
/// 全局的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()); }
介紹完線程和RunLoop的關系後,主要看下RunLoop和RunLoopModel,
RunLooprunLoop 在CoreFoundation對外一共有五大類最為對外的接口:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
一個 RunLoZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcCCw/LqsyPS4ybj2IE1vZGWjrMO/uPYgTW9kZSDT1rD8uqzI9LjJuPYgU291cmNlL1RpbWVyL09ic2VydmVyoaPDv7TOtffTwyBSdW5Mb29wILXE1ve6r8r9yrGjrNa7xNzWuLaoxuTW0NK7uPYgTW9kZaOs1eK49k1vZGWxu7PG1/cgQ3VycmVudE1vZGWho8jnufvQ6NKqx9C7uyBNb2Rlo6zWu8Tczcuz9iBMb29wo6zU2dbY0MLWuLao0ru49iBNb2RlIL34yOuho9Xi0fnX9tb30qrKx86qwcu31rj0v6qyu82s1+m1xCBTb3VyY2UvVGltZXIvT2JzZXJ2ZXKjrMjDxuS7pbK707DP7KGjPC9wPg0KPGg0IGlkPQ=="2-cfrunloopsourceref">2. CFRunLoopSourceRef
是事件產生的地方,Source包含了兩個部分:Source0 和 Source1。
source0:處理如UIEvent,CFSocket這樣的事件
source1:Mach port驅動,CFMachport,CFMessagePort
Source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
Source1 包含了一個 mach_port 和一個回調(函數指針),被用於和通過內核和其他 mach_port 相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。
NSTimer是對RunLoopTimer的封裝
是基於時間的觸發器,它和 NSTimer 是toll-free bridge 的。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調
Cocoa框架中很多機制比如CAAnimation等都是由RunLoopObserver觸發的。observer到當前狀態的變化進行通知
觀察者,每個 Ovserver 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能通過回調接受到這個變化。可以觀測的時間點有以下幾個:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即將進入Loop kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒 kCFRunLoopExit = (1UL << 7), // 即將退出Loop };
CFRunLoopMode 和 CFRunLoop 的結構大致如下:
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 CFRunLoopModeRef _currentMode; // Current Runloop Mode CFMutableSetRef _modes; // Set ... };
這裡有個概念叫 “CommonModes”:一個 Mode 可以將自己標記為”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。RunLoop 會自動將 _commonModeItems 加入到具有 “Common” 標記的所有Mode裡。
應用場景舉例:主線程的 RunLoop 裡有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記為”Common”屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你創建一個 Timer 並加到 DefaultMode 時,Timer 會得到重復回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調,並且也不會影響到滑動操作。
有時你需要一個 Timer,在兩個 Mode 中都能得到回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 裡去。
從上面代碼可以看到,RunLoop的核心是基於 mach port 的,其進入休眠時調用的函數是 mach_msg()。為了解釋這幾個概念,下面稍微介紹一下OSX/iOS的系統架構。
蘋果官方將整個系統大致劃分為上述4個層次:
應用層包括用戶能接觸到的圖形應用,例如 Spotlight、Aqua、SpringBoard等。
應用框架層即開發人員接觸到的 Cocoa 等框架。
核心框架層包括各種核心框架、OpenGL 等內容。
Darwin 即操作系統的核心,包括系統內核、驅動、Shell 等內容,這一層是開源的,其所有源碼都可以在opensource.apple.com裡找到。
CFRunLoop { current mode = kCFRunLoopDefaultMode common modes = { UITrackingRunLoopMode kCFRunLoopDefaultMode } common mode items = { // source0 (manual) CFRunLoopSource {order =-1, { callout = _UIApplicationHandleEventQueue}} CFRunLoopSource {order =-1, { callout = PurpleEventSignalCallback }} CFRunLoopSource {order = 0, { callout = FBSSerialQueueRunLoopSourceHandler}} // source1 (mach port) CFRunLoopSource {order = 0, {port = 17923}} CFRunLoopSource {order = 0, {port = 12039}} CFRunLoopSource {order = 0, {port = 16647}} CFRunLoopSource {order =-1, { callout = PurpleEventCallback}} CFRunLoopSource {order = 0, {port = 2407, callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}} CFRunLoopSource {order = 0, {port = 1c03, callout = __IOHIDEventSystemClientAvailabilityCallback}} CFRunLoopSource {order = 0, {port = 1b03, callout = __IOHIDEventSystemClientQueueCallback}} CFRunLoopSource {order = 1, {port = 1903, callout = __IOMIGMachPortPortCallback}} // Ovserver CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry callout = _wrapRunLoopWithAutoreleasePoolHandler} CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting callout = _UIGestureRecognizerUpdateObserver} CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit callout = _afterCACommitHandler} CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit callout = _wrapRunLoopWithAutoreleasePoolHandler} // Timer CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0, next fire date = 453098071 (-4421.76019 @ 96223387169499), callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)} }, modes = { CFRunLoopMode { sources0 = { /* same as 'common mode items' */ }, sources1 = { /* same as 'common mode items' */ }, observers = { /* same as 'common mode items' */ }, timers = { /* same as 'common mode items' */ }, }, CFRunLoopMode { sources0 = { /* same as 'common mode items' */ }, sources1 = { /* same as 'common mode items' */ }, observers = { /* same as 'common mode items' */ }, timers = { /* same as 'common mode items' */ }, }, CFRunLoopMode { sources0 = { CFRunLoopSource {order = 0, { callout = FBSSerialQueueRunLoopSourceHandler}} }, sources1 = (null), observers = { CFRunLoopObserver >{activities = 0xa0, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} )}, timers = (null), }, CFRunLoopMode { //這是一個占位的 Mode,沒有實際作用。 sources0 = { CFRunLoopSource {order = -1, { callout = PurpleEventSignalCallback}} }, sources1 = { CFRunLoopSource {order = -1, { callout = PurpleEventCallback}} }, observers = (null), timers = (null), }, CFRunLoopMode { sources0 = (null), sources1 = (null), observers = (null), timers = (null), } } }
這裡能看到蘋果所有的Model
蘋果注冊了一個 Source1 (基於 mach port 的) 用來接收系統事件,其回調函數為 __IOHIDEventSystemClientQueueCallback()。
當一個硬件事件(觸摸/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收。這個過程的詳細情況可以參考 這裡 。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨後用 mach port 轉發給需要的App進程。隨後蘋果注冊的那個 Source1 就會觸發回調,並調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理並包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。
當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記為待處理。
蘋果注冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,並執行GestureRecognizer的回調。
當有 UIGestureRecognizer 的變化(創建/銷毀/狀態改變)時,這個回調都會進行相應處理。
當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記為待處理,並被提交到一個全局的容器去。
蘋果注冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數裡會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪制和調整,並更新 UI 界面。
AFNetworking 創建一個常駐服務線程去處理數據返回
使用NSOperation+NSURLConnection並發模型都會面臨NSURLConnection下載完成前線程退出導致NSOperation對象接收不到回調的問題。
AFNetWorking解決這個問題的方法是按照官方的
guid上寫的NSURLConnection的delegate方法需要在connection發起的線程runloop中調用,於是AFNetWorking直接借鑒了Apple自己的一個IOS/samplecode/MVCNetworking/Introduction/Intro.html" target="_blank">Demo的實現方法單獨起一個global thread,內置一個runloop,所有的connection都由這個runloop發起,回調也是它接收,不占用主線程,也不耗CPU資源。
利用CFRunLoopMode的特性,可以將圖片的加載放到NSDefaultRunLoopMode的mode裡,這樣在滾動UITrackingRunLoopMode這個mode時不會被加載而影響到。
UIImage *downloadedImage = ...; [self.avatarImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
CFRunLoopRunInMode(kCFRunLoopDefaultMode,second, NO);
CFRunLoopStop(CFRunLoopGetMain());
/// 用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); }