Swift 6 新特性速览:从数据竞争安全到 Swift Assist 的实战价值
上周刚把团队里一个跑了三年的老项目从 Swift 5.9 升到 Swift 6.0,配合 Xcode 16 发版,我算是真切体会到了这次更新的“狠劲”。之前我们那个电商 App 的订单列表页,因为要在后台预加载用户收藏状态,同时刷新本地缓存,多线程读写同一个 UserDefaults 扩展时,每隔个把月就会收到几份崩溃报告,堆栈里全是 EXC_BAD_ACCESS,排查起来头都大。
Swift 6 这次把数据竞争安全直接拉满,默认开启了完整的数据竞争检查。编译期就给我报了二十多个警告,全是我们之前觉得“没问题”的跨线程访问。比如下面这段我们之前常用的代码,在 Swift 6 里直接编译不过:
class UserProfileManager {
var cachedProfile: [String: Any]?
func fetchProfile(userId: String) async {
// 模拟网络请求
let result = await APIService.getUserProfile(id: userId)
// 这里直接修改,Swift 6 会报错:Mutation of non-isolated property 'cachedProfile' in a concurrent context
self.cachedProfile = result
}
func getCachedProfile() -> [String: Any]? {
return cachedProfile
}
}
// 调用时
let manager = UserProfileManager()
Task {
await manager.fetchProfile(userId: "123")
}
Task {
let profile = manager.getCachedProfile() // 另一个线程读取
}
之前我们靠自觉加 DispatchQueue 保护,现在 Swift 6 直接要求你用 Actor 隔离。我花了半天把 UserProfileManager 改成了 Actor,编译就通过了:
actor UserProfileManager {
var cachedProfile: [String: Any]?
func fetchProfile(userId: String) async {
let result = await APIService.getUserProfile(id: userId)
self.cachedProfile = result
}
func getCachedProfile() -> [String: Any]? {
return cachedProfile
}
}
// 调用时
let manager = UserProfileManager()
Task {
await manager.fetchProfile(userId: "123")
}
Task {
let profile = await manager.getCachedProfile() // 必须通过 await 访问
}
改完之后跑了两天,之前那种偶发的崩溃直接消失了。我们测试环境模拟了 500 QPS 的并发请求,内存占用反而比之前用锁的时候稳了 12% 左右,因为 Actor 的隔离机制比我们手写 DispatchQueue 要轻量得多。
另外 Xcode 16 里集成的 Swift Assist 也挺实用。上周我要写一个 Codable 解析的宏,之前得翻半天文档,现在直接在 Xcode 里描述需求:“生成一个宏,自动给 struct 添加 init(from decoder:) 方法,忽略 null 值字段”,它直接生成了可用的代码框架。我们团队新来的实习生用它写路由注册宏,之前要写 200 行的样板代码,现在 30 行就搞定了,他还顺便把之前遗留的 5 个解析错误给修好了。
不过迁移的时候也踩了点小坑。我们项目里有个用了三年的 NotificationCenter 扩展,在 Swift 6 里因为 @objc 方法和并发上下文冲突,编译报了一堆错。后来发现是之前为了兼容 OC 代码写的桥接,现在直接把那个扩展重写成 Swift 原生的 AsyncStream 监听,反而代码更清晰了。整个迁移过程花了大概 3 人/天,对于一个 20 万行的项目来说,这个成本我觉得挺值。
架构选型复盘:某日活百万社区为何从 MVC 彻底转向 SwiftUI + POP
去年我们接手了一个日活 120 万的社区 App,代码是五年前用 MVC 写的,现在维护起来简直是噩梦。举个具体的例子:首页的 FeedViewController 里有 3000 多行代码,光是处理 tableView 的 delegate 和 dataSource 就占了 800 行,每次加个新的卡片样式(比如后来要加的直播卡片、投票卡片),就得在那堆 if-else 里翻半天,还经常改出其他卡片的 bug。
我们当时算过一笔账:过去半年里,首页相关的 bug 占了全 App 的 45%,平均每个版本要花 5 天修首页的 UI 错乱问题。最夸张的一次,产品要加个“置顶帖”的样式,我改了 FeedViewController 里的布局逻辑,结果导致评论区输入框的键盘弹不出来,排查了整整一天才发现是两个 UIView 的 frame 计算冲突了——MVC 里 View 和 Controller 的耦合太深,牵一发而动全身。
后来我们决定彻底转向 SwiftUI + POP(面向协议编程)。选 SwiftUI 不是赶时髦,是因为我们统计过,之前用 UIKit 写一个动态高度的卡片,要算 sizeForItemAtIndexPath,还要处理自动布局约束,平均要 40 行代码;用 SwiftUI 描述状态的话,同样的功能 10 行就搞定,而且自动适配不同屏幕尺寸。
我们先用 POP 把卡片的逻辑抽象出来。比如定义一个 FeedCard 协议:
protocol FeedCard: Identifiable {
var id: String { get }
var cardType: CardType { get }
var createTime: Date { get }
func render() -> AnyView
}
enum CardType {
case text
case image
case live
case poll
}
然后每种卡片实现这个协议,比如图片卡片:
struct ImageFeedCard: FeedCard {
let id: String
let imageURL: URL
let title: String
let createTime: Date
var cardType: CardType { .image }
func render() -> AnyView {
AnyView(
VStack(alignment: .leading, spacing: 8) {
AsyncImage(url: imageURL) { image in
image.resizable().aspectRatio(contentMode: .fill)
} placeholder: {
ProgressView()
}
.frame(height: 200)
.clipped()
Text(title)
.font(.headline)
.lineLimit(2)
Text(createTime, style: .relative)
.font(.caption)
.foregroundColor(.gray)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(8)
)
}
}
首页的 View 就变得极其简单,只需要遍历卡片数组:
struct FeedListView: View {
let cards: [any FeedCard]
var body: some View {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(cards, id: \.id) { card in
card.render()
}
}
.padding()
}
}
}
改完之后效果立竿见影:首页的代码量从 3000 行降到了 800 行左右,新增卡片样式只需要加一个新的 FeedCard 实现,不用动原有的 View 代码。上个月我们加直播卡片,前后端加上测试只用了 2 天,之前用 MVC 至少要 5 天。而且 SwiftUI 的声明式写法让我们 UI 错乱的 bug 直接减少了 70%,最近三个版本首页相关的 bug 只占全 App 的 8% 了。
不过也不是一帆风顺。我们那个老项目里有很多自定义的转场动画,SwiftUI 一开始做起来挺别扭。后来发现可以用 matchedGeometryEffect 配合 UIViewRepresentable 包装原有的 UIKit 动画,才解决了问题。现在回过头看,当初咬牙迁移是对的——现在新来的开发看首页代码,半小时就能上手改需求,这在之前 MVC 时代想都不敢想。
并发模型踩坑实录:Actor 隔离如何拯救电商订单系统的库存数据
前年双 11 我们电商 App 的订单系统出了个大问题:大促开始后的前 10 分钟,有 37 个用户反馈下单成功了但库存没扣,导致超卖了 12 件商品。当时我盯着后台日志看,发现库存扣减的接口 QPS 到了 800 的时候,偶尔会出现两个请求同时读到同一个库存数量(比如 5 件),然后都扣减 1 件,最后库存变成了 4 件,但实际应该只剩 3 件。
我们当时的代码是这样的(简化版):
class InventoryManager {
private var stock: [String: Int] = [:] // 商品ID: 库存数量
func reduceStock(goodsId: String, quantity: Int) -> Bool {
guard let currentStock = stock[goodsId], currentStock >= quantity else {
return false
}
// 模拟网络延迟
Thread.sleep(forTimeInterval: 0.01)
stock[goodsId] = currentStock - quantity
return true
}
}
问题就出在 reduceStock 不是线程安全的。当多个 Task 同时调用这个方法时,可能同时执行到 let currentStock = stock[goodsId],拿到相同的库存值。我们当时紧急加了个 DispatchQueue 串行队列保护:
class InventoryManager {
private var stock: [String: Int] = [:]
private let queue = DispatchQueue(label: "com.inventory.queue")
func reduceStock(goodsId: String, quantity: Int) -> Bool {
var result = false
queue.sync {
guard let currentStock = stock[goodsId], currentStock >= quantity else {
return
}
Thread.sleep(forTimeInterval: 0.01)
stock[goodsId] = currentStock - quantity
result = true
}
return result
}
}
虽然临时解决了问题,但后来发现这个串行队列成了瓶颈。大促时订单接口的平均响应时间从 120ms 涨到了 800ms,因为所有库存操作都挤在一个队列里。后来我们升级到 Swift 5.5 的并发模型,用 Actor 重构了这部分逻辑:
actor InventoryManager {
private var stock: [String: Int] = [:]
func reduceStock(goodsId: String, quantity: Int) async -> Bool {
guard let currentStock = stock[goodsId], currentStock >= quantity else {
return false
}
// 模拟异步操作,比如更新数据库
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
stock[goodsId] = currentStock - quantity
return true
}
func getStock(goodsId: String) -> Int {
return stock[goodsId] ?? 0
}
// 初始化库存
func setStock(goodsId: String, quantity: Int) {
stock[goodsId] = quantity
}
}
调用的时候用 Task 包裹:
let inventoryManager = InventoryManager()
// 模拟大促时的并发请求
Task {
let success = await inventoryManager.reduceStock(goodsId: "goods_001", quantity: 1)
print("订单1扣减结果: \(success)")
}
Task {
let success = await inventoryManager.reduceStock(goodsId: "goods_001", quantity: 1)
print("订单2扣减结果: \(success)")
}
Actor 的隔离机制比串行队列高效多了,因为它是基于 Swift 并发模型的协作式调度,而不是阻塞线程。重构后我们压测了一下:同样 800 QPS 的并发请求,库存扣减的响应时间稳定在 150ms 左右,而且再也没有出现过超卖的情况。上个月我们做 618 预热,库存接口扛住了 1200 QPS 的峰值,错误率是 0。
不过用 Actor 的时候也有个坑。有一次我在 Actor 里调用了一个外部的异步回调闭包,结果编译报错说“不能在 actor 隔离上下文里调用非隔离的闭包”。后来才明白,Actor 的方法默认是隔离的,如果闭包不是在 Actor 内部定义的,就需要用 nonisolated 标记或者用 Task 包裹。我们当时有个老代码是用的 URLSession 的回调方式,改成 async/await 之后才解决这个问题。现在我们的订单系统里所有涉及共享状态的操作,全用 Actor 隔离,再也没出现过数据竞争导致的诡异 bug。
4. 利用 Swift Macros 减负:Codable 解析代码量减少 70% 的实战技巧
去年我在重构一个电商 App 的订单模块时,盯着满屏的 CodingKeys 枚举和手动解析逻辑发愁。那个订单模型有 32 个字段,光是写 init(from decoder:) 就花了半个下午,还因为手滑写错了一个字段名导致线上解析失败。后来 Swift 5.9 引入的宏(Macros)彻底改变了这种局面,我在 Xcode 16 里用 Swift 6.0 重写这个模块时,代码量直接从 420 行压缩到了 120 行左右。
很多刚接触 Swift 的开发者可能觉得宏听起来很玄乎,其实你可以把它想象成装修时的“预制构件”。以前我们盖房子(写代码)要自己搬砖、和水泥(手写解析逻辑),现在工厂直接把墙板、楼梯(重复代码)做好,我们只要按图纸组装就行。Swift 的宏就是在编译阶段帮我们生成这些“预制构件”的——它不会在运行时增加任何开销,因为代码在编译时就已经生成好了。
我那个订单模型最初长这样(简化版):
struct Order: Codable {
let orderId: String
let userId: String
let totalAmount: Double
let items: [OrderItem]
let createTime: Date
let status: OrderStatus
enum CodingKeys: String, CodingKey {
case orderId = "order_id"
case userId = "user_id"
case totalAmount = "total_amount"
case items
case createTime = "create_time"
case status
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
orderId = try container.decode(String.self, forKey: .orderId)
userId = try container.decode(String.self, forKey: .userId)
totalAmount = try container.decode(Double.self, forKey: .totalAmount)
items = try container.decode([OrderItem].self, forKey: .items)
createTime = try container.decode(Date.self, forKey: .createTime)
status = try container.decode(OrderStatus.self, forKey: .status)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(orderId, forKey: .orderId)
try container.encode(userId, forKey: .userId)
try container.encode(totalAmount, forKey: .totalAmount)
try container.encode(items, forKey: .items)
try container.encode(createTime, forKey: .createTime)
try container.encode(status, forKey: .status)
}
}
这还只是 6 个字段的情况,实际项目里订单模型有 32 个字段时,init 和 encode 方法加起来超过 200 行。更麻烦的是,每次后端加字段,我都要同步修改 CodingKeys、初始化方法和编码方法,有一次大促前后端临时加了 discountAmount 字段,我漏改了 encode 方法,导致用户看到的订单金额少了折扣部分,差点引发客诉。
后来我用了 Codable 宏,代码变成了这样:
@Codable
struct Order {
@CodingKey("order_id")
let orderId: String
@CodingKey("user_id")
let userId: String
let totalAmount: Double
let items: [OrderItem]
@CodingKey("create_time")
let createTime: Date
let status: OrderStatus
}
// 宏生成的代码会被编译器自动插入,我们不需要手动写
这里的 @Codable 是我自定义的一个宏(Swift 5.9+ 支持自定义宏),它的作用是在编译时自动生成 CodingKeys 枚举、init(from decoder:) 和 encode(to encoder:) 方法。我只需要用 @CodingKey 标注需要映射的 JSON 字段名,其他重复工作全交给宏处理。
实际落地时我遇到过一个坑:后端返回的 create_time 有时候是时间戳(Int),有时候是字符串(String),原来的手动解析我可以写 if let time = try? container.decode(Int.self, forKey: .createTime) { ... } 做兼容,但宏默认生成的代码是强类型的。后来我给宏加了一个 optional 参数,允许字段可选,再配合自定义解码器处理类型转换:
@Codable
struct Order {
// ... 其他字段
@CodingKey("create_time", optional: true)
let createTime: Date?
}
// 自定义日期解码器
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
if let timestamp = try? container.decode(Int.self) {
return Date(timeIntervalSince1970: TimeInterval(timestamp))
}
if let dateStr = try? container.decode(String.self) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter.date(from: dateStr) ?? Date()
}
return Date()
}
现在这个订单模块已经稳定运行了 8 个月,后端加字段时我只需要加一行属性声明,再也不用碰解析逻辑。根据我统计,团队里 12 个模型文件用宏重构后,总代码量从 3200 行降到了 900 行左右,新同事接手时也不用再翻几百行的解析代码了。Swift 6.0 对宏的支持更完善了,编译速度比 Swift 5.9 快了 15% 左右(我们项目编译时间从 42 秒降到了 36 秒),这也是我推荐现在开始用宏的原因。
5. 内存与性能调优:ARC 原理与解决闭包循环引用的真实压测数据
上个月我们 App 的“消息中心”页面在大促期间突然崩溃率飙升,后台监控显示这个页面的内存占用在 3 分钟内从 80MB 涨到了 450MB,最后触发 OOM(Out Of Memory)崩溃。我带着团队排查了两天,最后发现是一个闭包里的循环引用导致的——这个问题在 Swift 里太常见了,但很多开发者直到线上出问题才意识到它的严重性。
先说说 ARC(自动引用计数)是怎么工作的。你可以把它想象成一个图书管理员,每个对象(书)都有一个借阅记录(引用计数)。当有人借这本书(强引用),记录加 1;还书(引用释放),记录减 1。如果记录变成 0,管理员就把书扔掉(对象释放)。Swift 的 ARC 就是干这个的,它会在编译时自动插入引用计数的增减代码,我们不需要手动管理内存——但问题出在“循环借阅”上:如果 A 书被 B 书引用,B 书又被 A 书引用,两本书的借阅记录永远大于 0,管理员就永远扔不掉它们,这就是循环引用。
我们那个消息中心的问题出在这个代码里:
class MessageViewController: UIViewController {
var messages: [Message] = []
var loadMoreAction: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
loadMoreAction = {
// 这里捕获了 self
self.loadMoreMessages()
}
}
func loadMoreMessages() {
// 加载更多消息的逻辑
API.request(.messages(page: currentPage)) { [weak self] result in
guard let self = self else { return }
self.messages.append(contentsOf: result.data)
self.tableView.reloadData()
}
}
deinit {
print("MessageViewController deinit")
}
}
当时我测试发现,每次进入消息中心再退出,deinit 方法都不会调用——这说明控制器没有被释放。原因就是 loadMoreAction 闭包捕获了 self(强引用),而 self 又持有 loadMoreAction 属性,形成了循环引用。大促期间用户频繁进出这个页面,每次都留下一个没释放的控制器,内存自然就爆了。
解决的方法很简单,用 weak 或 unowned 打破循环:
class MessageViewController: UIViewController {
var messages: [Message] = []
var loadMoreAction: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
// 用 weak 捕获 self,避免强引用
loadMoreAction = { [weak self] in
guard let self = self else { return }
self.loadMoreMessages()
}
}
// ... 其他方法不变
}
改完之后,我做了一次压测:用 Instruments 的 Allocations 工具监控,模拟用户每秒进入/退出消息中心 5 次。修改前,10 分钟后内存占用 420MB,对象数量 12 万个;修改后,10 分钟后内存稳定在 85MB,对象数量 2.3 万个。更关键的是,deinit 方法每次退出都会调用,说明控制器正常释放了。
这里有个细节要注意:weak 和 unowned 的选择。如果闭包执行时 self 可能已经被释放(比如网络请求回调),用 weak 并加 guard 判断;如果确定 self 在闭包执行时一定存在(比如视图生命周期内的闭包),可以用 unowned,但风险是如果 self 提前释放,访问会变成野指针导致崩溃。我们团队现在统一规定:网络请求回调、异步操作闭包必须用 weak;视图事件处理(比如按钮点击)可以用 unowned,因为按钮被点击时控制器肯定还在。
还有一次,我在优化一个图片处理工具时,发现 DispatchQueue.global().async 里的闭包即使加了 weak self,还是会有内存问题。后来才明白,那个闭包里还捕获了 self 的一个属性 imageCache(一个字典),而 imageCache 又间接持有 self。这时候光加 weak self 不够,需要把需要的属性先取出来再用:
// 错误写法
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
let cache = self.imageCache // 这里还是通过 self 访问,可能间接强引用
// 处理图片
}
// 正确写法
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
let cache = self.imageCache // 先取出属性
// 或者直接用局部变量
let localCache = self.imageCache
// 处理图片
}
Swift 6.0 对 ARC 做了一些优化,比如在编译时更严格地检查可能的循环引用场景,Xcode 16 现在会在你写可能形成循环引用的代码时给出警告(虽然还不是强制报错)。根据苹果官方数据,Swift 6 的 ARC 在高频对象创建场景下的性能比 Swift 5 提升了 8% 左右,我们那个图片处理工具处理 1000 张图片的耗时从 1.2 秒降到了 1.1 秒,虽然看起来不多,但在低端设备上还是能感觉到流畅度提升。
6. 全栈视角拓展:Swift 在 Vapor 后端与 Core ML 端侧 AI 的应用
很多 iOS 开发者以为 Swift 只能写前端 App,其实我在过去两年里用 Swift 做了不少全栈尝试:用 Vapor 写后端 API,用 Core ML 做端侧 AI 识别,甚至把同一套 Swift 代码同时跑在 iOS 和 macOS 上。这种“一套语言打通前后端”的体验,是其他移动开发语言很难给到的。
先说 Vapor 后端。去年我们团队要做一个小型的订单状态推送服务,需要接收前端的 WebSocket 连接,实时推送订单更新。如果用 Node.js 或 Java,我得重新熟悉生态,但用 Vapor 的话,我可以直接用 Swift 写——毕竟我写了 8 年 Swift,连类型系统、并发模型都是熟的。当时用的是 Vapor 4.8(现在 Vapor 已经支持 Swift 6.0 了),写一个简单的 RESTful API 只需要几行代码:
import Vapor
func routes(_ app: Application) throws {
// 获取订单列表
app.get("orders") { req async throws -> [Order] in
let userId = try req.query.get(String.self, at: "user_id")
// 从数据库查询订单(这里用模拟数据)
return [
Order(id: "1", userId: userId, amount: 100.0),
Order(id: "2", userId: userId, amount: 200.0)
]
}
// 创建订单
app.post("orders") { req async throws -> Order in
let order = try req.content.decode(Order.self)
// 保存到数据库(模拟)
print("保存订单: \(order.id)")
return order
}
}
// Order 模型(和 iOS 端共用)
struct Order: Content {
let id: String
let userId: String
let amount: Double
}
这个服务上线后,我们做了一次性能测试:在 4 核 8G 的云服务器上,用 wrk 压测,QPS 达到了 1200 左右(对比同配置的 Node.js Express 服务,QPS 大约是 1500)。虽然比 Node.js 稍低,但优势在于类型安全——我在写 API 时,如果返回的数据结构和 Order 模型不匹配,编译就会报错,而不是运行时才发现问题。有一次我改了 Order 模型加了 discount 字段,忘了改 API 返回逻辑,Xcode 直接标红了,避免了一次线上数据不一致的问题。
再说 Core ML 的端侧 AI。今年我们 App 要加一个“拍照识别商品”的功能,最初考虑用云端 API,但用户反馈上传图片太慢(平均 800ms),而且有隐私风险。后来我用 Core ML 把 ResNet50 模型集成到端侧,识别速度直接降到了 120ms 以内,还不用联网。
集成过程比我想象中简单,Swift 的 Core ML 框架已经把模型调用封装得很好了:
import CoreML
import Vision
class ProductRecognizer {
private let model: VNCoreMLModel
init?() {
// 加载模型(ResNet50.mlmodel 放在项目里)
guard let modelURL = Bundle.main.url(forResource: "ResNet50", withExtension: "mlmodelc"),
let mlModel = try? MLModel(contentsOf: modelURL),
let visionModel = try? VNCoreMLModel(for: mlModel) else {
return nil
}
self.model = visionModel
}
func recognize(image: CGImage, completion: @escaping (String?) -> Void) {
let request = VNCoreMLRequest(model: model) { request, error in
guard let results = request.results as? [VNClassificationObservation],
let firstResult = results.first else {
completion(nil)
return
}
// 返回置信度最高的分类
completion(firstResult.identifier)
}
let handler = VNImageRequestHandler(cgImage: image, options: [:])
DispatchQueue.global(qos: .userInitiated).async {
do {
try handler.perform([request])
} catch {
completion(nil)
}
}
}
}
这里有个实际遇到的问题:模型文件太大(ResNet50 有 102MB),导致 App 包体积超标。后来我用 Core ML 的量化工具把模型从 32 位浮点数压缩到 16 位,体积降到了 51MB,识别准确率只下降了 0.3%(从 92.1% 降到 91.8%),用户完全感知不到。现在这个功能每天处理 3 万多次识别请求,端侧处理省了至少 30% 的服务器成本。
Swift 6.0 对服务器端和机器学习的支持都在加强:Vapor 已经发布了支持 Swift 6 的版本,并发处理性能比之前提升了 12%;Core ML 在 Swift 6 里可以直接用 async/await 调用模型,不用再写回调闭包。我最近在尝试把订单推送服务和商品识别功能整合,用同一套 Swift 代码处理后端逻辑和端侧智能——这种全栈的流畅感,是其他技术栈很难替代的。
站长实战手记
一个差点让我通宵的库存并发问题
去年我接手了一个电商 App 的订单模块重构。当时业务场景很直接:用户抢购限量商品,后台库存得实时扣减。我一开始图省事,把库存数据放到了一个全局的单例里,用 DispatchQueue 加锁来保护数据。
结果上线前压测,数据直接乱了。高并发下,两个线程同时读到库存为 1,都通过了校验,导致超卖。我当时盯着 Instruments 看了半天才反应过来,传统的锁在复杂业务流里太容易漏掉边界情况了。
后来我直接把核心库存逻辑扔进了 Actor 里。Actor 的隔离机制强制我通过异步方式访问状态,编译器直接帮我拦住了那些不安全的同步调用。改完再跑压测,数据稳得一批,超卖问题彻底消失。
关于技术选型的真心话
这几年 Swift 生态变化太快,我也走过弯路:
* SwiftUI:如果你的 App 需要支持 iOS 15 以下,或者页面有极其复杂的定制交互,别硬上。我在一个老项目里强推 SwiftUI,结果为了兼容旧系统写了很多奇怪的适配代码,维护成本反而高了。
* Swift Macros:虽然能减少样板代码,但调试起来真的很痛苦。如果是小团队或者新人多,建议先观望,别为了炫技把代码搞得太难读。
* Vapor:后端如果用 Swift,你得接受生态圈比 Node.js 或 Go 小这个现实。我用来写过内部工具,写业务接口很顺手,但遇到冷门需求时,找库的过程很折磨人。
给你的建议
学 Swift 别光看文档里的语法糖。找个有并发、有内存问题的真实场景去练手,比如写个简单的购物车或者下载器。只有在调试器里亲眼看到那些奇怪的内存引用或者线程死锁,你才算真正把这门语言学明白了。