4a3fda41b48866c549d838aebdf15d6a
Swift 5 & 5.1 为我们带来了什么

WWDC 2019 Session 402: What's New in Swift ?
Session 演讲者:Ted Kremenek, Anna Zaks
文章作者:Not_Found
文章校对:Lefe_x

这个 Session 分为两个部分,前半部分会简单介绍一下宏观上的优化(包体积的减少,启动速度的提升等),及一些开源工具,后半部分主要是一些 Swift 5 及 Swift 5.1 带来的新特性。

宏观上的优化

ABI 的稳定

ABI 是什么

ABI 代表应用程序二进制接口,它定义了编译后的代码在运行时如何与其他库通信的细节。例如函数如何调用,数据如何在内存中呈现,哪些元数据可用,以及如何访问等等,这些细节对于编译后的代码来说都是必需的。

带来的变化:

1.Swift 项目与使用的核心库不必须使用相同的编译器进行编译

在 ABI 稳定前

如果一个 Swift 项目使用了一个 Swift 编写的核心库,因为项目和库是分开编译的,为了保证在运行时项目和库有一个兼容 ABI 去调操作系统的接口,最简单的方法,就是约束项目和库使用的编译器是相同的版本。

在 ABI 稳定后

在 Swift 5 以后,不同版本的 Swift 编译器使用的 ABI 都相同了,项目和库使用的 Swift 版本就不受约束了。

2.Module 稳定性

在 Swift 中,一个库和它导出的 API 接口列表称为一个 Module 。

在 Swift 5.1 以前,如果项目中引用了一个 Module ,编译时,会去读取 . Swift module 文件,里面会包含 Module 的 API 列表,因为 Module 的底层实现比较复杂,跟编译器耦合比较严重,所以项目使用的编译器必须和 Module 之前编译构建时使用的编译器版本相同,才能运行。通俗的来说,就是你开发一个库,你还得使用不同版本的 Swift 编译器对代码进行编译生成 framework ,这样使用特定 Swift 版本的项目才能集成。

在 Swift 5.1 以后,构建 Module 时,会生成一个 . Swift interface 文件,这个文件里面会包含 Module 的稳定的API 列表,这样使用 Module 的项目的编译器版本不必与 Module 的保持一致了。

所有App共享Runtime库

目前 Swift 的 Runtime 库已经集成到了所有的苹果操作系统中,所有 App可以共享同一个 Runtime 库(而不用将 App 使用的 Swift Runtime 在编译时绑定到每个 App 的 ipa 包中去,减小了包体积)。

1.包体积会变小

当用户在 iOS 12.2 及以后的系统中下载 App 时,下载的 App 会是不包含 Swift Runtime 库的,所有 App 共享同一个 Swift Runtime 库,可以节约下载流量。当用户在 iOS 12.2 以前的系统中下载 App 时, App 会是包含 Swift Runtime 库的版本。( Xcode 打包后会有一个 App Thining 的选项,在这一步会对App生成两种可执行文件,包含 Swift Runtime库的和不包含 Swift Runtime 库的,供不同 iOS 系统的 App Store 使用)。

2. App 启动时间的优化

一个使用 Swift 编写没有任何功能的 App ,只是启动 App ,在非共享 Swift Runtime 库时,大概有 5% 的时间需要用于处理 App 内置的 Swift Runtime
库。

在 Swift 5 之后,由于使用共享 Swift Runtime 库时,这部分的时间就降低为0 了,所以可以缩短 App 启动时间。

代码大小的优化

Swift 5 之后,构建的代码大小会减少 10% 。
如果在 Xcode 开启 “optimize for size” 选项,构建的代码大小会减少 15% 。

Swift 和Objective-C混编的优化

NSDictionary 向 Dictionary 之间的转换比以前快了 1.6 倍。
String 往 NSString 的转换比以前快了 15 倍。

首选的字符串编码从 UTF-16 切换到 UTF-8

