源起
四年前串远,在我強(qiáng)大的忽悠能力下,成功說服了領(lǐng)導(dǎo)使用 Swift 來開發(fā)新 APP芽卿。那時(shí)候自己也沒有多想太多揭芍,例如穩(wěn)定性問題胳搞、效率問題卸例、維護(hù)成本問題等等称杨。選擇 Swift 開發(fā)的理由,僅是懷著學(xué)習(xí)的心態(tài)筷转,以及對(duì) OC 反人類的語法感到厭惡姑原。也正因選擇了 Swift,這幾年來呜舒,我每年都得以學(xué)習(xí)一門新語言锭汛,它們分別為 Swift 2、Swift 3袭蝗、Swift 5…...
在這幾年時(shí)間了唤殴,我做兩件非常有意思的小事情,第一件是將 OC 的 APP 用 Swift 來重寫到腥,以跟進(jìn)時(shí)代的步伐朵逝;第二件是為了滿足公司對(duì)"熱"的要求,而將 Swift 的代碼改成 OC乡范。雖說這兩件微不足道的小事情并不是發(fā)生在同一家公司中配名,但卻感觸深刻,每每想起晋辆,總有那么一種沖動(dòng)渠脉,覺得自己需要寫點(diǎn)什么。
現(xiàn)在瓶佳,Swift 來到了 5.0 這個(gè)重要的里程碑上芋膘,作為一名 Swifter,希望自己能對(duì)這門語言有更深入的了解霸饲,同時(shí)嘗試給大家分享下自己對(duì)這門語言的一些經(jīng)驗(yàn)與見解索赏。在這個(gè)系列中,主要會(huì)針對(duì) Swift 的 "現(xiàn)代"贴彼、"安全"潜腻、"快" 三個(gè)方面,從源碼層去了解其具體的實(shí)現(xiàn)器仗,當(dāng)然在這個(gè)系列中還會(huì)包含 Swift 標(biāo)準(zhǔn)庫的應(yīng)用與實(shí)現(xiàn)進(jìn)行分析融涣,從底層角度了解 Swift 這門有那么一點(diǎn)意思的語言。
在立下這個(gè) flag 的同時(shí)精钮,也給自己挖了一個(gè)巨坑威鹿,這希望自己在填坑的過程中,可以與大家共同學(xué)習(xí)成長轨香,在 Swift 造詣上忽你,更上一層樓。
環(huán)境準(zhǔn)備
開發(fā)環(huán)境
masOS 10.14.5臂容,Xcode 10.3科雳,默認(rèn)使用當(dāng)下最新的正式版本根蟹。
編譯 Swift 源碼
我們可以在 GitHub 上的 Swift 倉庫 查看支 Swift 的所有源碼代碼,然后 Clone 到本地進(jìn)行閱讀糟秘,如果直接閱讀 Clone 下來的 Swift 庫简逮,你們發(fā)現(xiàn)大量的 .swift.gyb 的文件,GYB 文件是蘋果團(tuán)隊(duì)預(yù)處理的一種文件尿赚,里面包含著其它語言散庶,會(huì)影響到我們對(duì) Swift 源碼的閱讀,所以我們需要對(duì)從 GitHub Clone 的代碼進(jìn)行一次編譯凌净,生成閱讀性更強(qiáng)的 Swift 代碼悲龟。具體操作如下:
# 安裝編譯工具
brew install cmake ninja
# 創(chuàng)建源碼放置目錄
mkdir swift-source
cd swift-source
# 拉取源碼
git clone https://github.com/apple/swift.git
# 拉取編譯所需依賴的倉庫
./swift/utils/update-checkout --clone
最后一行命令會(huì)拉取編譯 Swift 庫所依賴的倉庫,在天朝冰寻,如果下載失敗或者下載慢躲舌,可能要多試幾次才能成功,我們都懂性雄。完成上面的步驟后没卸,我們可以執(zhí)行 apple 給我們提供好的 build 腳本進(jìn)行編譯:
./swift/utils/build-script -x -R
- -x 會(huì)生成一個(gè) Xcode 的項(xiàng)目,我們可能通過 Xcode 來閱讀源碼
- -R 指的是使用 release 進(jìn)行編譯秒旋,編譯更快约计,產(chǎn)物也更小,機(jī)器快迁筛,硬盤大的小伙伴們可無視煤蚌。
整個(gè)編譯過程是比較漫長的,當(dāng)然最終編譯速度最終還是得看機(jī)器的性能细卧,歐洲人請(qǐng)無視尉桩。
更新 Swift 源碼
隨著 Swift 的迭代開發(fā),我們想查看 Swift 新特性的源碼時(shí)贪庙,就需要我們?nèi)ジ?Swift 源碼蜘犁,當(dāng)然我們不需要重新執(zhí)行上面的操作,我們只需要執(zhí)行 update-checkout 腳本并重新編譯:
./swift/utils/update-checkout
./swift/utils/build-script -x -R
切換 Swift 版本
如果我們想查看某個(gè)版本中特有的特性止邮,這個(gè)時(shí)候我們需要切換 Swift 的版本这橙,但是如果只是簡單地切換 Swift 倉庫的分支是無法編譯通過的,他對(duì)應(yīng)的依賴也需要變更导披,我們可以通過 apple 提供的腳本進(jìn)行切換版本:
./swift/utils/update-checkout --tag swift-5.0-RELEASE
gyb 轉(zhuǎn)換為 Swift 代碼
因?yàn)?Swift 的編譯需要較長時(shí)候屈扎,并且占用大量硬盤空間,那么我們可以針對(duì)單個(gè) gyb 文件進(jìn)行轉(zhuǎn)換:
./swift/utils/gyb \
--line-directive '' \
-o ./xxxxxxx/Sequence.swift \
./swift/stdlib/public/core/Sequence.swift.gyb
- --line-directive '' 用于在生成的文件中去掉不必要的說明撩匕;
- -o 指定輸出目錄鹰晨;
- 最后是指定需要轉(zhuǎn)換的 gyb 文件目錄。
Hello World
在編譯完成 Swift 源碼后,可以通過 Xcode 來查看 Swift 的源碼模蜡。當(dāng)然漠趁,我們還是從萬碼起源的 Hello World 開始去嘗試閱讀 Swift 源碼。
print("Hello World")
這可能是你曾經(jīng)寫下的第一行代碼哩牍,那么棚潦,我們來看看在 Swift 中令漂,這行代碼到底是怎么實(shí)現(xiàn)的膝昆。
查看源碼
如果你想查看一個(gè)方法的源碼的方式有兩種:
- 如果你知道是具體的那個(gè)文件的類,則可以通過打開對(duì)應(yīng)的文件查看叠必,但是這種方法通常都是不太好使的荚孵,畢竟我們記不住那么多文件名;
- 通過全局搜索:**public func 方法名( **的方式來定位 public 方法的具體位置纬朝,協(xié)議收叶、類也是同理。
因?yàn)?print() 方法的源碼放在 Print.swift 文件中共苛,所以上面所說的兩種方式都是可行的判没。
print 源碼解讀
通過上述方式,我們可以找到 print() 的源碼實(shí)現(xiàn)如下:
public func print(
_ items: Any...,
separator: String = " ",
terminator: String = "\n"
) {
if let hook = _playgroundPrintHook {
var output = _TeeStream(left: "", right: _Stdout())
_print(items, separator: separator, terminator: terminator, to: &output)
hook(output.left)
}
else {
var output = _Stdout()
_print(items, separator: separator, terminator: terminator, to: &output)
}
}
首先我們可以看到 print() 方法是一個(gè)全局的 public 方法隅茎,所以我們可以在任意的地方調(diào)用他澄峰,其次該方法支持三個(gè)參數(shù),最后兩位是帶默認(rèn)值的參數(shù)辟犀。這就是給我們最直觀的感覺俏竞,接下來我們來看下他的內(nèi)部實(shí)現(xiàn):
if let hook = _playgroundPrintHook {...} else {...}
判斷當(dāng)前開發(fā)環(huán)境是不是 playground,如果是則將 output 定向到了 _TeeStream堂竟,如果不是則使用 _Stdout魂毁。
這樣我就可以很好理解 Playground 右邊顯示值的功能是怎么實(shí)現(xiàn)的了。
接下來我們繼續(xù)來閱讀 print() 的源碼出嘹,不管是否 plyground席楚,最終都會(huì)調(diào)用 _print() 方法:
internal func _print<Target : TextOutputStream>(
_ items: [Any],
separator: String = " ",
terminator: String = "\n",
to output: inout Target
) {
var prefix = ""
output._lock()
defer { output._unlock() }
for item in items {
output.write(prefix)
_print_unlocked(item, &output)
prefix = separator
}
output.write(terminator)
}
我們可以看到,首先將 output 加鎖税稼,然后通過 defer 關(guān)鍵字在函數(shù) return 前將 output 解鎖酣胀,保證了 output 在 write 過程中是線程安全的。然后我們再看 _print_unlocked() 方法:
internal func _print_unlocked<T, TargetStream : TextOutputStream>(
_ value: T, _ target: inout TargetStream
) {
if _isOptional(type(of: value)) {
let debugPrintable = value as! CustomDebugStringConvertible
debugPrintable.debugDescription.write(to: &target)
return
}
if case let streamableObject as TextOutputStreamable = value {
streamableObject.write(to: &target)
return
}
if case let printableObject as CustomStringConvertible = value {
printableObject.description.write(to: &target)
return
}
if case let debugPrintableObject as CustomDebugStringConvertible = value {
debugPrintableObject.debugDescription.write(to: &target)
return
}
let mirror = Mirror(reflecting: value)
_adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false)
}
從源碼來看娶聘,可以看出來闻镶,如果輸入的對(duì)象如果實(shí)現(xiàn) TextOutputStreamable,則打印出來的是它的值丸升,如果它實(shí)現(xiàn)的是 CustomStringConvertible 或者 CustomDebugStringConvertible 時(shí)铆农,print 實(shí)際打印出來的 description 內(nèi)容。
CustomStringConvertible 和 CustomDebugStringConvertible 都是熟悉的協(xié)議,沒有必要過多但是絕對(duì)墩剖,但對(duì)于 TextOutputStreamable 這種比較陌生的協(xié)議猴凹,必要時(shí)需要我們?nèi)ゲ殚喿x文檔:
遇到陌生的關(guān)鍵字或者協(xié)議時(shí),不需要著急岭皂,果爸爸的文檔一般都很詳細(xì)的郊霎,可以優(yōu)先查看文檔,閱讀 API 文檔可以快速了解學(xué)習(xí)一門語言爷绘。
繼續(xù)閱讀源碼發(fā)現(xiàn)书劝,如 value 實(shí)現(xiàn)以上協(xié)議時(shí)狂芋,則可以輸出相應(yīng)的描述內(nèi)容薇宠,如果未實(shí)現(xiàn)以上協(xié)議:
let mirror = Mirror(reflecting: value)
_adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false)
則通過 Mirror 映射輸出相應(yīng)的值, _adHocPrint_unlocked() 的具體實(shí)現(xiàn)就不繼續(xù)糾纏觅够,否則就沒完沒了陶因,感覺興趣的讀者可以自行研究下去骡苞。
小結(jié)
作為開篇,寫到這里就結(jié)束了楷扬,介紹了如何搭建環(huán)境和一個(gè)簡單例子解幽,在后面章節(jié)里,我們再來詳細(xì)聊聊 Swift 的那些事烘苹。
作為一名作者躲株,最大的成就感就是讀者在看了你的文章后,又有提筆寫文的沖動(dòng)螟加。作為一名程序員的我徘溢,最大的成就感莫過于你看了我的文章后,有想打開電腦寫下幾行代碼的沖動(dòng)捆探。