零、简介
下面介绍Swift 5 & Swift 5.1中的新特性,内容主要来自WWDC2019 - Session 402 ;
主要包括以下三个部分:
ABI & Module Stability
性能提升
Language and Standard Library 新增的一些功能及特性
一、ABI Stability
What is ABI?
ABI = “Application Binary Interface”
ABI:程序的二进制接口,它定义了编译后的代码在运行时如何与其他库通信的细节;
比如方法的调用是怎么工作的、方法中的参数是怎样从调用方传递到被调用方的、元数据在内存中是怎样分布的等;
ABI稳定所带来的改变
稳定前:如果一个 Swift 项目使用了一个 Swift 编写的核心库,因为项目和库是分开编译的,为了保证在运行时项目和库有一个兼容 ABI 去调操作系统的接口,最简单的方法,就是约束项目和库使用的编译器是相同的版本。
稳定后:Program and framework can be built with different compilers (Swift 5 or later)
二、Module Stability
What is Module?
Module:一个库和它导出的 API 接口列表称为一个 Module;
(A module is a single unit of code distribution—a framework or application that is built and shipped as a single unit and that can be imported by another module with Swift’s import keyword.)
Module稳定所带来的改变
稳定前:如果项目中引用了一个 Module ,编译时,会去读取 . swiftmodule
文件,里面会包含 Module 的 API 列表,因为 Module 的底层实现比较复杂,跟编译器耦合比较严重,所以项目使用的编译器必须和 Module 之前编译构建时使用的编译器版本相同,才能运行。
通俗的来说,就是你开发一个库,你还得使用不同版本的 Swift 编译器对代码进行编译生成 framework
,这样使用特定 Swift 版本的项目才能集成。
稳定后:构建 Module 时,会生成一个 . swiftinterface
文件,这个文件里面会包含 Module 的稳定的API 列表,这样使用 Module 的项目的编译器版本不必与 Module 的保持一致了。(Swift 5.1 introduces a stable and textual module interface file)
ABI 稳定是针对编译和运行的每个操作系统实现的。现在的 ABI 稳定,只针对Swift5 在Apple 平台,其他的平台如 Linux、Windows等,预计会在后面去完善;
更多:
Binary Frameworks in Swift--Session-416
三、Performance
Shared Swift Runtime for Apps in the OS
目前(Swift 5) Swift 的 Runtime 库已经集成到了所有的苹果操作系统中,这样做带来如下的好处:
-
安装包的体积会变小
当用户在 iOS 12.2 及以后的系统中下载 App 时,下载的 App 会是不包含 Swift Runtime 库的,所有 App 共享同一个来自OS中的Swift Runtime 库,减小了包体积。当用户在 iOS 12.2 以前的系统中下载 App 时, App 会是包含 Swift Runtime 库的版本
-
更快的启动速度
Swift 5 相对于 Swift 4.2启动速度提升5%,在Swift 4.2中这5%的时间用来处理runtime相关
-
Code Size Reductions
Swift 5 之后,构建的代码大小会减少 10%
如果在 Xcode 开启 “optimize for size” 选项,构建的代码大小会减少 15% -
Faster Bridging BetweenSwift and Objective-C
NSDictionary to Dictionary Bridging 提高 1.6x
String to NSString 提高 15x
String Form UTF-16 To UTF-8
在Swift 5中将String的首选编码方式由UTF-16切换到UTF-8,同时保留了同Object-c的高效交互性;因为是底层的改变,所以对我们一般的开发者而言不需要做特别的处理,但是我们需要了解这样的改变带来的一些好处;
好处
处理ASCII字符串内容,UTF-8 使用的内存比 UTF-16 少 50%...(以前提到过)
UTF-8是一种单字节Unicode编码,是与C、系统编程、服务器端编程、脚本编写、客户端编程以及处理
源代码和文本格式的工具进行互操作的首选编码统一存储表示,增加处理性能(从下面的图中可以了解到)
增强对于小字符串的支持
Background
Swift 5前:字符串的存储可能下面两种编码之一
- ASCII storage class when contents are all ASCII
- UTF-16 for Unicode-rich text
Swift 5:上面两种方式被UTF-8 所代替
Opaque strings
An opaque string is capable of handling all string operations through resilient function calls. Nothing about their implementation is exposed outside the standard library. Currently, these are used for lazily-bridged NSStrings that do not provide access to contiguous UTF-8 (i.e. ASCII) bytes in memory. This can be expanded in the future to support any kind of future string forms we may want or need to add.
不透明字符串能够通过弹性函数调用处理所有字符串操作,它们的实现没有任何内容暴露在标准库之外。目前,这些用于延迟桥接的NSStrings,它们不能访问存储器中连续的UTF-8(即ASCII)字节;
(因为Swift中字符串是结构体,所以将标记的NSString指针映射到不透明的字符串)
contiguous:
All other strings are contiguous, that is, they can provide a pointer and length to valid UTF-8 bytes in memory.
所有其他字符串都是连续的,也就是说,它们可以提供指向内存中有效 UTF-8 字节的指针和长度。
indirect string
An indirect string has declared in the ABI that it is capable of providing a pointer to validly-encoded UTF-8 bytes in memory through a resilient function. Currently, indirect strings are used for NSStrings that can provide access to contiguous UTF-8 in memory. Since the means by which they get a UTF-8 pointer is behind a resilient function call, they can also be used to support shared strings, that is strings which share their contents with some other data structure (e.g. Array<UInt8>
). Accessing the content of an indirect string requires an extra level of indirection (hence the name) to get the pointer, but after that, they hit all the fast-paths in String’s implementation.
immediate string
剩余的contiguous string就是immediate string,它们处于 Swift 的首选形式,以便最高效、最直接的访问。
Small String
Swift 4.2 在 64 位平台上引入了一个小字符串表示形式,即不高于15 个 ASCII 代码单元长度字符串,这些字符串将值直接存储在 String 结构中,而无需分配或内存管理。在4.2的模型中无法将小字符串扩展到非ASCII内容。通过支持 UTF-8 的统一代码路径,我们能够增强小字符串,以支持多达 15 个 UTF-8 代码单元的长度。
我们还在32位平台上添加了小字符串支持,我们将最多10个UTF-8代码单元的字符串直接打包到String结构中。
large string
A large string holds a reference to a class instance with tail-allocated contents at a statically known offset from the instance’s address. This means there is no additional indirection on top of the reference itself.
更多:
https://forums.swift.org/t/piercing-the-string-veil/21700
四、Swift Tooling and Open Source
Sourcekitd
-
SourceKit
是一套工具集,使得大多数 Swift 源代码层面的操作特性得以支持,例如源代码解析、语法高亮、排版(typesetting)、自动补全、跨语言头文件生成等等。
-
Sourcekitd
是一个对 SourceKit 进行压力测试的工具,以便于发现 Bug ,并进行修复。
SourceKit-LSP
-
用于编译器或者开发环境与语言之间的协议,他定义像自动补全、跳转到定义、查找引用等功能;
-
SourceKit-LSP 是Swift语言对该协议的实现,其他遵循 LSP 协议的编译器或者开发环境中也可以进行 Swift 项目开发,以后用vim也可以写Swift了…
五、Language and Standard Library
Implicit returns(Swift 5.1) - SE-0255
struct Rectangle {
var width = 0.0, height = 0.0
var area: Double {
return width * height
}
}
struct Rectangle {
var width = 0.0, height = 0.0
var area: Double { width * height }
}
注意:这个特性只限于单行表达式
默认值初始化方法(Swift 5.1) - SE-0242
一个 Struct 的各个属性有默认值时,编译器会基于属性逐一去生成初始化方法;
struct Dog {
var name = "Generic dog name"
var age = 0
}
上面的结构体系统会默认生产下面的两个初始化方法
func Dog()
func Dog(age:Int, name: String)
因为上面的原因,我们通过下面的1, 2方法初始化都是没有问题的,但是3的初始化是有问题的
// 1
let boltNewborn = Dog() ✔️
// 2
let daisyNewborn = Dog(name: "Daisy", age: 0) ✔️
// 3
let benjiNewborn = Dog(name: "Benji") ❌
新特性之后,现在可以了😁
let benjiNewborn = Dog(name: "Benji") ✔️
SIMD Vectors(Swift 5) - SE-0229
用于矢量编程的更好的API ??
Opaque Result Types(Swift 5.1) - SE-0244
不透明类型是调用者只知道该对象的功能,而不知道它是什么类型;
被调用者知道实际的返回类型;
protocol Fighter { }
struct XWing: Fighter { }
struct YWing: Fighter { }
func launchFighter() -> Fighter {
return XWing()
}
let red4 = launchFighter()
let red5 = launchFighter()
我们调用 launchFighter() 方法,获得一个 Fighter,但是我们不知道具体的类型是什么,这么用是没有问题的,但是当我们需要对上面两个结果进行相等判断的时候,就会获得错误,如下图:
我们可能想通过让Fighter 去遵守Equatable协议去解决这个问题,但是如果这样做了我们获得下一个错误
这里的问题是,Equatable 需要比较这两个实例,也就是Self,来判断他们是否相等,但是Swift不能保证这两个实例的本质是相等的,例如我们可能用[1, 2, 3]和Fighter去进行比较;
在以前我们可以通过泛型来解决上面的问题,现在有了不透明类型;
下面让我们用不透明类型解决类似问题,需要用到some
关键字,上的报错就消失了;
func launchOpaqueFighter() -> some Fighter {
return XWing()
}
更直观的例子:
func makeInt() -> some Equatable {
return 1
}
func makeString() -> some Equatable {
return "string"
}
let a = makeInt()
let b = makeInt()
let c = makeString()
上面结果的原因就是,虽然我们只知道获得遵守Equatable协议的结果,但是Swift编译器知道具体的返回值是什么类型的,所以编译器允许我们 a和b做==判断,但是 c 和 a不被允许;
Property Wrapper Types(Swift 5.1) - SE-0258
static var usesTouchID: Bool {
get {
return UserDefaults.standard.bool(forKey: "USES_TOUCH_ID")
}
set {
UserDefaults.standard.set(newValue, forKey: "USES_TOUCH_ID")
}
}
static var isLoggedIn: Bool {
get {
return UserDefaults.standard.bool(forKey: "LOGGED_IN")
}
set {
UserDefaults.standard.set(newValue, forKey: "LOGGED_IN")
}
}
上面这两个属性中的getter
和 setter
方法中大部分的代码是重复的,可以将这些重复代码提取出来,封装一种属性,取名叫 UserDefault
,使用 @propertyWrapper
关键字来修饰,在这样在定义 usesTouchID
和 isLoggedIn
使用UserDefault
来修饰就可以大量减少重复代码
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var value: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
@UserDefault("USES_TOUCH_ID", defaultValue: false)
static var usesTouchID: Bool
@UserDefault("LOGGED_IN", defaultValue: false)
static var isLoggedIn: Bool
上面代码中的@propertyWrapper
关键字告诉编译器,这个类型是的主要目的是包装它的属性,制定访问模式;
允许我们通过这种方式@UserDefault("USES_TOUCH_ID", defaultValue: false)
来获得value: T
;
这里其实就是将泛型应用到了属性的定义中来了,减少重复代码,其中 value 属性是@propertyWrapper
中提供的;
DSLs (Domain Specific Languages)
<html>
<head>
<title>\(name)’s WWDC19 Blog</title>
</head>
<body>
<h2>Welcome to \(name)’s WWDC19 Blog! //这里缺少了一个</h2>
<br>
Check out the talk schedule and latest news from
<a href="https://developer.apple.com/wwdc19/">the source</a>
</body>
</html>
在xcode中写上面的HTML 代码,即使我们漏写了标签的,也不会给我提示或者报错,因为对于 Swift 编译器来说,这些只是一个长文本,所以无法进行语法检查;
""" <html>
<head>
<title>\(name)’s WWDC19 Blog</title> </head>
<body>
<h2>Welcome to \(name)’s WWDC19 Blog!
<br>
Check out the talk schedule and latest news from
<a href="https://developer.apple.com/wwdc19/">the source</a>
</body>
</html>
"""
所以发明了 DSL ,期望大家使用 Xcode 写 HTML 的时候用下面这种 DSL 来写(Swift 5.1)
html {
head {
title("\(name)'s WWDC19 Blog")
}
body {
h2 { "Welcome to \(name)'s WWDC19 Blog!" }
br()
"Check out the talk schedule and latest news from "
a{
"the source"
}.href(“https://developer.apple.com/wwdc19/")
}
if !loggedIn {
button {
"Log in for comments"
}
.onclick("triggerLogin()")
}
}
上面的这些代码看起来了Swift很像,我们可以看见Swift中熟悉的闭包、方法调用等,也用到了变量,提供了语法的高亮等特性;同样可以用Swift的条件控制语句等;
实现原理
像下图中显示的,在Swift中用方法重构了HTML中的每个标签
public func html(@HTMLBuilder content: () -> HTML) -> HTML { ... }
public func head(@HTMLBuilder content: () -> HTML) -> HTML { ... }
public func body(@HTMLBuilder content: () -> HTML) -> HTML { ... }
在这些方法中特别的是,参数闭包都是用@HTMLBuilder
属性来修饰的,这个属性告诉编译器,这些闭包都用 HTML builder 类型来进行处理,具体处理过程如下:
DSL结合SwiftUI的使用
init(alignment: HorizontalAlignment = .center, spacing: Length? = nil,
@ViewBuilder content: () -> Content)
上面的是VStack的初始化方法,在方法中用@ViewBuilder
修饰闭包,和@HTMLBuilder
类似;
更多:
Extension
Result Type(Swift 5.0) - SE-0235
主要目的是为我们提供更简单、更清晰的方式来处理复杂代码(如异步 API)中的错误;
public enum Result<Success, Failure> where Failure : Error {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)
....
🌰:
enum NetworkError: Error {
case badURL
}
func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void) {
guard let url = URL(string: urlString) else {
completionHandler(.failure(.badURL))
return
}
completionHandler(.success(5))
}
fetchUnreadCount1(from: "https://www.baidu.com") { result in
switch result {
case .success(let count):
print("\(count) unread messages.")
case .failure(let error):
print(error.localizedDescription)
}
}
fetchUnreadCount1(from: "https://www.baidu.com") { result in
if let count = try? result.get() {
print("\(count) unread messages.")
}
}
Handling Future Enum Cases(Swift 5.0) - SE-0192
Swift中要求所有switch语句都是详尽无遗的,必须涵盖所有情况;如果不能一一处理,一般我们用default处理剩下的情况;
enum PasswordError {
case short
case obvious
case simple
case new
}
switch passwordError {
case .obvious:
print("obvious")
case .short:
print("short")
default:
print("other wrong")
}
当我们新增一种情况,比如case new
,按照我们上面的写法,是没有问题的。但是当匹配到new
情况时,实际走的是default
,程序正常运行不会给我们任何新增case的提示,现在新增@unknown
关键字来解决这个问题;增加了@unknown
关键字后,会出现警告;
Universal Self(Swift 5.1) - SE-0068
这对于动态类型特别有用,需要在运行时确定某物的确切类型;
class NetworkManager {
class var maximumActiveRequests: Int {
return 4
}
func printDebugData() {
print("Maximum network requests: \(NetworkManager.maximumActiveRequests).")
}
}
class ThrottledNetworkManager: NetworkManager {
override class var maximumActiveRequests: Int {
return 1
}
}
let manager = ThrottledNetworkManager()
manager.printDebugData()
//Maximum network requests: 4
打印结果不是我们所期望的 1,下面我们将用Self关键字进行更改,将NetworkManager.maximumActiveRequests
更改为Self.maximumActiveRequests
;
class NetworkManager {
class var maximumActiveRequests: Int {
return 4
}
func printDebugData() {
print("Maximum network requests: \(Self.maximumActiveRequests).")
}
}
let manager = ThrottledNetworkManager()
manager.printDebugData()
//Maximum network requests: 1