WWDC 2018:Swift 更新了什麼?

四娘發表於2018-06-10

WWDC 2018 Session 401 What's New in Swift?

這個 Session 分為兩個部分,前半部分會簡單介紹一下 Swift 開源相關的事情,後半部分我們深入瞭解一下 Swift 4.2 帶來了哪些更新。

社群的發展

首先我們來看一下 Swift 的一些統計資料,Swift 自開源之後,總共有 600 個程式碼貢獻者,合併了超過 18k pull request。

社群主導的持續整合

Swift 想要成為一門跨平臺的泛用語言,大概一個月之前,Swift 團隊擴充了原有的公開整合平臺,叫做 Community-Hosted Continuous Integration,如果大家想要把 Swift 帶到其它平臺上,就可以在這上面去接入你們自己的硬體機器,Swift 會定期在這些硬體上跑一遍整合測試,這樣大家就可以非常及時地瞭解到最新的 Swift 是否能在你們的平臺上正常執行。

目前已經接入了 Fedora 27 / Ubuntu on PowerPC / Debian 9.1 on Raspberry Pi 等等:

WWDC 2018:Swift 更新了什麼?

Swift 論壇

同時,Swift 的團隊付出了很大的精力在維護 Swift 的社群上,兩個月前 Swift 社群正式從郵件列表轉向論壇,讓大家可以更容易貢獻自己的力量,例如說三月份的這一份提案:

SE-0200

大家只要簡單回答這些問題,參與討論即可,如果你對於這方面的理解不深,不想貿然發言的話,其實只要大概閱讀過社群成員們的發言,對這件事情有了解,那也是一種參與,以後也許這個提案出來了你還可以寫篇文章跟大家講講當時討論的內容和要點。

如果你在維護一個 Swift 相關的計劃,你可以考慮在論壇上申請一個板塊,讓社群的人也可以關注到你的計劃並且參與到其中來。

WWDC 2018:Swift 更新了什麼?

Swift 的文件現在改為由 swift.org 來維護,網址是 docs.swift.org

Chris Lattner

Chris Lattner 大神離開蘋果的時候,有很多人在討論這會不會對 Swift 的發展有不好的影響,但過去一年,實際上 Chris 也還是儘自己的力量在推動 Swift 發展,去谷歌甚至可以說是在那裡做 Swift 的佈道師。

Chris 進了谷歌之後,谷歌 fork 了一個 Swift 的倉庫,作為谷歌裡開發者 Commit 的中轉站,過去一年修復了很多 Swift 在 Linux 上的執行問題,讓 Swift 在 Linux 上的執行更加穩定。谷歌還寫了一個 Swift Formatter,現在正在開發階段。

並且促成了 Swift 與 Tensorflow 的合作,開發了 Swift for Tensorflow,主要是因為 Python 已經漸漸無法滿足 Tensorflow 的使用,上百萬次的學習迴圈讓效能表現變得異常重要,需要一門語言去跟 Tensorflow 有更緊密的互動,大家可能覺得其它語言也都可以使用 Tensorflow,沒有什麼特別,實際上其它語言都只是開發了 Tensorflow 的 API,而 Swift 程式碼則會被直接編譯成 Tensorflow Graph,具有更強的效能,甚至 Tensorflow 團隊還為 Swift 開發了專門的語法,讓 Swift 變成 Tensorflow 裡的一等公民。加入了與 Python 的互動之後,讓 Swift 在機器學習領域得到了更加好的生態。

Chris 在過去一年,拉谷歌入局一起維護 Swift,加強 Swift 在 Linux 上的表現,還給 Swift 開闢了一個機器學習的領域,並且在 Swift 社群持續活躍貢獻著自己的才華,現在我想大家完全可以不必擔心說 Chris 的離開會對 Swift 產生什麼不好的影響。

WWDC 2018:Swift 更新了什麼?

並且 Swift 的開發團隊和社群裡也有很多做出了巨大貢獻的大神,例如這一次 Session 的主講之一 Slava,核心團隊負責人的 Ted,Doug Gregor,Xiaodi Wu 等等,他們也一樣把自己的精力和才華貢獻給了 Swift。