可以看看这篇文章《UTF-8 String》,里面有详细介绍。

Swift 工具和开源

Swift Docker Image

跟大部分开发者没有关系,参与 Swift 开发,贡献代码的人需要了解一些,大致是在 Docker Hub 创建了一个 Swift 的 Docker 镜像,降低了参与 Swift 开发的门槛。

Sourcekitd

SourceKit 是一套工具集,使得大多数 Swift 源代码层面的操作特性得以支持,例如源代码解析、语法高亮、排版(typesetting)、自动补全、跨语言头文件生成等等。
Sourcekitd 是一个对 SourceKit 进行压力测试的工具,以便于发现 Bug ,并进行修复。

可以看看这两篇文章,进行详细了解。
Introducing the sourcekitd Stress Tester
Translation - 起底 SourceKit

SourceKit-LSP

LSP(Language-Server-Protocol)是由红帽、微软和 Codenvy 联合推出的一个开源的语言服务器协议。可以让各种IDE便捷地支持各种程序代码的开发。

通俗的来说,就是 Swift 也支持这个 LSP 协议,并且为此写了一个项目SourceKit-LSP ,便于在 VSCode ,Vim 等其他遵循 LSP 协议的 IDE 中进行 Swift 项目开发,Session 里面演示了在 Vim 中进行 Swift 项目开发。

想要了解更多可以看看下面这两篇文章:
项目地址
VSCode 使用 LSP 进行 Swift 开发

Swift 5 及 Swift 5.1的一些新特性

这一部分主要讲的是 Swift 5 和 Swift 5.1 的具体特性更新, Swift-evolution 这个项目里面包含了 Swift 所有特性更新和介绍。

Swift-evolution 的 Github 地址
Swift-evolution 的网站主页

单表达式函数的隐式返回

最低支持版本: Swift 5.1

如果一个闭包只包含一个表达式,那么可以把 return 省略掉,隐式返回该表达式(我们经常见到的map, filter, flatmap 这些高阶函数经常用到这个特性)。在 Swift 5.1 中,一个函数只包含一个表达式,也可以将 return 省略掉。

举例说明:
没有这个新特性以前:

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

一个 Struct 的各个属性有默认值时,编译器会基于属性逐一去生成初始化方法(PS: 提出这个新特性的开发者叫 Alejandro Alonso ,刚刚高中毕业)。

举例说明:

struct Dog {
  var age = 0
  var name = "Generic dog name"
}

没有这个特性以前,编译器默认会生成这两个初始化方法

func Dog()
func Dog(age:Int, name: String)

执行这两行代码进行初始化是没有问题的

let boltNewborn = Dog()
let daisyNewborn = Dog(name: "Daisy", age: 0) 
let benjiNewborn = Dog(name: "Benji")

但是如果执行这行代码进行初始化,会报 "cannot invoke initializer for type 'Dog' with an argument list of type '(name: String)'" 错误,因为编译器没有自动生成 func Dog(name: String) 方法
//有了新特性以后,会自动生成 func Dog(name: String) 方法,调用这个方法进行初始化时,没有传入的属性会被赋以默认值,也就是执行下面的代码不会报错,生成的 benjiNewborn 对象的 age 会是0 , name 会是 Benji

let benjiNewborn = Dog(name: "Benji")

想要了解更多可以看看这个详细介绍

SIMD — 用于矢量编程的更好的API

最低支持版本: Swift 5

这个特性主要跟图形计算比较相关,大部分开发者可能用不上,或者也是使用 C/C++ 进行这方面的计算,所以感兴趣的朋友可以自己看看这个详细介绍

字符串插值的新设计

最低支持版本: Swift 5

1.在字符串插值方面,比 Swift 4.2 中的快1.7 倍。
2.扩大了字符串插值的应用范围。

在没有这个新特性以前,这样写是错误的,因为插值发生在翻译之前,所以不能这么写。

