本文首發(fā)CSDN惑灵,如需轉(zhuǎn)載請(qǐng)與CSDN聯(lián)系山上。
記得第一次讀這個(gè)文檔還是3年前,那時(shí)也只是泛讀英支。如今關(guān)于iOS多線程的文章層出不窮佩憾,但我覺得若想更好的領(lǐng)會(huì)各個(gè)實(shí)踐者的文章,應(yīng)該先仔細(xì)讀讀官方的相關(guān)文檔干花,打好基礎(chǔ)妄帘,定會(huì)有更好的效果。文章中有對(duì)官方文檔的翻譯池凄,也有自己的理解寄摆,官方文檔中代碼片段的示例在這篇文章中都進(jìn)行了完整的重寫,還有一些文檔中沒有的代碼示例修赞,并且都使用Swift完成婶恼,給大家一些Objc與Swift轉(zhuǎn)換的參考。
官方文檔地址:Threading Programming Guide
何時(shí)使用Run Loop
前文中多次提到過柏副,在主線程中Run Loop是隨著應(yīng)用程序一起啟動(dòng)的勾邦,也就是說當(dāng)我們打開一個(gè)應(yīng)用時(shí),主線程中的Run Loop就已經(jīng)啟動(dòng)了割择,尤其現(xiàn)在我們都使用Xcode中的項(xiàng)目模版創(chuàng)建項(xiàng)目眷篇,更是不用考慮主線程中Run Loop的狀體。所以只有在二級(jí)線程中荔泳,也就是我們自己創(chuàng)建的線程中才有機(jī)會(huì)手動(dòng)的創(chuàng)建的Run Loop蕉饼,并對(duì)其進(jìn)行配置的操作。
在前文中還提到過玛歌,Run Loop在線程中的主要作用就是幫助線程常駐在進(jìn)程中昧港,并且不會(huì)過多消耗資源。所以說Run Loop在二級(jí)線程中也不是必須需要的支子,要根據(jù)該線程執(zhí)行的任務(wù)類型以及在整個(gè)應(yīng)用中擔(dān)任何作用而決定是否需要使用Run Loop创肥。比如說,如果你創(chuàng)建一個(gè)二級(jí)線程只是為了執(zhí)行一個(gè)不會(huì)頻繁執(zhí)行的一次性任務(wù),或者需要執(zhí)行很長時(shí)間的任務(wù)叹侄,那么可能就不需要使用Run Loop了巩搏。如果你需要一個(gè)線程執(zhí)行周期性的定時(shí)任務(wù),或者需要較為頻繁的與主線程之間進(jìn)行交互趾代,那么就需要使用Run Loop贯底。歸納一下需要使用Run Loop的情況大概有以下四點(diǎn):
- 通過基于端口或自定義的數(shù)據(jù)源與其他線程進(jìn)行交互。
- 在線程中執(zhí)行定時(shí)事件源的任務(wù)撒强。
- 使用Cocoa框架提供的
performSelector…
系列方法禽捆。 - 在線程中執(zhí)行較為頻繁的,具有周期性的任務(wù)尿褪。
光說不練假把式睦擂,下面就讓我們來看看如何具體創(chuàng)建得湘、配置杖玲、操作Run Loop。
Run Loop對(duì)象
要想操作配置Run Loop淘正,那自然需要通過Run Loop對(duì)象來完成摆马,它提供了一系列接口,可幫助我們便捷的添加Input sources鸿吆、timers以及觀察者囤采。較高級(jí)別的Cocoa框架提供了NSRunLoop
類,較底層級(jí)別的Core Foundation框架提供了指向CFRunloopRef
的指針惩淳。
獲取Run Loop對(duì)象
前文中提到過蕉毯,在Cocoa和Core Foundation框架中都沒有提供創(chuàng)建Run Loop的方法,只有從當(dāng)前線程獲取Run Loop的方法:
- 在Cocoa框架中思犁,
NSRunLoop
類提供了類方法currentRunLoop()
獲取NSRunLoop
對(duì)象代虾。該方法是獲取當(dāng)前線程中已存在的Run Loop,如果不存在激蹲,那其實(shí)還是會(huì)創(chuàng)建一個(gè)Run Loop對(duì)象返回棉磨,只是Cocoa框架沒有向我們暴露該接口。
- 在Core Foundation框架中提供了
CFRunLoopGetCurrent()
函數(shù)獲取CFRunLoop
對(duì)象学辱。
雖然這兩個(gè)Run Loop對(duì)象并不完全等價(jià)乘瓤,它們之間還是可以轉(zhuǎn)換的,我們可以通過NSRunLoop
對(duì)象提供的getCFRunLoop()
方法獲取CFRunLoop
對(duì)象策泣。因?yàn)?code>NSRunLoop和CFRunLoop
指向的都是當(dāng)前線程中同一個(gè)Run Loop衙傀,所以在使用時(shí)它們可以混用,比如說要給Run Loop添加觀察者時(shí)就必須得用CFRunLoop
了萨咕。
配置Run Loop觀察者
前文中提到過差油,可以向Run Loop中添加各種事件源和觀察者,這里事件源是必填項(xiàng),也就是說Run Loop中至少要有一種事件源蓄喇,不論是Input source還是timer发侵,如果Run Loop中沒有事件源的話妆偏,那么在啟動(dòng)Run Loop后就會(huì)立即退出叔锐。而觀察者是可選項(xiàng),如果沒有監(jiān)控Run Loop各運(yùn)行狀態(tài)的需求步责,可以不配置觀察者,這一節(jié)先看看如何向Run Loop中添加觀察者蔗包。
在Cocoa框架中,并沒有提供創(chuàng)建配置Run Loop觀察者的相關(guān)接口耻矮,所以我們只能通過Core Foundation框架中提供的對(duì)象和方法創(chuàng)建并配置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已經(jīng)啟動(dòng)")
break
case CFRunLoopActivity.BeforeTimers:
print("Run Loop分配定時(shí)任務(wù)前")
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()
下面解讀一下上述代碼示例毡琉,launch()
方法在主線程中慧耍,通過NSThread
類的類方法detachNewThreadSelector:toTarget:withObject:
創(chuàng)建并啟動(dòng)一個(gè)二級(jí)線程煌珊,將createAndConfigObserverInSecondaryThread()
方法作為事件消息傳入該二級(jí)線程,這個(gè)方法的主要作用就是在二級(jí)線程中創(chuàng)建配置Run Loop觀察者并啟動(dòng)Run Loop,然后讓主線程持續(xù)3秒畴博,以便二級(jí)線程有足夠的時(shí)間執(zhí)行任務(wù)俱病。
在createAndConfigObserverInSecondaryThread()
中共有8個(gè)關(guān)鍵步驟庶艾,下面一一進(jìn)行說明:
-
第一步:通過
NSRunLoop
類的類方法currentRunLoop()
獲取當(dāng)前線程的Run Loop袁余,這里獲取到的Run Loop對(duì)象是NSRunLoop
對(duì)象。 - 第二步:申明當(dāng)前對(duì)象的變量颖榜,至于為什么要這么做,在下一步中會(huì)有說明掩完。
-
第三步:通過Core Foundation框架的
CFRunLoopObserverContext
結(jié)構(gòu)體構(gòu)造Run Loop觀察者上下文,大家需要注意前兩個(gè)參數(shù)且蓬,我們先看看這個(gè)結(jié)構(gòu)體:
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>!)!)
}
-
version
:結(jié)構(gòu)體版本號(hào)欣硼,必須設(shè)置為0恶阴。 -
info
:上下文中retain
焦匈、release
、copyDescription
三個(gè)回調(diào)函數(shù)以及Run Loop觀察者的回調(diào)函數(shù)所有者對(duì)象的指針垦写。在Swift中彰触,UnsafePointer
結(jié)構(gòu)體代表C系語言中申明為常量的指針梯澜,UnsafeMutablePoinger
結(jié)構(gòu)體代表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>)
-
第四步:通過Core Foundation框架的
CFRunLoopObserverCreate
函數(shù)創(chuàng)建CFRunLoopObserver
對(duì)象:
public func CFRunLoopObserverCreate(allocator: CFAllocator!, _ activities: CFOptionFlags, _ repeats: Bool, _ order: CFIndex, _ callout: CFRunLoopObserverCallBack!, _ context: UnsafeMutablePointer<CFRunLoopObserverContext>) -> CFRunLoopObserver!
-
allocator
:該參數(shù)為對(duì)象內(nèi)存分配器渴析,一般使用默認(rèn)的分配器kCFAllocatorDefault
晚伙。 -
activities
:該參數(shù)配置觀察者監(jiān)聽Run Loop的哪種運(yùn)行狀態(tài)。在示例中俭茧,我們讓觀察者監(jiān)聽Run Loop的所有運(yùn)行狀態(tài)咆疗。 -
repeats
:該參數(shù)標(biāo)識(shí)觀察者只監(jiān)聽一次還是每次Run Loop運(yùn)行時(shí)都監(jiān)聽。 -
order
:觀察者優(yōu)先級(jí)母债,當(dāng)Run Loop中有多個(gè)觀察者監(jiān)聽同一個(gè)運(yùn)行狀態(tài)時(shí)午磁,那么就根據(jù)該優(yōu)先級(jí)判斷,0為最高優(yōu)先級(jí)別毡们。 -
callout
:觀察者的回調(diào)函數(shù)迅皇,在Core Foundation框架中用CFRunLoopObserverCallBack
重定義了回調(diào)函數(shù)的閉包。 -
context
:觀察者的上下文衙熔。
-
第五步:因?yàn)?code>NSRunLoop沒有提供操作觀察者的接口登颓,所以我們需要
getCFRunLoop()
方法獲取到CFRunLoop
對(duì)象。 -
第六步:通過
CFRunLoopAddObserver
函數(shù)向當(dāng)前線程的Run Loop中添加創(chuàng)建好的觀察者:
func CFRunLoopAddObserver(_ rl: CFRunLoop!, _ observer: CFRunLoopObserver!, _ mode: CFString!)
-
rl
:當(dāng)前線程的CFRunLoop
對(duì)象红氯。 -
observer
:創(chuàng)建好的觀察者框咙。 -
mode
:設(shè)置將觀察者添加到哪個(gè)Run Loop模式中。
這里需要注意的是痢甘,一個(gè)觀察者只能被添加到一個(gè)Run Loop中喇嘱,但是可以被添加到Run Loop中的多個(gè)模式中。
-
第七步:通過Timer事件源向當(dāng)前線程發(fā)送重復(fù)執(zhí)行的定時(shí)任務(wù)塞栅,時(shí)間間隔為0.5秒者铜,因?yàn)橹皇菫榱藴y(cè)試觀察者,所以
fireTimer()
是一個(gè)空任務(wù)放椰。另外前文中提到過作烟,如果Run Loop中沒有任何數(shù)據(jù)源,那么Run Loop啟動(dòng)后會(huì)立即退出庄敛,所以大家可以把這行注釋了運(yùn)行看看會(huì)有什么效果俗壹。 -
第八步:通過
NSRunLoop
對(duì)象的runUntilDate(limitDate: NSDate)
方法啟動(dòng)Run Loop,設(shè)置Run Loop的運(yùn)行時(shí)長為1秒藻烤。這里將其放在一個(gè)循環(huán)里绷雏,最大循環(huán)次數(shù)為10次头滔,也就是說,如果不考慮主線程的運(yùn)行時(shí)間涎显,該二級(jí)線程的Run Loop可運(yùn)行10次坤检。
再來看看觀察者的回調(diào)方法observerCallbackFunc()
,上面在介紹CFRunLoopObserverCreate
函數(shù)時(shí)提到觀察者的回調(diào)函數(shù)是CFRunLoopObserverCallBack
重定義的一個(gè)閉包期吓,我們來看看這個(gè)閉包:
typealias CFRunLoopObserverCallBack = (CFRunLoopObserver!, CFRunLoopActivity, UnsafeMutablePointer<Void>) -> Void
這個(gè)閉包沒有返回值早歇,第一個(gè)參數(shù)是觸發(fā)監(jiān)聽的觀察者,第二個(gè)參數(shù)是觀察者監(jiān)聽的Run Loop運(yùn)行狀態(tài)讨勤,第三個(gè)參數(shù)是觀察者的運(yùn)行上下文環(huán)境箭跳。所以在回調(diào)方法中,我們只需要根據(jù)第二個(gè)參數(shù)的值即可判斷觀察者監(jiān)聽到的Run Loop狀態(tài)潭千。大家可以拷貝上面的代碼谱姓,建一個(gè)Command Application運(yùn)行看看結(jié)果。
啟動(dòng)Run Loop
在啟動(dòng)Run Loop前務(wù)必要保證已添加一種類型的事件源刨晴,原因在前文中已提到多次屉来。在Cocoa框架和Core Foundation框架中啟動(dòng)Run Loop大體有三種形式,分別是無條件啟動(dòng)狈癞、設(shè)置時(shí)間限制啟動(dòng)茄靠、指定特定模式啟動(dòng)。
無條件啟動(dòng)
NSRunLoop
對(duì)象的run()
方法和Core Foundation框架中的CFRunLoopRun()
函數(shù)都是無條件啟動(dòng)Run Loop的方式蝶桶。這種方式雖然是最簡單的啟動(dòng)方式慨绳,但也是最不推薦使用的一個(gè)方式,因?yàn)檫@種方式將Run Loop置于一個(gè)永久運(yùn)行并且不可控的狀態(tài)莫瞬,它使Run Loop只能在默認(rèn)模式下運(yùn)行儡蔓,無法給Run Loop設(shè)置特定的或自定義的模式郭蕉,而且以這種模式啟動(dòng)的Run Loop只能通過CFRunLoopStop(_ rl: CFRunLoop!)
函數(shù)強(qiáng)制停止疼邀。
設(shè)置時(shí)間限制啟動(dòng)
該方式對(duì)應(yīng)的方法是NSRunLoop
對(duì)象的runUntilDate(_ limitDate: NSDate)
方法,在啟動(dòng)Run Loop時(shí)設(shè)置超時(shí)時(shí)間召锈,一旦超時(shí)那么Run Loop則自動(dòng)退出旁振。該方法的好處是可以在循環(huán)中反復(fù)啟動(dòng)Run Loop處理相關(guān)任務(wù),而且可控制運(yùn)行時(shí)長涨岁。
指定特定模式啟動(dòng)
該方式對(duì)應(yīng)的方法是NSRunLoop
對(duì)象的runMode(_ mode: String, beforeDate limitDate: NSDate)
方法和Core Foundation框架的CFRunLoopRunInMode(_ mode: CFString!, _ seconds: CFTimeInterval, _ returnAfterSourceHandled: Bool)
函數(shù)拐袜。前者有兩個(gè)參數(shù),第一個(gè)參數(shù)是Run Loop模式梢薪,第二個(gè)參數(shù)仍然是超時(shí)時(shí)間蹬铺,該方法使Run Loop只處理指定模式中的事件源事件,當(dāng)處理完事件或超時(shí)Run Loop會(huì)退出秉撇,該方法的返回值類型是Bool
甜攀,如果返回true
則表示Run Loop啟動(dòng)成功秋泄,并分派執(zhí)行了任務(wù)或者達(dá)到超時(shí)時(shí)間,若返回false
則表示Run Loop啟動(dòng)失敗规阀。后者有三個(gè)參數(shù)恒序,前兩個(gè)參數(shù)的作用一樣,第三個(gè)參數(shù)的意思是Run Loop是否在執(zhí)行完任務(wù)后就退出谁撼,如果設(shè)置為false
歧胁,那么代表Run Loop在執(zhí)行完任務(wù)后不退出,而是一直等到超時(shí)后才退出厉碟。該方法返回Run Loop的退出狀態(tài):
-
CFRunLoopRunResult.Finished
:表示Run Loop已分派執(zhí)行完任務(wù)喊巍,并且再無任務(wù)執(zhí)行的情況下退出。 -
CFRunLoopRunResult.Stopped
:表示Run Loop通過CFRunLoopStop(_ rl: CFRunLoop!)
函數(shù)強(qiáng)制退出箍鼓。 -
CFRunLoopRunResult.TimedOut
:表示Run Loop因?yàn)槌瑫r(shí)時(shí)間到而退出玄糟。 -
CFRunLoopRunResult.HandledSource
:表示Run Loop已執(zhí)行完任務(wù)而退出,改狀態(tài)只有在returnAfterSourceHandled
設(shè)置為true
時(shí)才會(huì)出現(xiàn)袄秩。
退出Run Loop
退出Run Loop的方式總體來說有三種:
- 啟動(dòng)Run Loop時(shí)設(shè)置超時(shí)時(shí)間阵翎。
- 強(qiáng)制退出Run Loop。
- 移除Run Loop中的事件源之剧,從而使Run Loop退出郭卫。
第一種方式是推薦使用的方式,因?yàn)榭梢越oRun Loop設(shè)置可控的運(yùn)行時(shí)間背稼,讓它執(zhí)行完所有的任務(wù)以及給觀察者發(fā)送通知贰军。第二種強(qiáng)制退出Run Loop主要是應(yīng)對(duì)無條件啟動(dòng)Run Loop的情況。第三種方式是最不推薦的方式蟹肘,雖然在理論上說當(dāng)Run Loop中沒有任何數(shù)據(jù)源時(shí)會(huì)立即退出词疼,但是在實(shí)際情況中我們創(chuàng)建的二級(jí)線程除了執(zhí)行我們指定的任務(wù)外,有可能系統(tǒng)還會(huì)讓其執(zhí)行一些系統(tǒng)層面的任務(wù)帘腹,而且這些任務(wù)我們一般無法知曉贰盗,所以用這種方式退出Run Loop往往會(huì)存在延遲退出。
Run Loop對(duì)象的線程安全性
Run Loop對(duì)象的線程安全性取決于我們使用哪種API去操作阳欲。Core Foundation框架中的CFRunLoop
對(duì)象是線程安全的舵盈,我們可以在任何線程中使用。Cocoa框架的NSRunLoop
對(duì)象是線程不安全的球化,我們必須在擁有Run Loop的當(dāng)前線程中操作Run Loop秽晚,如果操作了不屬于當(dāng)前線程的Run loop,會(huì)導(dǎo)致異常和各種潛在的問題發(fā)生筒愚。
自定義Run Loop事件源
Cocoa框架因?yàn)槭禽^為高層的框架赴蝇,所以沒有提供操作較為底層的Run Loop事件源相關(guān)的接口和對(duì)象,所以我們只能使用Core Foundation框架中的對(duì)象和函數(shù)創(chuàng)建事件源并給Run Loop設(shè)置事件源巢掺。
創(chuàng)建Run Loop事件源對(duì)象
我們定義自己的Run Loop事件源首先就是需要?jiǎng)?chuàng)建事件源句伶,我們來看看創(chuàng)建事件源的方法:
func CFRunLoopSourceCreate(_ allocator: CFAllocator!, _ order: CFIndex, _ context: UnsafeMutablePointer<CFRunLoopSourceContext>) -> CFRunLoopSource!
-
allocator
:該參數(shù)為對(duì)象內(nèi)存分配器芍耘,一般使用默認(rèn)的分配器kCFAllocatorDefault
。 -
order
:事件源優(yōu)先級(jí)熄阻,當(dāng)Run Loop中有多個(gè)接收相同事件的事件源被標(biāo)記為待執(zhí)行時(shí)斋竞,那么就根據(jù)該優(yōu)先級(jí)判斷,0為最高優(yōu)先級(jí)別秃殉。 -
context
:事件源上下文坝初。
Run Loop事件源上下文很重要,我們來看看它的結(jié)構(gòu):
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)!)
}
該結(jié)構(gòu)體中我們需要關(guān)注的是前兩個(gè)和后三個(gè)屬性:
-
version
:事件源上下文的版本钾军,必須設(shè)置為0鳄袍。 -
info
:上下文中retain
、release
吏恭、copyDescription
拗小、equal
、hash
樱哼、schedule
哀九、cancel
、perform
這八個(gè)回調(diào)函數(shù)所有者對(duì)象的指針搅幅。 -
schedule
:該回調(diào)函數(shù)的作用是將該事件源與給它發(fā)送事件消息的線程進(jìn)行關(guān)聯(lián)阅束,也就是說如果主線程想要給該事件源發(fā)送事件消息,那么首先主線程得能獲取到該事件源茄唐。 -
cancel
:該回調(diào)函數(shù)的作用是使該事件源失效息裸。 -
perform
:該回調(diào)函數(shù)的作用是執(zhí)行其他線程或當(dāng)前線程給該事件源發(fā)來的事件消息。
將事件源添加至Run Loop
事件源創(chuàng)建好之后沪编,接下來就是將其添加到指定某個(gè)模式的Run Loop中呼盆,我們來看看這個(gè)方法:
func CFRunLoopAddSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFString!)
-
rl
:希望添加事件源的Run Loop對(duì)象,類型是CFRunLoop
蚁廓。 -
source
:我們創(chuàng)建好的事件源访圃。 -
mode
:Run Loop的模式。(可以回顧之前文章)
我們?cè)賮砜纯催@個(gè)方法都干了些什么:
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);
}
}
.....
}
從上述的代碼片段可以看出纳令,在CFRunLoopAddSource
中調(diào)用了__CFRunLoopSourceSchedule
內(nèi)部函數(shù)挽荠,而該函數(shù)中正是執(zhí)行了Run Loop事件源上下文中的schedule
回調(diào)函數(shù)。也就是說當(dāng)把事件源添加到Run Loop中后就會(huì)將事件源與給它發(fā)送事件消息的線程進(jìn)行關(guān)聯(lián)平绩。
標(biāo)記事件源及喚醒Run Loop
前面的文章中說過,srouce0類型漠另,也就是非port類型的事件源都需要進(jìn)行手動(dòng)標(biāo)記捏雌,標(biāo)記完還需要手動(dòng)喚醒Run Loop,下面我們來看看這兩個(gè)方法:
func CFRunLoopSourceSignal(_ source: CFRunLoopSource!)
func CFRunLoopWakeUp(_ rl: CFRunLoop!)
這里需要注意的是喚醒Run Loop并不等價(jià)與啟動(dòng)Run Loop笆搓,因?yàn)閱?dòng)Run Loop時(shí)需要對(duì)Run Loop進(jìn)行模式性湿、時(shí)限的設(shè)置纬傲,而喚醒Run Loop只是當(dāng)已啟動(dòng)的Run Loop休眠時(shí)重新讓其運(yùn)行。
執(zhí)行Run Loop事件源的任務(wù)
喚醒Run Loop意味著讓休眠的Run Loop重新運(yùn)行肤频,那么我們就從啟動(dòng)Run Loop叹括,讓其開始運(yùn)行的方法看起:
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);
.....
}
.....
}
從上述代碼片段中可以看出,當(dāng)Run Loop運(yùn)行后會(huì)調(diào)用內(nèi)部函數(shù)__CFRunLoopDoSources0
執(zhí)行自定義事件源的任務(wù)宵荒,在執(zhí)行之前會(huì)通過內(nèi)部函數(shù)__CFRunLoopSourceIsSignaled(rls)
判斷事件源是否已被標(biāo)記為待執(zhí)行汁雷,然后執(zhí)行Run Loop事件上下文中的perform
回調(diào)函數(shù)。
移除Run Loop事件源
當(dāng)我們自定義的事件源完成使命后就可以將其從Run Loop中移除报咳,我們來看看對(duì)應(yīng)的方法:
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);
}
}
.....
}
從上述代碼片段可以看出侠讯,當(dāng)我們調(diào)用了CFRunLoopRemoveSource
方法后,其實(shí)是執(zhí)行了Run Loop事件源上下文中的cancel
回調(diào)函數(shù)暑刃。
自定義Run Loop事件源的實(shí)際運(yùn)用
在講解示例之前厢漩,我們先來看看示例Demo的效果:
在這個(gè)示例中,創(chuàng)建了兩個(gè)自定義事件源岩臣,一個(gè)添加到主線程中溜嗜,另一個(gè)添加到二級(jí)線程中。主線程給二級(jí)線程中的自定義事件源發(fā)送事件消息架谎,目的是讓其改變所有UICollectionViewCell
的透明度粱胜,當(dāng)二級(jí)線程收到事件消息后執(zhí)行計(jì)算每個(gè)UICollectionViewCell
透明度的任務(wù),然后再給主線程的自定義事件源發(fā)送事件消息狐树,讓其更新UICollectionViewCell
的透明度并顯示焙压。下面來看看類圖:
整個(gè)工程一共就這六個(gè)類:
-
MainCollectionViewController
:程序主控制器,啟動(dòng)程序抑钟、展示UI及計(jì)算UICollectionViewCell
透明度的相關(guān)方法涯曲。 -
MainThreadRunLoopSource
:主線程自定義事件源管理對(duì)象,負(fù)責(zé)初始化事件源在塔,將事件源添加至指定線程幻件,標(biāo)記事件源并喚醒指定Run Loop以及包含上文中說過的事件源最主要的三個(gè)回調(diào)方法。 -
MainThreadRunLoopSourceContext
:主線程自定義事件源上下文蛔溃,可獲取到對(duì)應(yīng)的事件源及添加了該事件源的Run Loop绰沥。 -
SecondaryThreadRunLoopSource
:二級(jí)線程自定義事件源管理對(duì)象,負(fù)責(zé)初始化事件源贺待,將事件源添加至指定線程徽曲,標(biāo)記事件源并喚醒指定Run Loop以及包含上文中說過的事件源最主要的三個(gè)回調(diào)方法。 -
SecondaryThreadRunLoopSourceContext
:二級(jí)線程自定義事件源上下文麸塞,可獲取到對(duì)應(yīng)的事件源及添加了該事件源的Run Loop秃臣。 -
AppDelegate
:應(yīng)用程序代理類,這里零時(shí)充當(dāng)為各自定義事件源回調(diào)方法執(zhí)行內(nèi)容的管理類。
下面我按照程序的運(yùn)行順序一一對(duì)這些類及屬性和方法進(jìn)行簡單說明奥此。
程序開始運(yùn)行
MainCollectionViewController
類中與UI展示相關(guān)的方法在這里就不再累贅了弧哎。點(diǎn)擊Start按鈕,調(diào)用start()
方法稚虎,初始化MainThreadRunLoopSource
對(duì)象撤嫩,在這個(gè)過程中初始化了CFRunLoopSourceContext
對(duì)象并且創(chuàng)建CFRunLoopSource
對(duì)象以及初始化該事件源的指令池:
let mainThreadRunLoopSource = MainThreadRunLoopSource()
mainThreadRunLoopSource.addToCurrentRunLoop()
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>()
這里需要注意的是CFRunLoopSourceContext
的init
方法中的第二個(gè)參數(shù)和CFRunLoopSourceCreate
方法的第三個(gè)參數(shù)都是指針,那么在Swift中蠢终,將對(duì)象轉(zhuǎn)換為指針的方法有兩種:
- 使用
unsafeBitCast
方法序攘,該方法會(huì)將第一個(gè)參數(shù)的內(nèi)容按照第二個(gè)參數(shù)的類型進(jìn)行轉(zhuǎn)換。一般當(dāng)需要對(duì)象與指針來回轉(zhuǎn)換時(shí)使用該方法蜕径。 - 在對(duì)象前面加
&
符號(hào)两踏,表示傳入指針地址。
當(dāng)主線程的自定義事件源初始化完成之后兜喻,調(diào)用addToCurrentRunLoop()
方法梦染,將事件源添加至當(dāng)前Run Loop中,即主線程的Run Loop:
let cfrunloop = CFRunLoopGetCurrent()
if let rls = runloopSource {
CFRunLoopAddSource(cfrunloop, rls, kCFRunLoopDefaultMode)
}
接下來創(chuàng)建二級(jí)線程朴皆,并且讓其執(zhí)行二級(jí)線程的配置任務(wù):
let secondaryThread = NSThread(target: self, selector: "startThreadWithRunloop", object: nil)
secondaryThread.start()
在二級(jí)線程中同樣初始化自定義事件源帕识,并將將其添加至二級(jí)線程的Run Loop中,然后啟動(dòng)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)
}
}
執(zhí)行事件源的schedule回調(diào)函數(shù)
前文中說過將事件源添加至Run Loop后會(huì)觸發(fā)事件源的schedule
回調(diào)函數(shù)遂铡,所以當(dāng)執(zhí)行完mainThreadRunLoopSource.addToCurrentRunLoop()
這句代碼后肮疗,便會(huì)觸發(fā)主線程自定義事件源的schedule
回調(diào)函數(shù):
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)
}
}
這里還需注意的是在Swift2.0中,如果一個(gè)作為回調(diào)函數(shù)方法的返回類型是指向函數(shù)的指針扒接,這類指針可以轉(zhuǎn)換為閉包伪货,并且要在閉包前面加上@convention(c)
標(biāo)注。在runloopSourceScheduleRoutine()
方法中钾怔,獲取到主線程事件源對(duì)象并初始化事件源上下文對(duì)象碱呼,然后將該事件源上下文對(duì)象傳給AppDelegate
的對(duì)應(yīng)方法注冊(cè)該事件源上下文對(duì)象:
func registerMainThreadRunLoopSource(runloopSourceContext: MainThreadRunLoopSourceContext) {
mainThreadRunloopSourceContext = runloopSourceContext
}
自然當(dāng)在二級(jí)線程中執(zhí)行完secondaryThreadRunLoopSource.addToCurrentRunLoop()
這句代碼后,也會(huì)觸發(fā)二級(jí)線程自定義事件源的schedule
回調(diào)函數(shù):
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)
}
}
這里要注意的是宗侦,在該方法中同樣是將二級(jí)線程事件源上下文對(duì)象傳給了AppDelegate
的對(duì)應(yīng)方法愚臀,但是這里用了performSelectorOnMainThread
方法,讓其在主線程中執(zhí)行矾利,目的在于注冊(cè)完上下文對(duì)象后就接著從主線程給二級(jí)線程發(fā)送事件消息了姑裂,其實(shí)我將這里作為了主線程觸發(fā)二級(jí)線程執(zhí)行任務(wù)的觸發(fā)點(diǎn):
func registerSecondaryThreadRunLoopSource(runloopSourceContext: SecondaryThreadRunLoopSourceContext) {
secondaryThreadRunloopSourceContext = runloopSourceContext
sendCommandToSecondaryThread()
}
func sendCommandToSecondaryThread() {
secondaryThreadRunloopSourceContext?.runloopSource?.commandBuffer?.append(mainThreadRunloopSourceContext!)
secondaryThreadRunloopSourceContext?.runloopSource?.signalSourceAndWakeUpRunloop(secondaryThreadRunloopSourceContext!.runloop!)
}
從上述代碼中可以看到在sendCommandToSecondaryThread()
方法中,將主線程的事件源上下文放入了二級(jí)線程事件源的指令池中男旗,這里我設(shè)計(jì)的是只要指令池中有內(nèi)容就代表事件源需要執(zhí)行后續(xù)任務(wù)了舶斧。然后執(zhí)行了二級(jí)線程事件源的signalSourceAndWakeUpRunloop()
方法,給其標(biāo)記為待執(zhí)行剑肯,并喚醒二級(jí)線程的Run Loop:
func signalSourceAndWakeUpRunloop(runloop: CFRunLoopRef) {
CFRunLoopSourceSignal(runloopSource)
CFRunLoopWakeUp(runloop)
}
執(zhí)行事件源的perform回調(diào)函數(shù)
當(dāng)二級(jí)線程事件源被標(biāo)記并且二級(jí)線程Run Loop被喚醒后捧毛,就會(huì)觸發(fā)事件源的perform
回調(diào)函數(shù):
func runloopSourcePerformRoutine() -> @convention(c) (UnsafeMutablePointer<Void>) -> Void {
return { info -> Void in
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.performSelector("performSecondaryThreadRunLoopSourceTask")
}
}
二級(jí)線程事件源的perform
回調(diào)函數(shù)會(huì)在當(dāng)前線程,也就是二級(jí)線程中執(zhí)行AppDelegate
中的對(duì)應(yīng)方法:
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!)
}
}
從上述代碼中可以看到让网,先會(huì)判斷二級(jí)線程事件源的指令池中有沒有內(nèi)容呀忧,如果有的話,那么執(zhí)行計(jì)算UICollectionViewCell
透明度的任務(wù)溃睹,然后從指令池中獲取到主線程事件源上下文對(duì)象而账,將二級(jí)線程事件源上下文對(duì)象放入主線程事件源的指令池中,并將主線程事件源標(biāo)記為待執(zhí)行因篇,然后喚醒主線程Run Loop泞辐。之后便會(huì)觸發(fā)主線程事件源的perform
回調(diào)函數(shù):
func runloopSourcePerformRoutine() -> @convention(c) (UnsafeMutablePointer<Void>) -> Void {
return { info -> Void in
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.performSelector("performMainThreadRunLoopSourceTask")
}
}
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)
}
}
在performMainThreadRunLoopSourceTask()
方法中同樣會(huì)先判斷主線程事件源的指令池是否有內(nèi)容,然后執(zhí)行MainCollectionViewController
中的刷新UI的方法竞滓,最后再次給二級(jí)線程發(fā)送事件消息咐吼,以此循環(huán)。大家可以去Github下載該示例的源碼商佑,編譯環(huán)境是Xcode7.2锯茄,然后可以自己試著在界面中添加一個(gè)Stop按鈕,讓事件源執(zhí)行cancel
回調(diào)函數(shù)茶没。