What is Swift 4.2?

接下來我們要了解一下 Swift 4.2,那麼 Swift 4.2 是什麼?它在整個開發週期中是一個什麼樣的角色?

Swift 每半年就會有一次 Major Release,Swift 4.2 就是繼 4.0 和 4.1 之後的一次 Major Release,官方團隊一直致力於提升開發體驗,這是 Swift 4.2 的開發目標:

  • 更快的編譯速度
  • 增加功能提升程式碼編寫效率
  • 讓 SDK 提供 Swift 更好的支援
  • 提升 ABI 的相容性

WWDC 2018:Swift 更新了什麼?

Swift 5 會在 2019 年前期正式釋出,ABI 最終會在這一個版本里穩定下來,並且 Swift 的執行時也會內嵌到作業系統裡,到時候 App 的啟動速度會有進一步的提升,並且打包出來的程式也會變得更小。

如果大家對於 ABI 穩定的計劃感興趣的話,可以關注一下這一份進度表 ABI Dashboard

編譯器的改進

程式碼相容性

跟 Xcode 9 一樣,Xcode 10 裡也只會搭載一個 Swift 編譯器,並且提供兩種相容模式,同時相容之前的兩個 Swift 版本,這三種模式都可以使用新的 API,新的語言功能。

WWDC 2018:Swift 更新了什麼?

並且不只是 Swift 的語法層面的相容,開發組三種模式也同時覆蓋 SDK 的相容,也就是說只要你的程式碼在 Xcode 8,Swift 3 的環境下能跑,那麼在 Xcode 10 裡使用相容模式也肯定可以跑起來。

但 Swift 4.2 確實提供了更多優秀的功能,為了接下來的開發,這會是最後一個支援 Swift 3 相容模式的版本。

更快的 Debug 編譯速度

接下來我們來討論一下編譯速度的提升,這是在 Macbook Pro 四核 i7 上測試現有 App 的結果:

WWDC 2018:Swift 更新了什麼?

Wikipedia 是一個 Objective-C 和 Swift 混編的專案,可能更加貼近大家的實際專案,專案的編譯速度實際上取決於很多方面,例如說專案的配置,圖片檔案的數量跟大小。

1.6 倍的提升是整體的速度,如果我們只關注 Swift 的編譯時間的話,實際上它總共提升了 3 倍,對於很大一部分專案來說,一次全量編譯大概可以比以前快兩倍。

這些提升來自於哪裡呢?由於 Swift 裡並不需要匯入標頭檔案,但每一個檔案由可以訪問到模組裡的其他檔案裡的內容,所以編譯階段會有大量的重複工作去進行 symbol 查詢,這次編譯器構建了一個編譯 pipeline 去減少重複的跨檔案執行。

Compilation Mode vs. Optimization Level

WWDC 2018:Swift 更新了什麼?

另外這一次,把“編譯模式”從“優化級別”裡剝離了出來,編譯模式意味著我們如何編譯我們的模組,目前總共有兩種模式:

  • 增量化編譯(Incremental):也就是以前的 Single File,逐個檔案編譯。
  • 模組化編譯(Whole Module):整個模組一起編譯。

增量編譯雖然全量編譯一次會比模組化編譯慢,但是之後修改一次檔案就只需要再編譯一次相關的檔案即可,而不必整個模組都重新編譯一次。

整個模組一起編譯的話會更加快,據說原理是把所有檔案都合併為一個檔案,然後再進行編譯,以此減少跨檔案的 symbol 查詢。但一旦改動了其中一個檔案,就需要重新再把整個模組編譯一遍。

增加了這個編譯選項實際上還有一個很重要的意義,以前我們只有三種選項,可以達到下面三種效果:

增量化編譯 模組化編譯
優化
不優化

優化是需要消耗時間的的,現在我們可以使用模組化並且不優化的選項,達到最快的編譯速度,把這個選項應用到我們專案裡不經常改動的那一部分程式碼裡的話(例如 pod 的依賴庫),就可以大大提高我們的編譯速度。

我把這個配置應用到專案裡之後,實測編譯速度從 113s 加快到了到了 64s,只要在 podfile 里加入這一段程式碼就可以了(在 Xcode 9.3 也可以正常使用):