let quantity = 10
label.text = NSLocalizedString(
    "You have \(quantity) apples,
    comment: "Number of apples"
)
label.text = String(format: formatString, quantity)

在没有这个新特性以前,如果想要在 NSLocalizedString 中传值,必须写成下面这样。

let quantity = 10
label.text = NSLocalizedString(
    "You have %lld apples,
    comment: "Number of apples"
)
label.text = String(format: formatString, quantity)

在有了这个新特性以后可以直接在 NSLocalizedString 中通过反斜杠进行愉快地插值,所以扩大了插值地使用范围,增强了代码可读性。

let quantity = 10
label.text = NSLocalizedString(
    "You have \(quantity) apples",
    comment: "Number of apples"
)
label.text = String(format: formatString, quantity)

底层实现原理:上面的传入的 ”You have (quantity) apples“ 不单是作为一个字符串传入了,而是会分解为 "You have ",quantity," apples" 三个参数,最终在生成一个 LocalizedStringKey ,这样来完成插值功能的实现。

// In  Swift UI.framework
public struct Text {
  public init(
    _ key: LocalizedStringKey,
    tableName: String? = nil,
      bundle: Bundle? = nil,
    comment: StaticString? = nil
  )
}

// Generated by the  Swift compiler
var builder = LocalizedStringKey.StringInterpolation(
    literalCapacity: 16, interpolationCount: 1
).
builder.appendLiteral("You have ")
builder.appendInterpolation(quantity)
builder.appendLiteral(" apples")
LocalizedStringKey(stringInterpolation: builder)

想要了解更多可以看看这个详细介绍

Opaque Result Types 不透明的返回类型

与协议类型相比:

1.它会隐藏其返回值的类型信息。

2.会保留类型标识 ,只是调用者无法访问,编译器可以访问类型信息,进行类型推断(这个特性是在 iOS13 才支持)。

想要了解更多可以看看下面这些:
详细介绍
Opaque Result Types的相关文档
泛型语法改进第一弹 —— Opaque Result Types

属性包装类型

先看这个例子吧

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")
  }
}

//像上面这两个属性其实大部分的代码其实是重复的,可以将这些重复代码提取出来,封装一种属性,取名叫 UserDefault ,使用 @propertyWrapper 关键字来修饰,在这样在定义 usesTouchID 和 isLoggedIn 使用 UserDefault 来修饰就可以大量减少重复代码


struct UserDefault<T> {
  let key: String
  let defaultValue: T
    init(_ key: String, defaultValue: T) {
        ...
        UserDefaults.standard.register(defaults: [key: defaultValue])
    }
    var value: T {
        get {
                return UserDefaults.standard.object(forKey: key) as? T ??   defaultValue
        }
        set {
               UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

//定义usesTouchID和isLoggedIn

("USES_TOUCH_ID", defaultValue: false)
 static var usesTouchID: Bool
("LOGGED_IN", defaultValue: false)
 static var isLoggedIn: Bool

本来泛型只能应用于函数和类型,这里其实就是将泛型应用到了属性的定义中来了,减少重复代码。

想要了解更多可以看看这些:
详细介绍
Swift 文档-泛型

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>

上面是一段漏写了</h2> 标签的 HTML 代码,但是对于 Swift 编译器来说,只是一个长文本,所以无法进行语法检查,所以发明了一种 DSL ,期望大家使用 Xcode 写 HTML 的时候用下面这种 DSL 来写。

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/")
    }
}

然后通过调用下面这些函数来构造 HTML 元素,这样 Xcode 就可以进行语法检查了,其实这些 DSL 是前端领域里面早就有的模板语言。模板语言主要是解决HTML代码缺乏动态性而出现的,而 Swift 发现 DSL 主要是用于使用 Swift UI 进行App界面开发。想要了解更多关于模板语言的细节,可以看看这篇文章《前端数据模版引擎的总结》

public func html content: () -> HTML) -> HTML { ... } 
public func head content: () -> HTML) -> HTML { ... } 
public func body content: () -> HTML) -> HTML { ... }

