Arclin

Advocate Technology. Enjoy Technology.

0%

What's new in Swift6.2

本文总结自WWDC 25 – What’s new in Swift

Swift-Subprocess

新增的Subprocess库可以更方便地执行命令(启动一个子进程)

1
2
3
4
5
6
7
8
9
import Subprocess

let swiftPath = FilePath("/usr/bin/swift")
let result = try await run(
.path(swiftPath),
arguments: ["--version"]
)

let swiftVersion = result.standardOutput

NotificationCenter

NotificationCenter进行了一些安全性更新

以前监听一个键盘出现事件,会有类似以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import UIKit

@MainActor
class KeyboardObserver {
func registerObserver(screen: UIScreen) {
let center = NotificationCenter.default
let token = center.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: screen,
queue: .main
) { notification in
guard let userInfo = notification.userInfo else { return }
let startFrame = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect
let endFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect

guard let startFrame, let endFrame else { return }

self.keyboardWillShow(startFrame: startFrame, endFrame: endFrame)
}
}

func keyboardWillShow(startFrame: CGRect, endFrame: CGRect) {}
}

以上代码会有几个问题:

  1. 通知名需要打对,不然永远不会执行通知回调,特别是有些通知名比较长又长得比较类似的时候容易补全选错。
  2. 与通知有关的信息储存在 非类型化的字典中(userInfo) 这要求你使用正确的键手动进行下标访问 并将所得结果动态转换为正确的类型
  3. 即使能保证通知回调闭包在主线程访问,但是在调用 Main Actor API (keyboardWillShow) 时 仍会收到并发错误 (Swift6下才会报错)。因为闭包默认不是Main actor 隔离的,而在一个非隔离的同步上下文中不能调用 Main actor 隔离的方法.要不就只能像下面这么改.
1
2
3
4
// 使用 MainActor.run 确保在main actor 上下文中执行
MainActor.run { [weak self] in
self?.keyboardWillShow(startFrame: startFrame, endFrame: endFrame)
}

Swift6.2有新的方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import UIKit

@MainActor
class KeyboardObserver {
func registerObserver(screen: UIScreen) {
let center = NotificationCenter.default
let token = center.addObserver(
of: screen,
for: .keyboardWillShow
) { keyboardState in
let startFrame = keyboardState.startFrame
let endFrame = keyboardState.endFrame

self.keyboardWillShow(startFrame: startFrame, endFrame: endFrame)
}
}

func keyboardWillShow(startFrame: CGRect, endFrame: CGRect) {}
}

通知变成了具体类型,这样子编译器可以直接检查订阅通知时数据内容和类型

定义通知的时候可以通过MainActorMessage定义该通知是在主线程发出的,AsyncMessage表示可能在任意线程发出

1
2
3
4
5
6
7
extension UIResponder { 
public struct KeyboardWillShowMessage: NotificationCenter.MainActorMessage
}

extension HTTPCookieStorage {
public struct CookiesChangedMessage: NotificationCenter.AsyncMessage
}

Observation

装饰器@Observable修饰的类可以用Observation类方法来进行监听数据变化,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Observation

enum Item {
case none
case banana
case star
}

@Observable
class Player {
let name: String
var score: Int = 0
var item: Item = .none

init(name: String) {
self.name = name
}
}

监听多个Player的属性的变化可以这么做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let player = Player(name: "Holly")
let values = Observations {
let score = "\(player.score) points"
let item =
switch player.item {
case .none: "no item"
case .banana: "a banana"
case .star: "a star"
}
return "\(score) and \(item)"
}


player.score += 2
player.item = .banana

当闭包中任一可观察属性的 willSet 被调用时将开始追踪更新,追踪进程将在代码暂停的下一个 await处结束,更新后的值将包含针对这两个代码点之间的所有同步更改,这可以确保针对多个属性进行的同步更新不会在对象处于不一致状态时引发观察更新

观察结果类型遵从 AsyncSequence 因此 你可以使用 for-await 循环 来迭代更新后的值

1
for await value in values { print(value) }

InlineArray