post_install do |installer|
  # 提高 pod 庫編譯速度
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['SWIFT_COMPILATION_MODE'] = 'wholemodule'
      if config.name == 'Debug'
        config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Onone'
      else
        config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Osize'
      end
    end
  end
end
複製程式碼

Runtime 優化

ARC

Swift 使用 ARC 進行記憶體管理,ARC 是在 MRC 的基礎上演進出來的,ARC 使用某種物件管理模型在編譯時,在合適的位置自動為我們插入 retain 跟 release 程式碼。

Swift 4.2 之前使用的模型是“持有(owned)”模型,呼叫方負責 retain,被呼叫方負責 release,換句話就是說被呼叫方持有了傳進來的物件,如下圖所示:

WWDC 2018:Swift 更新了什麼?

但實際上這種模型會產生很多不必要的 retain 跟 release,現在 Swift 4.2 改為使用“擔保(Guaranteed)”模型,由呼叫方去保證物件在函式呼叫的生命週期內不會被 release 掉,被呼叫方不再持有物件:

WWDC 2018:Swift 更新了什麼?

採取了這種模型之後,不止可以有更好的效能表現,還會讓編譯出來的二進位制檔案變得更小。

String

WWDC 2018:Swift 更新了什麼?

當我們在 64bit 的平臺上例項化一個 String 的時候,它的長度是 16 bytes,為了儲存不等長的內容,它會在堆裡申請一段空間去儲存,而那 16 個 bytes 裡會儲存著一些相關資訊,例如編碼格式,這是權衡了效能和記憶體佔用之後的出來的結果。

但 16 bytes 的記憶體佔用實際上還存在著優化空間,對於一些足夠小的字串,我們完全可以不必在堆裡獨立儲存,而是放到這 16 個 bytes 裡空餘的部分,這樣就可以讓小字串有更好的效能和更少的記憶體佔用。

具體原理跟 NSString 的 Tagged Pointer 一樣,但能比 NSString 存放稍微更大一點的字串。

減小程式碼尺寸

WWDC 2018:Swift 更新了什麼?

Swift 還增加了一個優化等級選項 "Optimize for Size",名如其意就是優化尺寸,編譯器通過減少泛型特例化,減少函式內聯等等手段,讓最終編譯出來的二進位制檔案變得更小

現實中效能可能並非人們最關心的,而應用的大小會更加重要,使用了這個編譯選項實測可以讓二進位制檔案減小 10-30%,而效能通常會多消耗 5%。

新的語法功能

可遍歷列舉

以前我們為了遍歷列舉值,可能會自己去實現一個 allCases 的屬性:

enum LogLevel {
    case warn
    case info
    
    static let allCases: [LogLevel] = [.warn, .info]
}
複製程式碼

但我們在新增新的 case 的時候可能會忘了去更新 allCases,現在我們在 Swift 4.2 裡可以使用 CaseIterable 協議,讓編譯器自動為我們建立 allCases

enum LogLevel: CaseIterable {
    case warn
    case info
}

for level in LogLevel.allCases {
    print(level)
}
複製程式碼

Conditional Conformance

Conditional Conformance 表達了這樣的一個語義:泛型型別在特定條件下會遵循一個特定的協議。例如,Array 只會在它的元素為 Equatable 的時候遵循 Equatable:

extension Array: Equatable where Element: Equatable {
    func ==<T : Equatable>(lhs: Array<Element>, rhs: Array<Element>) -> Bool { ... }
}
複製程式碼

這是一個非常強勁的功能,Swift 標準庫裡大量使用這個功能,Codable 也是通過這個功能去進行檢查,幫助我們自動生成解析程式碼的.

Hashable 的加強

與 Codable 類似,Swift 4.2 為 EquatableHashable 引入了自動實現的功能:

struct Stock: Hashable {
    var market: String
    var code: String
}
複製程式碼

