1. 程式人生 > >WWDC 2018:Swift 更新了什麽?

WWDC 2018:Swift 更新了什麽?

auto width gree 就會 func 影響 三種 swift htm

本文轉載自:https://juejin.im/post/5b1cb5805188257d507be5d4
所有權歸原文所有

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 等等:

技術分享圖片

Swift 論壇

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

技術分享圖片

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

如果你在維護一個 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 產生什麽不好的影響。

技術分享圖片

並且 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 的兼容性

技術分享圖片

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

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

編譯器的改進

代碼兼容性

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

技術分享圖片

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

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

更快的 Debug 編譯速度

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

技術分享圖片

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

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

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

Compilation Mode vs. Optimization Level

技術分享圖片

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

  • 增量化編譯(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,換句話就是說被調用方持有了傳進來的對象,如下圖所示:

技術分享圖片

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

技術分享圖片

采取了這種模型之後,不止可以有更好的性能表現,還會讓編譯出來的二進制文件變得更小。

String

技術分享圖片

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

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

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

減小代碼尺寸

技術分享圖片

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 去控制:

技術分享圖片

更多細節可以查看 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 裏得到加強,可以檢測出更多非法內存訪問的情況,並且提供了運行時的檢查,在未來,內存獨占訪問權的檢查會像數組越界一樣默認開啟:

技術分享圖片

推薦資源

推薦查看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。


作者:四娘
鏈接:https://juejin.im/post/5b1cb5805188257d507be5d4
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

WWDC 2018:Swift 更新了什麽?