讀 Threading Programming Guide 筆記(三)
來自: http://www.devtalking.com/articles/read-threading-programming-guide-3/
前文中多次提到過,在主線程中Run Loop是隨著應用程序一起啟動的,也就是說當我們打開一個應用時,主線程中的Run Loop就已經啟動了,尤其現在我們都使用Xcode中的項目模版創建項目,更是不用考慮主線程中Run Loop的狀體。所以只有在二級線程中,也就是我們自己創建的線程中才有機會手動的創建的Run Loop,并對其進行配置的操作。
在前文中還提到過,Run Loop在線程中的主要作用就是幫助線程常駐在進程中,并且不會過多消耗資源。所以說Run Loop在二級線程中也不是必須需要的,要根據該線程執行的任務類型以及在整個應用中擔任何作用而決定是否需要使用Run Loop。比如說,如果你創建一個二級線程只是為了執行一個不會頻繁執行的一次性任務,或者需要執行很長時間的任務,那么可能就不需要使用Run Loop了。如果你需要一個線程執行周期性的定時任務,或者需要較為頻繁的與主線程之間進行交互,那么就需要使用Run Loop。歸納一下需要使用Run Loop的情況大概有以下四點:
- 通過基于端口或自定義的數據源與其他線程進行交互。
- 在線程中執行定時事件源的任務。
- 使用Cocoa框架提供的 performSelector… 系列方法。
- 在線程中執行較為頻繁的,具有周期性的任務。
光說不練假把式,下面就讓我們來看看如何具體創建、配置、操作Run Loop。
Run Loop對象
要想操作配置Run Loop,那自然需要通過Run Loop對象來完成,它提供了一系列接口,可幫助我們便捷的添加Input sources、timers以及觀察者。較高級別的Cocoa框架提供了 NSRunLoop 類,較底層級別的Core Foundation框架提供了指向 CFRunloopRef 的指針。
獲取Run Loop對象
前文中提到過,在Cocoa和Core Foundation框架中都沒有提供創建Run Loop的方法,只有從當前線程獲取Run Loop的方法:
- 在Cocoa框架中, NSRunLoop 類提供了類方法 currentRunLoop() 獲取 NSRunLoop 對象。
該方法是獲取當前線程中已存在的Run Loop,如果不存在,那其實還是會創建一個Run Loop對象返回,只是Cocoa框架沒有向我們暴露該接口。
- 在Core Foundation框架中提供了 CFRunLoopGetCurrent() 函數獲取 CFRunLoop 對象。
雖然這兩個Run Loop對象并不完全等價,它們之間還是可以轉換的,我們可以通過 NSRunLoop 對象提供的 getCFRunLoop() 方法獲取 CFRunLoop 對象。因為 NSRunLoop 和 CFRunLoop 指向的都是當前線程中同一個Run Loop,所以在使用時它們可以混用,比如說要給Run Loop添加觀察者時就必須得用 CFRunLoop 了。
配置Run Loop觀察者
前文中提到過,可以向Run Loop中添加各種事件源和觀察者,這里事件源是必填項,也就是說Run Loop中至少要有一種事件源,不論是Input source還是timer,如果Run Loop中沒有事件源的話,那么在啟動Run Loop后就會立即退出。而觀察者是可選項,如果沒有監控Run Loop各運行狀態的需求,可以不配置觀察者,這一節先看看如何向Run Loop中添加觀察者。
在Cocoa框架中,并沒有提供創建配置Run Loop觀察者的相關接口,所以我們只能通過Core Foundation框架中提供的對象和方法創建并配置Run Loop觀察者,下面我們看看示例代碼:
import Foundation class TestThread: NSObject { func launch() { print("First event in Main Thread.") NSThread.detachNewThreadSelector("createAndConfigObserverInSecondaryThread", toTarget: self, withObject: nil) print(NSThread.isMultiThreaded()) sleep(3) print("Second event in Main Thread.") } func createAndConfigObserverInSecondaryThread() { autoreleasepool{ // 1 let runloop = NSRunLoop.currentRunLoop() // 2 var _self = self // 3 var observerContext = CFRunLoopObserverContext(version: 0, info: &_self, retain: nil, release: nil, copyDescription: nil) // 4 let observer = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.AllActivities.rawValue, true, 0, self.observerCallbackFunc(), &observerContext) if(observer != nil) { // 5 let cfRunloop = runloop.getCFRunLoop() // 6 CFRunLoopAddObserver(cfRunloop, observer, kCFRunLoopDefaultMode) } // 7 NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "fireTimer", userInfo: nil, repeats: true) var loopCount = 10 repeat { // 8 runloop.runUntilDate(NSDate(timeIntervalSinceNow: 1)) loopCount-- } while(loopCount > 0) } } func observerCallbackFunc() -> CFRunLoopObserverCallBack { return {(observer, activity, context) -> Void in switch(activity) { case CFRunLoopActivity.Entry: print("Run Loop已經啟動") break case CFRunLoopActivity.BeforeTimers: print("Run Loop分配定時任務前") break case CFRunLoopActivity.BeforeSources: print("Run Loop分配輸入事件源前") break case CFRunLoopActivity.BeforeWaiting: print("Run Loop休眠前") break case CFRunLoopActivity.AfterWaiting: print("Run Loop休眠后") break case CFRunLoopActivity.Exit: print("Run Loop退出后") break default: break } } } func fireTimer() { } } let testThread = TestThread() testThread.launch()
</div>
下面解讀一下上述代碼示例, launch() 方法在主線程中,通過 NSThread 類的類方法 detachNewThreadSelector:toTarget:withObject: 創建并啟動一個二級線程,將 createAndConfigObserverInSecondaryThread() 方法作為事件消息傳入該二級線程,這個方法的主要作用就是在二級線程中創建配置Run Loop觀察者并啟動Run Loop,然后讓主線程持續3秒,以便二級線程有足夠的時間執行任務。
在 createAndConfigObserverInSecondaryThread() 中共有8個關鍵步驟,下面一一進行說明:
- 第一步 :通過 NSRunLoop 類的類方法 currentRunLoop() 獲取當前線程的Run Loop,這里獲取到的Run Loop對象是 NSRunLoop 對象。
- 第二步 :申明當前對象的變量,至于為什么要這么做,在下一步中會有說明。
- 第三步 :通過Core Foundation框架的 CFRunLoopObserverContext 結構體構造Run Loop觀察者上下文,大家需要注意前兩個參數,我們先看看這個結構體:
public struct CFRunLoopObserverContext { public var version: CFIndex public var info: UnsafeMutablePointer<Void> public var retain: (@convention(c) (UnsafePointer<Void>) -> UnsafePointer<Void>)! public var release: (@convention(c) (UnsafePointer<Void>) -> Void)! public var copyDescription: (@convention(c) (UnsafePointer<Void>) -> Unmanaged<CFString>!)! public init() public init(version: CFIndex, info: UnsafeMutablePointer<Void>, retain: (@convention(c) (UnsafePointer<Void>) -> UnsafePointer<Void>)!, release: (@convention(c) (UnsafePointer<Void>) -> Void)!, copyDescription: (@convention(c) (UnsafePointer<Void>) -> Unmanaged<CFString>!)!) }
</div>
- version :結構體版本號,必須設置為0。
- info :上下文中 retain 、 release 、 copyDescription 三個回調函數以及Run Loop觀察者的回調函數所有者對象的指針。在Swift中, UnsafePointer 結構體代表C系語言中申明為常量的指針, UnsafeMutablePoinger 結構體代表C系語言中申明為非常量的指針,比如說:
C: void functionWithConstArg(const int *constIntPointer); Swift: func functionWithConstArg(constIntPointer: UnsafePointer<Int32>) C: void functionWithNotConstArg(unsigned int *unsignedIntPointer); Swift: func functionWithNotConstArg(unsignedIntPointer: UnsafeMutablePointer<UInt32>) C: void functionWithNoReturnArg(void *voidPointer); Swift: func functionWithNoReturnArg(voidPointer: UnsafeMutablePointer<Void>)
</div>
- 第四步 :通過Core Foundation框架的 CFRunLoopObserverCreate 函數創建 CFRunLoopObserver 對象:
public func CFRunLoopObserverCreate(allocator: CFAllocator!, _ activities: CFOptionFlags, _ repeats: Bool, _ order: CFIndex, _ callout: CFRunLoopObserverCallBack!, _ context: UnsafeMutablePointer<CFRunLoopObserverContext>) -> CFRunLoopObserver!
</div>
- allocator :該參數為對象內存分配器,一般使用默認的分配器 kCFAllocatorDefault 。
- activities :該參數配置觀察者監聽Run Loop的哪種運行狀態。在示例中,我們讓觀察者監聽Run Loop的所有運行狀態。
- repeats :該參數標識觀察者只監聽一次還是每次Run Loop運行時都監聽。
- order :觀察者優先級,當Run Loop中有多個觀察者監聽同一個運行狀態時,那么就根據該優先級判斷,0為最高優先級別。
- callout :觀察者的回調函數,在Core Foundation框架中用 CFRunLoopObserverCallBack 重定義了回調函數的閉包。
- context :觀察者的上下文。
- 第五步 :因為 NSRunLoop 沒有提供操作觀察者的接口,所以我們需要 getCFRunLoop() 方法獲取到 CFRunLoop 對象。
- 第六步 :通過 CFRunLoopAddObserver 函數向當前線程的Run Loop中添加創建好的觀察者:
func CFRunLoopAddObserver(_ rl: CFRunLoop!, _ observer: CFRunLoopObserver!, _ mode: CFString!)
</div>
- rl :當前線程的 CFRunLoop 對象。
- observer :創建好的觀察者。
- mode :設置將觀察者添加到哪個Run Loop模式中。
這里需要注意的是,一個觀察者只能被添加到一個Run Loop中,但是可以被添加到Run Loop中的多個模式中。
- 第七步 :通過Timer事件源向當前線程發送重復執行的定時任務,時間間隔為0.5秒,因為只是為了測試觀察者,所以 fireTimer() 是一個空任務。另外前文中提到過,如果Run Loop中沒有任何數據源,那么Run Loop啟動后會立即退出,所以大家可以把這行注釋了運行看看會有什么效果。
- 第八步 :通過 NSRunLoop 對象的 runUntilDate(limitDate: NSDate) 方法啟動Run Loop,設置Run Loop的運行時長為1秒。這里將其放在一個循環里,最大循環次數為10次,也就是說,如果不考慮主線程的運行時間,該二級線程的Run Loop可運行10次。
再來看看觀察者的回調方法 observerCallbackFunc() ,上面在介紹 CFRunLoopObserverCreate 函數時提到觀察者的回調函數是 CFRunLoopObserverCallBack 重定義的一個閉包,我們來看看這個閉包:
typealias CFRunLoopObserverCallBack = (CFRunLoopObserver!, CFRunLoopActivity, UnsafeMutablePointer<Void>) -> Void
</div>
這個閉包沒有返回值,第一個參數是觸發監聽的觀察者,第二個參數是觀察者監聽的Run Loop運行狀態,第三個參數是觀察者的運行上下文環境。所以在回調方法中,我們只需要根據第二個參數的值即可判斷觀察者監聽到的Run Loop狀態。大家可以拷貝上面的代碼,建一個Command Application運行看看結果。
啟動Run Loop
在啟動Run Loop前務必要保證已添加一種類型的事件源,原因在前文中已提到多次。在Cocoa框架和Core Foundation框架中啟動Run Loop大體有三種形式,分別是無條件啟動、設置時間限制啟動、指定特定模式啟動。
無條件啟動
NSRunLoop 對象的 run() 方法和Core Foundation框架中的 CFRunLoopRun() 函數都是無條件啟動Run Loop的方式。這種方式雖然是最簡單的啟動方式,但也是最不推薦使用的一個方式,因為這種方式將Run Loop置于一個永久運行并且不可控的狀態,它使Run Loop只能在默認模式下運行,無法給Run Loop設置特定的或自定義的模式,而且以這種模式啟動的Run Loop只能通過 CFRunLoopStop(_ rl: CFRunLoop!) 函數強制停止。
設置時間限制啟動
該方式對應的方法是 NSRunLoop 對象的 runUntilDate(_ limitDate: NSDate) 方法,在啟動Run Loop時設置超時時間,一旦超時那么Run Loop則自動退出。該方法的好處是可以在循環中反復啟動Run Loop處理相關任務,而且可控制運行時長。
指定特定模式啟動
該方式對應的方法是 NSRunLoop 對象的 runMode(_ mode: String, beforeDate limitDate: NSDate) 方法和Core Foundation框架的 CFRunLoopRunInMode(_ mode: CFString!, _ seconds: CFTimeInterval, _ returnAfterSourceHandled: Bool) 函數。前者有兩個參數,第一個參數是Run Loop模式,第二個參數仍然是超時時間,該方法使Run Loop只處理指定模式中的事件源事件,當處理完事件或超時Run Loop會退出,該方法的返回值類型是 Bool ,如果返回 true 則表示Run Loop啟動成功,并分派執行了任務或者達到超時時間,若返回 false 則表示Run Loop啟動失敗。后者有三個參數,前兩個參數的作用一樣,第三個參數的意思是Run Loop是否在執行完任務后就退出,如果設置為 false ,那么代表Run Loop在執行完任務后不退出,而是一直等到超時后才退出。該方法返回Run Loop的退出狀態:
- CFRunLoopRunResult.Finished :表示Run Loop已分派執行完任務,并且再無任務執行的情況下退出。
- CFRunLoopRunResult.Stopped :表示Run Loop通過 CFRunLoopStop(_ rl: CFRunLoop!) 函數強制退出。
- CFRunLoopRunResult.TimedOut :表示Run Loop因為超時時間到而退出。
- CFRunLoopRunResult.HandledSource :表示Run Loop已執行完任務而退出,改狀態只有在 returnAfterSourceHandled 設置為 true 時才會出現。
退出Run Loop
退出Run Loop的方式總體來說有三種:
- 啟動Run Loop時設置超時時間。
- 強制退出Run Loop。
- 移除Run Loop中的事件源,從而使Run Loop退出。
第一種方式是推薦使用的方式,因為可以給Run Loop設置可控的運行時間,讓它執行完所有的任務以及給觀察者發送通知。第二種強制退出Run Loop主要是應對無條件啟動Run Loop的情況。第三種方式是最不推薦的方式,雖然在理論上說當Run Loop中沒有任何數據源時會立即退出,但是在實際情況中我們創建的二級線程除了執行我們指定的任務外,有可能系統還會讓其執行一些系統層面的任務,而且這些任務我們一般無法知曉,所以用這種方式退出Run Loop往往會存在延遲退出。
Run Loop對象的線程安全性
Run Loop對象的線程安全性取決于我們使用哪種API去操作。Core Foundation框架中的 CFRunLoop 對象是線程安全的,我們可以在任何線程中使用。Cocoa框架的 NSRunLoop 對象是線程不安全的,我們必須在擁有Run Loop的當前線程中操作Run Loop,如果操作了不屬于當前線程的Run loop,會導致異常和各種潛在的問題發生。
自定義Run Loop事件源
Cocoa框架因為是較為高層的框架,所以沒有提供操作較為底層的Run Loop事件源相關的接口和對象,所以我們只能使用Core Foundation框架中的對象和函數創建事件源并給Run Loop設置事件源。
創建Run Loop事件源對象
我們定義自己的Run Loop事件源首先就是需要創建事件源,我們來看看創建事件源的方法:
func CFRunLoopSourceCreate(_ allocator: CFAllocator!, _ order: CFIndex, _ context: UnsafeMutablePointer<CFRunLoopSourceContext>) -> CFRunLoopSource!
</div>
- allocator :該參數為對象內存分配器,一般使用默認的分配器 kCFAllocatorDefault 。
- order :事件源優先級,當Run Loop中有多個接收相同事件的事件源被標記為待執行時,那么就根據該優先級判斷,0為最高優先級別。
- context :事件源上下文。
Run Loop事件源上下文很重要,我們來看看它的結構:
struct CFRunLoopSourceContext { var version: CFIndex var info: UnsafeMutablePointer<Void> var retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)! var release: ((UnsafePointer<Void>) -> Void)! var copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)! var equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)! var hash: ((UnsafePointer<Void>) -> CFHashCode)! var schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)! var cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)! var perform: ((UnsafeMutablePointer<Void>) -> Void)! init() init(version version: CFIndex, info info: UnsafeMutablePointer<Void>, retain retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)!, release release: ((UnsafePointer<Void>) -> Void)!, copyDescription copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)!, equal equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)!, hash hash: ((UnsafePointer<Void>) -> CFHashCode)!, schedule schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, cancel cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, perform perform: ((UnsafeMutablePointer<Void>) -> Void)!) }
</div>
該結構體中我們需要關注的是前兩個和后三個屬性:
- version :事件源上下文的版本,必須設置為0。
- info :上下文中 retain 、 release 、 copyDescription 、 equal 、 hash 、 schedule 、 cancel 、 perform 這八個回調函數所有者對象的指針。
- schedule :該回調函數的作用是將該事件源與給它發送事件消息的線程進行關聯,也就是說如果主線程想要給該事件源發送事件消息,那么首先主線程得能獲取到該事件源。
- cancel :該回調函數的作用是使該事件源失效。
- perform :該回調函數的作用是執行其他線程或當前線程給該事件源發來的事件消息。
將事件源添加至Run Loop
事件源創建好之后,接下來就是將其添加到指定某個模式的Run Loop中,我們來看看這個方法:
func CFRunLoopAddSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFString!)
</div>
- rl :希望添加事件源的Run Loop對象,類型是 CFRunLoop 。
- source :我們創建好的事件源。
- mode :Run Loop的模式。(可以回顧之前文章)
我們再來看看這個方法都干了些什么:
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { ..... __CFRunLoopSourceSchedule(rls, rl, rlm); ..... } static void __CFRunLoopSourceSchedule(CFRunLoopSourceRef rls, CFRunLoopRef rl, CFRunLoopModeRef rlm) { ..... if (0 == rls->_context.version0.version) { if (NULL != rls->_context.version0.schedule) { rls->_context.version0.schedule(rls->_context.version0.info, rl, rlm->_name); } } ..... }
</div>
從上述的代碼片段可以看出,在 CFRunLoopAddSource 中調用了 __CFRunLoopSourceSchedule 內部函數,而該函數中正是執行了Run Loop事件源上下文中的 schedule 回調函數。也就是說當把事件源添加到Run Loop中后就會將事件源與給它發送事件消息的線程進行關聯。
標記事件源及喚醒Run Loop
前面的文章中說過,srouce0類型,也就是非port類型的事件源都需要進行手動標記,標記完還需要手動喚醒Run Loop,下面我們來看看這兩個方法:
func CFRunLoopSourceSignal(_ source: CFRunLoopSource!) func CFRunLoopWakeUp(_ rl: CFRunLoop!)
</div>
這里需要注意的是喚醒Run Loop并不等價與啟動Run Loop,因為啟動Run Loop時需要對Run Loop進行模式、時限的設置,而喚醒Run Loop只是當已啟動的Run Loop休眠時重新讓其運行。
執行Run Loop事件源的任務
喚醒Run Loop意味著讓休眠的Run Loop重新運行,那么我們就從啟動Run Loop,讓其開始運行的方法看起:
extension NSRunLoop { ..... public func runUntilDate(limitDate: NSDate) { while runMode(NSDefaultRunLoopMode, beforeDate: limitDate) && limitDate.timeIntervalSinceReferenceDate > CFAbsoluteTimeGetCurrent() { } } public func runMode(mode: String, beforeDate limitDate: NSDate) -> Bool { ..... let limitTime = limitDate.timeIntervalSinceReferenceDate let ti = limitTime - CFAbsoluteTimeGetCurrent() CFRunLoopRunInMode(modeArg, ti, true) return true } } SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { CHECK_FOR_FORK(); return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); } SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { ..... result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, false); ..... return result; } static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, Boolean waitIfEmpty) { ..... __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); ..... } static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) { CFTypeRef sources = NULL; ..... if (__CFRunLoopSourceIsSignaled(rls)) { ..... rls->_context.version0.perform(rls->_context.version0.info); ..... } ..... }
</div>
從上述代碼片段中可以看出,當Run Loop運行后會調用內部函數 __CFRunLoopDoSources0 執行自定義事件源的任務,在執行之前會通過內部函數 __CFRunLoopSourceIsSignaled(rls) 判斷事件源是否已被標記為待執行,然后執行Run Loop事件上下文中的 perform 回調函數。
移除Run Loop事件源
當我們自定義的事件源完成使命后就可以將其從Run Loop中移除,我們來看看對應的方法:
func CFRunLoopRemoveSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFString!) void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { ..... __CFRunLoopSourceCancel(rls, rl, rlm); ..... } static void __CFRunLoopSourceCancel(CFRunLoopSourceRef rls, CFRunLoopRef rl, CFRunLoopModeRef rlm) { if (0 == rls->_context.version0.version) { if (NULL != rls->_context.version0.cancel) { rls->_context.version0.cancel(rls->_context.version0.info, rl, rlm->_name); } } ..... }
</div>
從上述代碼片段可以看出,當我們調用了 CFRunLoopRemoveSource 方法后,其實是執行了Run Loop事件源上下文中的 cancel 回調函數。
自定義Run Loop事件源的實際運用
在講解示例之前,我們先來看看示例Demo的效果:
在這個示例中,創建了兩個自定義事件源,一個添加到主線程中,另一個添加到二級線程中。主線程給二級線程中的自定義事件源發送事件消息,目的是讓其改變所有 UICollectionViewCell 的透明度,當二級線程收到事件消息后執行計算每個 UICollectionViewCell 透明度的任務,然后再給主線程的自定義事件源發送事件消息,讓其更新 UICollectionViewCell 的透明度并顯示。下面來看看類圖:
整個工程一共就這六個類:
- MainCollectionViewController :程序主控制器,啟動程序、展示UI及計算 UICollectionViewCell 透明度的相關方法。
- MainThreadRunLoopSource :主線程自定義事件源管理對象,負責初始化事件源,將事件源添加至指定線程,標記事件源并喚醒指定Run Loop以及包含上文中說過的事件源最主要的三個回調方法。
- MainThreadRunLoopSourceContext :主線程自定義事件源上下文,可獲取到對應的事件源及添加了該事件源的Run Loop。
- SecondaryThreadRunLoopSource :二級線程自定義事件源管理對象,負責初始化事件源,將事件源添加至指定線程,標記事件源并喚醒指定Run Loop以及包含上文中說過的事件源最主要的三個回調方法。
- SecondaryThreadRunLoopSourceContext :二級線程自定義事件源上下文,可獲取到對應的事件源及添加了該事件源的Run Loop。
- AppDelegate :應用程序代理類,這里零時充當為各自定義事件源回調方法執行內容的管理類。
下面我按照程序的運行順序一一對這些類及屬性和方法進行簡單說明。
程序開始運行
MainCollectionViewController 類中與UI展示相關的方法在這里就不再累贅了。點擊 Start 按鈕,調用 start() 方法,初始化 MainThreadRunLoopSource 對象,在這個過程中初始化了 CFRunLoopSourceContext 對象并且創建 CFRunLoopSource 對象以及初始化該事件源的指令池:
let mainThreadRunLoopSource = MainThreadRunLoopSource() mainThreadRunLoopSource.addToCurrentRunLoop()
</div>
var runloopSourceContext = CFRunLoopSourceContext(version: 0, info: unsafeBitCast(self, UnsafeMutablePointer<Void>.self), retain: nil, release: nil, copyDescription: nil, equal: nil, hash: nil, schedule: runloopSourceScheduleRoutine(), cancel: runloopSourceCancelRoutine(), perform: runloopSourcePerformRoutine()) runloopSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &runloopSourceContext) commandBuffer = Array<SecondaryThreadRunLoopSourceContext>()
</div>
這里需要注意的是 CFRunLoopSourceContext 的 init 方法中的第二個參數和 CFRunLoopSourceCreate 方法的第三個參數都是指針,那么在Swift中,將對象轉換為指針的方法有兩種:
- 使用 unsafeBitCast 方法,該方法會將第一個參數的內容按照第二個參數的類型進行轉換。一般當需要對象與指針來回轉換時使用該方法。
- 在對象前面加 & 符號,表示傳入指針地址。
當主線程的自定義事件源初始化完成之后,調用 addToCurrentRunLoop() 方法,將事件源添加至當前Run Loop中,即主線程的Run Loop:
let cfrunloop = CFRunLoopGetCurrent() if let rls = runloopSource { CFRunLoopAddSource(cfrunloop, rls, kCFRunLoopDefaultMode) }
</div>
接下來創建二級線程,并且讓其執行二級線程的配置任務:
let secondaryThread = NSThread(target: self, selector: "startThreadWithRunloop", object: nil) secondaryThread.start()
</div>
在二級線程中同樣初始化自定義事件源,并將將其添加至二級線程的Run Loop中,然后啟動Run Loop:
func startThreadWithRunloop() { autoreleasepool{ var done = false let secondaryThreadRunLoopSource = SecondaryThreadRunLoopSource() secondaryThreadRunLoopSource.addToCurrentRunLoop() repeat { let result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 5, true) if ((result == CFRunLoopRunResult.Stopped) || (result == CFRunLoopRunResult.Finished)) { done = true; } } while(!done) } }
</div>
執行事件源的schedule回調函數
前文中說過將事件源添加至Run Loop后會觸發事件源的 schedule 回調函數,所以當執行完 mainThreadRunLoopSource.addToCurrentRunLoop() 這句代碼后,便會觸發主線程自定義事件源的 schedule 回調函數:
func runloopSourceScheduleRoutine() -> @convention(c) (UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void { return { (info, runloop, runloopMode) -> Void in let mainThreadRunloopSource = unsafeBitCast(info, MainThreadRunLoopSource.self) let mainThreadRunloopSourceContext = MainThreadRunLoopSourceContext(runloop: runloop, runloopSource: mainThreadRunloopSource) let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate appDelegate.performSelector("registerMainThreadRunLoopSource:", withObject: mainThreadRunloopSourceContext) } }
</div>
這里還需注意的是在Swift2.0中,如果一個作為回調函數方法的返回類型是指向函數的指針,這類指針可以轉換為閉包,并且要在閉包前面加上 @convention(c) 標注。在 runloopSourceScheduleRoutine() 方法中,獲取到主線程事件源對象并初始化事件源上下文對象,然后將該事件源上下文對象傳給 AppDelegate 的對應方法注冊該事件源上下文對象:
func registerMainThreadRunLoopSource(runloopSourceContext: MainThreadRunLoopSourceContext) { mainThreadRunloopSourceContext = runloopSourceContext }
</div>
自然當在二級線程中執行完 secondaryThreadRunLoopSource.addToCurrentRunLoop() 這句代碼后,也會觸發二級線程自定義事件源的 schedule 回調函數:
func runloopSourceScheduleRoutine() -> @convention(c) (UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void { return { (info, runloop, runloopMode) -> Void in let secondaryThreadRunloopSource = unsafeBitCast(info, SecondaryThreadRunLoopSource.self) let secondaryThreadRunloopSourceContext = SecondaryThreadRunLoopSourceContext(runloop: runloop, runloopSource: secondaryThreadRunloopSource) let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate appDelegate.performSelectorOnMainThread("registerSecondaryThreadRunLoopSource:", withObject: secondaryThreadRunloopSourceContext, waitUntilDone: true) } }
</div>
這里要注意的是,在該方法中同樣是將二級線程事件源上下文對象傳給了 AppDelegate 的對應方法,但是這里用了 performSelectorOnMainThread 方法,讓其在主線程中執行,目的在于注冊完上下文對象后就接著從主線程給二級線程發送事件消息了,其實我將這里作為了主線程觸發二級線程執行任務的觸發點:
func registerSecondaryThreadRunLoopSource(runloopSourceContext: SecondaryThreadRunLoopSourceContext) { secondaryThreadRunloopSourceContext = runloopSourceContext sendCommandToSecondaryThread() } func sendCommandToSecondaryThread() { secondaryThreadRunloopSourceContext?.runloopSource?.commandBuffer?.append(mainThreadRunloopSourceContext!) secondaryThreadRunloopSourceContext?.runloopSource?.signalSourceAndWakeUpRunloop(secondaryThreadRunloopSourceContext!.runloop!) }
</div>
從上述代碼中可以看到在 sendCommandToSecondaryThread() 方法中,將主線程的事件源上下文放入了二級線程事件源的指令池中,這里我設計的是只要指令池中有內容就代表事件源需要執行后續任務了。然后執行了二級線程事件源的 signalSourceAndWakeUpRunloop() 方法,給其標記為待執行,并喚醒二級線程的Run Loop:
func signalSourceAndWakeUpRunloop(runloop: CFRunLoopRef) { CFRunLoopSourceSignal(runloopSource) CFRunLoopWakeUp(runloop) }
</div>
執行事件源的perform回調函數
當二級線程事件源被標記并且二級線程Run Loop被喚醒后,就會觸發事件源的 perform 回調函數:
func runloopSourcePerformRoutine() -> @convention(c) (UnsafeMutablePointer<Void>) -> Void { return { info -> Void in let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate appDelegate.performSelector("performSecondaryThreadRunLoopSourceTask") } }
</div>
二級線程事件源的 perform 回調函數會在當前線程,也就是二級線程中執行 AppDelegate 中的對應方法:
func performSecondaryThreadRunLoopSourceTask() { if secondaryThreadRunloopSourceContext!.runloopSource!.commandBuffer!.count > 0 { mainCollectionViewController!.generateRandomAlpha() let mainThreadRunloopSourceContext = secondaryThreadRunloopSourceContext!.runloopSource!.commandBuffer![0] secondaryThreadRunloopSourceContext!.runloopSource!.commandBuffer!.removeAll() mainThreadRunloopSourceContext.runloopSource?.commandBuffer?.append(secondaryThreadRunloopSourceContext!) mainThreadRunloopSourceContext.runloopSource?.signalSourceAndWakeUpRunloop(mainThreadRunloopSourceContext.runloop!) } }
</div>
從上述代碼中可以看到,先會判斷二級線程事件源的指令池中有沒有內容,如果有的話,那么執行計算 UICollectionViewCell 透明度的任務,然后從指令池中獲取到主線程事件源上下文對象,將二級線程事件源上下文對象放入主線程事件源的指令池中,并將主線程事件源標記為待執行,然后喚醒主線程Run Loop。之后便會觸發主線程事件源的 perform 回調函數:
func runloopSourcePerformRoutine() -> @convention(c) (UnsafeMutablePointer<Void>) -> Void { return { info -> Void in let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate appDelegate.performSelector("performMainThreadRunLoopSourceTask") } }
</div>
func performMainThreadRunLoopSourceTask() { if mainThreadRunloopSourceContext!.runloopSource!.commandBuffer!.count > 0 { mainThreadRunloopSourceContext!.runloopSource!.commandBuffer!.removeAll() mainCollectionViewController!.collectionView.reloadData() let timer = NSTimer(timeInterval: 1, target: self, selector: "sendCommandToSecondaryThread", userInfo: nil, repeats: false) NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode) } }
</div>
在 performMainThreadRunLoopSourceTask() 方法中同樣會先判斷主線程事件源的指令池是否有內容,然后執行 MainCollectionViewController 中的刷新UI的方法,最后再次給二級線程發送事件消息,以此循環。大家可以去Github下載該示例的 源碼 ,編譯環境是Xcode7.2,然后可以自己試著在界面中添加一個 Stop 按鈕,讓事件源執行 cancel 回調函數。