但這會帶來一個問題,hashValue 該怎麼實現?現有 hashValue 的 API 雖然簡單,但卻難以實現,你必須想出一種方法去把所有屬性糅合起來然後產生一個雜湊值,並且像 SetDictionary 這種圍繞雜湊表構建起來的序列,效能完全依賴於儲存的元素的雜湊實現,這是不合理的。

在 Swift 4.2 裡,改進了 Hashable 的 API,引入了一個新的 Hasher 型別來儲存雜湊演算法,新的 Hashable 長這個樣子:

protocol Hashable {
    func hash(into hasher: inout Hasher)
}
複製程式碼

現在我們不再需要在實現 Hashable 的時候就決定好具體的雜湊演算法,而是決定哪些屬性去參與雜湊的過程:

extension Stock: Hashable {
    func hash(into hasher: inout Hasher) {
        market.hash(into: &hasher)
        code.hash(into: &hasher)
    }
}
複製程式碼

這樣 Dictionary 就不再依賴於儲存元素的雜湊實現,可以自己選擇一個高效的雜湊演算法去構建 Hasher,然後呼叫 hash(into:) 方法去獲得元素的雜湊值。

Swift 會在每次執行時 為 DictionarySet 提供一個隨機的種子去產生隨機數作為雜湊的引數,所以 DictionarySet 都不會是一個有序的集合了,如果你的程式碼裡依賴於它們的順序的話,那就修復一下了。

而如果你希望使用一個自定義的隨機種子的話,可以使用環境變數 SWIFT_DETERMINISTIC_HASHING 去控制:

WWDC 2018:Swift 更新了什麼?

更多細節可以檢視 SE-0206 提案,不是很長,建議大家閱讀一遍。

隨機數產生

隨機數的產生是一個很大的話題,通常它都需要系統去獲取執行環境中的變數去做為隨機種子,這也造就了不同平臺上會有不同的隨機數 API:

#if os(iOS) || os(tvOS) || os(watchOS) || os(macOS)
    return Int(arc4random())
#else
    return random()
#endif
複製程式碼

但開發者不太應該去關係這些這麼瑣碎的事情,雖然 Swift 4.2 裡最重要的是 ABI 相容性的提升,但還是實現了一套隨機數的 API:

let randomIntFrom0To10 = Int.random(in: 0 ..< 10)
let randomFloat = Flow.random(in: 0 ..< 1)

let greetings = ["hey", "hi", "hello", "hola"]
print(greetings.randomElement()!)

let randomlyOrderGreetings = greetings.shuffled()
print(randomlyOrderedGreetings)
複製程式碼

我們現在可以簡單地獲取一個隨機數,獲取陣列裡的一個隨機元素,或者是把陣列打亂,在蘋果的平臺上或者是 Linux 上隨機數的產生都是安全的。

並且你還可以自己定義一個隨機數產生器:

struct CustomRandomNumberGenerator: RandomNumberGenerator { ... }

var generator = CustomRandomNumberGenerator()

let randomIntFrom0To10 = Int.random(in: 0 ..< 10, using: &generator)
let randomFloat = Flow.random(in: 0 ..< 1, using: &generator)

let greetings = ["hey", "hi", "hello", "hola"]
print(greetings.randomElement(using: &generator)!)

let randomlyOrderGreetings = greetings.shuffled(using: &generator)
print(randomlyOrderedGreetings)
複製程式碼

檢測目標執行平臺

以往我們自定義一些跨平臺的程式碼的時候,都是這麼判斷的:

#if os(iOS) || os(watchOS) || os(tvOS)
    import UIKit
    typealias Color = UIColor
#else
    import AppKit
    typealias Color = NSColor
#endif

extension Color { ... }
複製程式碼

但實際上我們關心的並不是到底我們的程式碼能跑在什麼平臺上,而是它能匯入什麼庫,所以 Swift 4.2 新增了一個判斷庫是否能匯入的巨集:

#if canImport(UIKit)
    import UIKit
    typealias Color = UIColor
#elseif canImport(AppKit)
    import AppKit
    typealias Color = NSColor
#else
    #error("Unsupported platform")
#endif
複製程式碼

並且 Swift 還新增了一套編譯巨集能夠讓我們在程式碼裡手動丟擲編譯錯誤 #error("Error") 或者是編譯警告 #warn("Warning")(以後不再需要 FIXME 這種東西了)。