实现原理:

head { 
    meta().charset("UTF-8")
  if cond { 
    title("Title 1")
  } else { 
    title("Title 2")
  } 
}

上面这段 DSL 会被编译器转换成下面这样,因为涉及到的前端元素有两个,一个是 meta 标签,所以会创建一个变量 a 来存储 meta 标签相关的信息,另一个是 title 元素,所以会创建一个变量 d 来存储 title 标签相关的信息,最终将变量 a 和变量 d 作为入参,调用 HTMLBuilder 的buildBlock 生成HTML 。

head {
  let a: HTML = meta().charset("UTF-8") 
  let d: HTML
  if cond {
    let b: HTML = title("Title 1")
    d = HTMLBuilder.buildEither(first: b) 
  } else {
    let c: HTML = title("Title 2")
    d = HTMLBuilder.buildEither(second: c) 
  }
  return HTMLBuilder.buildBlock(a, d) 
}

PPT 最后给了 Swift UI 中使用 DSL 的例子。

总结

这个 Session 主要是介绍了 Swift 5.0, Swift 5.1 的新特性。虽然说是新特性,其实不"新",因为在今年一月份的时候,苹果开发者网站上就发布了《 Swift 5 Release Notes for Xcode 10.2》这篇文章,里面就介绍了 Swift 5 的新特性(PS:当时我刚加入到知识小集组织中,然后就负责了这篇文章的初步翻译,经过南大的修改和美化,这篇文章的翻译版《 Swift 5 新特性一览》最终发布在知识小集的公众号中)。虽然像 ABI 稳定,包体积瘦身等一些新特性我们都已经了解了,但是毕竟是从 Swift 框架使用者的角度去学习了解这些新特性,并不一定全面和深入。通过观看这个 Session ,我们可以从 Swift 开发团队的角度来看待这些新特性,加深我们的理解。

参考资料:
What's New in Swift
《 Swift 5 新特性一览》
Swift 5 新特性预览
Evolving Swift On Apple Platforms After ABI Stability

© 著作权归作者所有
这个作品真棒,我要支持一下!
一年一度的 WWDC 又来啦!今年国内三大 iOS 组织(排名不分先后): 老司机 iOS 周报 知识小集 Sw...
12条评论

“首选的字符串编码从 UTF-16 切换到 UTF-8

可以看看这篇文章,里面有详细介绍。”

上面的链接没有

#1楼 @妙宝晓龙 感谢提醒,是格式出现了一点问题,是这一篇文章https://swift.org/blog/utf8-string/,现在已经有了

@propertyWrapper
那块的 set{} 写错位置啦

#3楼 @gaofei 感谢提醒,是复制的时候格式错乱了,已经改过来了

alexliu
#5

xiaozhuanlan 代码要是能提供复制就好了。否则直接copy paste 一堆line number

#5楼 @alexliu 这个应该是 MarkDown 主题的原因,我现在复制了一下代码,好像没有行号,你可以再试一下

Nemo
#7

OpaqueTypes 文档地址404了,应该是这个:https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html

#7楼 @Nemo 感谢提醒,已经修改过来了

Nemo
#9

Property Wrapper 提案链接应该是这个 https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md 官方将 Property Delegate 改名为 Property Wrapper 了,但我看注解还没改

#9楼 @Nemo 好的,谢谢提醒,官方是四天前改的,之前写文章的时候还是 Property Delegate,所以是旧的

Nemo
#11

#10楼 @Not Found-- 你有没有试过这个特性呢,我在 beta2 上尝试,$ 这个符号是没有用的

#11楼 @Nemo 我还没有升级,所以还没有试过这个新特性,可能是开发团队还是优化

top Created with Sketch.