以前使用Array的时候会在堆空间开辟一块内存用于数组缓冲区,当数组数量超过阈值的时候会再堆空间里面开辟新的空间然后copy旧数据放入新空间再添加内容,最后再销毁旧空间。则会导致一些内存分配和引用计数的开销(除非你的数组数量永远不变)。

InlineArray 是一个固定大小的新数组类型。它通过内联存储来存储元素大小是类型的一部分并且在尖括号中位于元素类型之前

1
public struct InlineArray<let count: Int, Element: ~Copyable>: ~Copyable

InlineArray不会开辟堆空间,直接在栈空间存储数据

当数组确定的时候,数组容量会内联到InlineArray的泛型类型中去

这种做法可以更加安全地使用下标访问数组元素,这样子编译期间就可以预知到数组越界的情况

Span

在数组操作中的常见问题是遍历过程中数组被修改导致内存异常。原因是访问连续内存使用的指针是不安全指针,不保证访问时内存存在。

标准库为所有将连续存储容器 (包括 Array、ArraySlice、 InlineArray 等) 提供了 span 属性,用于维护内存安全。

Span 维护内存安全的方法是,确保在你使用 Span 时 使连续内存保持有效状态。这样的保障机制消除了指针固有的内存安全问题,包括释放后使用和重叠的修改,这些问题会在编译时进行检查,而不会产生运行时开销,例如:修改原始容器(比如给数组添加元素)会阻止随后访问span, span修改后你将无法再次访问span变量。

另外span的生命周期无法超过原始容器,这被称为生命周期依赖。

关于Span的详细信息,可以参考: https://developer.apple.com/documentation/swift/span?changes=la

并发编程

在旧版本Swift, 在Main Actor内调用异步方法(被标记为async的方法),会报错这种调用方式可能存在数据争用问题。

因为PhotoProcessor类是不隔离的,MainActor是隔离的,编译器没办法保证线程安全。

一般来说可以这么改,添加一个非隔离上下文再调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()

func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else {
return nil
}

// 显式切换到非隔离上下文
return await MainActor.assumeIsolated {
await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}
}

或者说方法标记为非隔离方法,自己在方法实现的时候保证线程安全

1
2
3
4
5
class PhotoProcessor {
nonisolated func extractSticker(data: Data, with id: String?) async -> Sticker? {
// 实现代码
}
}

在Swift6.2之后不会报错, 数据争用安全保障机制会提供保护,首先, 对于那些未与特定 Actor 绑定的 async 函数,Swift不会立即转移它们,而是让函数在其被调用的 Actor (Main Actor)上继续运行,因为传递到 async 函数的值永远不会在 Actor 之外发送,Async 函数仍可以在自身的实现中转移工作,但客户端不必担心它们的可变状态。

一致性支持,新增自动推断Main Actor模式,也就是默认都会加上@MainActor, 以方便开发者构建单线程应用。(这种模式是可选的)

当你需要某个方法始终在并发线程池运行,可以给这个方法标记为@concurrent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Explicitly offloading async work

class PhotoProcessor {
var cachedStickers: [String: Sticker]

func extractSticker(data: Data, with id: String) async -> Sticker {
if let sticker = cachedStickers[id] {
return sticker
}

let sticker = await Self.extractSubject(from: data)
cachedStickers[id] = sticker
return sticker
}

@concurrent
static func extractSubject(from data: Data) async -> Sticker {}
}

其他

  • VS Code 支持 Swift插件

  • Xcode 26 以及最新版本的SPM 预编译了Swift-syntax,将加快Swift宏的构建速度

  • Xcode 优化LLDB调试,首次执行p和po命令速度会变快

  • LLDB新增一系列新命令,如language swift task info获取当前异步任务的详细信息,例如任务的优先级以及其子任务信息

  • 编写单元测试的时候可以调用 Attachment.record 方法将附加功能添加到测试, 便于诊断测试失败的原因

  • Embedded Swift 嵌入式Swift进行了一些更新,性能更高,编译更快,用Swift写的后台服务器服务的吞吐量提升了 40% 而硬件要求则降低了一半。

  • Swift gRPC 2发布。

  • Swift-Java 发布,支持Swift和java互相调用。