另外還增加了一套判斷執行環境的巨集,下面是我們判斷是否為模擬器環境的程式碼:

// Swift 4.2 以前
#if (os(iOS) || os(watchOS) || os(tvOS) &&
    (cpu(i396) || cpu(x86_64))
    ...
#endif

// Swift 4.2
#if hasTargetEnviroment(simulator)
    ...
#endif
複製程式碼

廢除 ImplicityUnwrappedOptional 型別

ImplicityUnwrappedOptional 又被稱為強制解包可選型別,它其實是一個非必要的工具,我們使用它最主要的目的是,減少顯式的解包,例如說 UIViewController 的生命週期裡, viewinit 的時候是一個空值,但是隻要 viewDidLoad 之後就會一直存在,如果我們每次都使用都需要手動顯式強制解包 view! 就會很繁瑣,使用了 IUO 就可以節省這一部分解包程式碼。

所以 ImplicityUnwrappedOptional 是與 Objective-C 的 API 互動時很有用的一個工具,所有未被標記上 nullability 的變數都會被作為 IUO 型別暴露給 Swift,它的出現同時也是為了暫時填補 Swift 里語言的未定義部分,去處理那些固定模式的程式碼。隨著語言的發展,我們應該明確 IUO 的作用,並且用好的方式去取代它。

SE-0054 提案就是為此而提出的,這個提案實際上在 Swift 3 裡就實現了一部分了,在 Swift 4.2 裡繼續完善並且完整得實現了出來。

以往我們標記 IUO 的時候,都是通過型別的形式去實現,在 Swift 4.2 之後,IUO 不再是一個型別,而是一個標記,編譯器會通過給變數標記上 @_autounwrapped 去實現,所有被標記為 IUO 的變數都由編譯器在編譯時進行隱式強制解包:

let x: Int! = 0 // x 被標記為 IUO,型別其實還是 Optional<Int>
let y = x + 1   // 實際上編譯時,編譯器會轉化為 x! + 1 去進行編譯
複製程式碼

這就更加符合我們的原本的目的,因為我們需要標記的是變數的 nullability,而通過型別去標記的話實際上我們是在給一個標記上 IUO,而並非是變數

當然,這樣的改變也會給之前的程式碼帶來一點小影響,因為我們標記的物件針對的是變數,而並非型別,所以以往作為型別存在的 IUO 就會變成非法的宣告:

let a: [Int!] = [] // 編譯不通過
複製程式碼

記憶體獨佔訪問權

同一時間內,程式碼對於某一段記憶體空間的訪問是具有獨佔性,聽起來很難懂是吧,舉個例子你就明白了,在遍歷陣列的同時對陣列進行修改:

var a = [1, 2, 3]

for number in a {
    a.append(number) // 產生未定義的行為
}
複製程式碼

Swift 通過記憶體獨佔訪問權的模型,可以在編譯時檢測出這種錯誤,在 Swift 4.2 裡得到加強,可以檢測出更多非法記憶體訪問的情況,並且提供了執行時的檢查,在未來,記憶體獨佔訪問權的檢查會像陣列越界一樣預設開啟:

WWDC 2018:Swift 更新了什麼?

推薦資源

推薦檢視Ole Begemann 大神的出品的 What's new in Swift 4.2,帶著大家用 Playground 親身體會一下 Swift 裡新的語法功能。

結語

Swift 5 是一個很重要的里程碑,ABI 的穩定意味著這一份設計需要支撐後面好幾個大版本的功能需求,延期我覺得不算是一件壞事,大家別忘了,蘋果是 Swift 的最大的使用者,這門語言會支撐蘋果未來十幾年的 SDK 開發和生態,所以他們才會在 ABI 穩定這件事情上更加謹慎小心,而且這也很符合今年蘋果的方針,穩中求進。

待 ABI 塵埃落定之後,Swift 的語法功能肯定還會有一波爆發,async/await,原生的正規表示式...,甚至蘋果可能會開發 Swift Only 的 SDK,這些都讓我更加期待 2019 年的 Swift 5。

相關文章