Swift 开发最佳实践
# Swift 开发最佳实践
本文档整理 Swift / SwiftUI 开发中的常见知识点、最佳实践和避坑指南,供团队参考。 最后更新:2026-08-26
# 一、闭包捕获与循环引用
# 1.1 闭包默认强引用
Swift 闭包默认会强引用捕获的对象。如果闭包被对象持有,同时闭包又强引用该对象,就会形成循环引用(Reference Cycle),导致内存泄漏。
// ❌ 循环引用示例
class MyClass {
var closure: (() -> Void)?
func setup() {
closure = {
self.doSomething() // 闭包强引用 self
}
}
// MyClass 强引用 closure,closure 强引用 MyClass → 循环引用
}
2
3
4
5
6
7
8
9
10
11
# 1.2 捕获列表:[weak self] vs [unowned self]
| 特性 | [weak self] | [unowned self] |
|---|---|---|
| 引用类型 | 弱引用 | 无主引用 |
| 是否可选 | 是(self?) | 否(self,直接访问) |
| self 被释放后 | 安全(nil,不执行) | 💥 崩溃(悬垂指针) |
| 适用场景 | self 可能被释放 | self 一定不会被释放,且闭包生命周期不超过 self |
# [weak self] 示例
// ✅ 使用 [weak self] 避免循环引用
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send() // self? 为 nil 时安全跳过
}
2
3
4
# [unowned self] 示例
// ✅ 确定 self 不会被释放时使用 [unowned self]
UIView.animate(withDuration: 0.3) { [unowned self] in
self.view.layoutIfNeeded() // self 一定存在,直接访问
}
2
3
4
# 1.3 什么时候必须用 [weak self]?
| 场景 | 是否需要 [weak self] | 原因 |
|---|---|---|
DispatchQueue.main.async | ✅ 推荐 | 闭包被 GCD 持有,可能形成循环引用 |
Timer.scheduledTimer | ✅ 必须 | Timer 强引用 target,容易形成循环引用 |
UIView.animate | ⚠️ 可选 | 闭包执行完毕后释放,通常不会泄漏,但用 [weak self] 更安全 |
即时执行的闭包(如 map、filter) | ❌ 不需要 | 闭包即时执行,不会被持有 |
@escaping 闭包 | ✅ 推荐 | 逃逸闭包可能被持有,容易形成循环引用 |
| 单例对象的闭包 | ⚠️ 可选 | 单例不会被释放,但用 [weak self] 是最佳实践 |
# 1.4 循环引用检测工具
- Xcode Memory Graph Debugger:运行时查看对象引用关系
- Instruments - Leaks:检测内存泄漏
- deinit 打印日志:在
deinit中打印日志,确认对象是否被释放
deinit {
print("\(type(of: self)) deinit") // 如果没打印,说明可能有循环引用
}
2
3
# 二、GCD 与异步编程
# 2.1 常用队列
| 队列 | 说明 | 使用场景 |
|---|---|---|
DispatchQueue.main | 主线程串行队列 | UI 更新、用户交互 |
DispatchQueue.global() | 全局并发队列 | 耗时操作(网络、计算、IO) |
DispatchQueue(label:) | 自定义串行队列 | 保护共享资源、顺序执行任务 |
DispatchQueue(label:, attributes: .concurrent) | 自定义并发队列 | 并发执行任务 |
# 2.2 异步派发
// 主线程异步执行(下一个 runloop)
DispatchQueue.main.async {
// UI 更新
}
// 后台线程异步执行
DispatchQueue.global().async {
// 耗时操作
DispatchQueue.main.async {
// 回到主线程更新 UI
}
}
// 延迟执行
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// 1 秒后执行
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.3 async 为什么能解决视图更新警告?
问题:在视图更新过程中调用 objectWillChange.send() 会触发警告:
Publishing changes from within view updates is not allowed
原因:SwiftUI 的视图更新在当前 runloop 中同步执行,如果此时发布变化,会导致未定义行为。
解决方案:用 DispatchQueue.main.async 将发布变化推迟到下一个 runloop:
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send() // 下一个 runloop 执行,当前视图更新已完成
}
2
3
关键点:不是"判定"是否在视图更新过程中,而是"确保"当前事务完成后再执行。
# 三、SwiftUI 属性包装器
# 3.1 常用属性包装器对比
| 属性包装器 | 用途 | 存储位置 | 触发视图更新 |
|---|---|---|---|
@State | 视图内部私有状态 | 视图内存 | ✅ |
@Binding | 引用父视图的 @State | 不存储,引用 | ✅ |
@ObservedObject | 观察外部对象 | 外部对象 | ✅(对象的 objectWillChange) |
@StateObject | 视图拥有的可观察对象 | 视图内存 | ✅ |
@EnvironmentObject | 全局共享对象 | 环境 | ✅ |
@Environment | 系统环境值(如 colorScheme) | 系统环境 | ✅ |
@AppStorage | UserDefaults 持久化 | UserDefaults | ✅(在视图中) |
@Published | 可观察对象的属性 | 对象内存 | ✅(触发 objectWillChange) |
# 3.2 @StateObject vs @ObservedObject
| 特性 | @StateObject | @ObservedObject |
|---|---|---|
| 所有权 | 视图拥有 | 外部拥有,视图观察 |
| 生命周期 | 与视图相同 | 不拥有,可能被释放 |
| 适用场景 | 视图创建并拥有的对象 | 外部传入的对象 |
| 注意事项 | 只会初始化一次 | 每次视图重建都可能重新初始化 |
// ✅ 视图拥有的对象用 @StateObject
struct MyView: View {
@StateObject var viewModel = ViewModel() // 视图拥有,生命周期与视图相同
}
// ✅ 外部传入的对象用 @ObservedObject
struct MyView: View {
@ObservedObject var viewModel: ViewModel // 外部拥有,视图只观察
}
2
3
4
5
6
7
8
9
# 3.3 @AppStorage 在 ObservableObject 中的坑
问题:@AppStorage 实现了 DynamicProperty 协议,它的 update() 方法只有在 SwiftUI 视图中才会被自动调用。在 ObservableObject 中,update() 不会被自动调用,所以 @AppStorage 的缓存值可能不会更新。
现象:在设置页面修改了 @AppStorage 属性,但在 ObservableObject 中读取到的还是旧值。
解决方案:在 ObservableObject 的 init 中监听 UserDefaults.didChangeNotification,主动调用所有 @AppStorage 属性的 update() 方法:
final class AppSettings: ObservableObject {
static let shared = AppSettings()
@AppStorage("showOverlayOnBackground") var showOverlayOnBackground: Bool = true
// ... 其他 @AppStorage 属性
private init() {
NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.refreshFromUserDefaults()
}
}
private func refreshFromUserDefaults() {
_showOverlayOnBackground.update()
// ... 其他属性的 update()
// 异步派发,避免在视图更新过程中调用
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 四、SwiftUI 视图更新机制
# 4.1 视图更新触发条件
SwiftUI 视图在以下情况会重新计算 body:
@State属性变化@ObservedObject/@StateObject/@EnvironmentObject的objectWillChange触发@Environment值变化(如colorScheme)- 父视图重新渲染(子视图可能重新渲染)
@AppStorage值变化(在视图中)
# 4.2 视图更新过程
当前 runloop
├── 状态变化(@State / objectWillChange)
├── SwiftUI 标记视图为"需要更新"
├── runloop 结束时,批量执行视图更新
│ ├── 计算 body
│ ├── diff(比较新旧视图)
│ └── 渲染
└── runloop 结束
2
3
4
5
6
7
8
# 4.3 为什么不能在视图更新过程中发布变化?
如果在视图更新过程中(比如 body 计算、onAppear)调用 objectWillChange.send():
- SwiftUI 正在更新视图,此时又收到"对象将要变化"的通知
- 可能导致无限循环、重复更新、或未定义行为
- SwiftUI 会输出警告:
Publishing changes from within view updates is not allowed
解决方案:用 DispatchQueue.main.async 推迟到下一个 runloop。
# 4.4 onAppear vs onChange vs task
| 修饰符 | 触发时机 | 适用场景 |
|---|---|---|
.onAppear | 视图出现时 | 初始化、加载数据 |
.onDisappear | 视图消失时 | 清理、取消任务 |
.onChange(of:) | 某个值变化时 | 响应状态变化 |
.task | 视图出现时,异步任务 | 异步加载数据(自动取消) |
# 五、内存管理最佳实践
# 5.1 单例的使用
// ✅ 推荐:线程安全的单例
final class MyManager {
static let shared = MyManager() // let 是线程安全的
private init() {} // 禁止外部创建
}
// ❌ 不推荐:非线程安全的单例
final class MyManager {
static var shared: MyManager!
private init() {}
}
2
3
4
5
6
7
8
9
10
11
# 5.2 避免强引用循环
| 场景 | 解决方案 |
|---|---|
| 闭包捕获 self | [weak self] 或 [unowned self] |
| delegate 模式 | delegate 用 weak |
| Timer | 用 [weak self],或在 deinit 中 invalidate() |
| 通知中心 | 在 deinit 中移除观察者,或使用 [weak self] |
| 异步任务 | 用 [weak self],任务完成后检查 self 是否存在 |
# 5.3 deinit 调试技巧
deinit {
print("✅ \(type(of: self)) deinit") // 确认对象被释放
}
2
3
如果 deinit 没有被调用,说明可能有循环引用。
# 六、错误处理最佳实践
# 6.1 try? vs try! vs do-catch
| 方式 | 说明 | 适用场景 |
|---|---|---|
try? | 错误返回 nil | 错误不重要,可以忽略 |
try! | 错误直接崩溃 | 确定不会出错(如资源文件加载) |
do-catch | 捕获并处理错误 | 需要处理错误的场景 |
// ✅ 推荐:do-catch 处理错误
do {
let data = try Data(contentsOf: url)
} catch {
print("加载失败: \(error)")
}
// ✅ 可以忽略错误时用 try?
let data = try? Data(contentsOf: url)
// ❌ 避免:除非确定不会出错
let data = try! Data(contentsOf: url) // 出错会崩溃
2
3
4
5
6
7
8
9
10
11
12
# 6.2 自定义错误类型
enum AppError: Error, LocalizedError {
case invalidPassword
case decryptionFailed
case fileNotFound(String)
var errorDescription: String? {
switch self {
case .invalidPassword:
return "密码错误"
case .decryptionFailed:
return "解密失败"
case .fileNotFound(let path):
return "文件不存在: \(path)"
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 七、线程安全最佳实践
# 7.1 共享资源保护
// ✅ 推荐:用串行队列保护共享资源
final class ThreadSafeStore {
private let queue = DispatchQueue(label: "com.example.store")
private var _items: [Item] = []
var items: [Item] {
queue.sync { _items } // 读用 sync
}
func add(_ item: Item) {
queue.async { self._items.append(item) } // 写用 async
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 7.2 主线程更新 UI
// ✅ 确保在主线程更新 UI
DispatchQueue.main.async {
self.label.text = "新内容"
}
// ✅ 检查是否在主线程
func updateUI() {
if Thread.isMainThread {
label.text = "新内容"
} else {
DispatchQueue.main.async { self.label.text = "新内容" }
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 八、代码组织最佳实践
# 8.1 目录结构
CipherNest/
├── App/ # App 入口、配置
├── Views/ # SwiftUI 视图
│ ├── Components/ # 可复用组件
│ └── Modifiers/ # 自定义 ViewModifier
├── ViewModels/ # 视图模型
├── Models/ # 数据模型
├── Services/ # 业务逻辑服务
├── Extensions/ # 扩展
├── Utilities/ # 工具类
├── Debug/ # 调试相关(#if DEBUG)
└── Resources/ # 资源文件
2
3
4
5
6
7
8
9
10
11
12
# 8.2 日志工具
// ✅ 推荐:每个文件有自己的 log 实例,tag 为文件名
struct MyView: View {
private let log = LogHelper(tag: "MyView")
var body: some View {
Text("Hello")
.onAppear {
log.info("onAppear")
}
}
}
2
3
4
5
6
7
8
9
10
11
# 8.3 扩展文件命名
Extensions/
├── String+Extension.swift
├── Date+Extension.swift
├── Color+Extension.swift
└── View+Extension.swift
2
3
4
5
# 九、性能优化最佳实践
# 9.1 避免不必要的视图更新
// ✅ 使用 Equatable 避免不必要的更新
struct Item: Identifiable, Equatable {
let id: UUID
var title: String
}
// ✅ 使用 .id() 强制视图重建
MyView()
.id(item.id) // id 变化时强制重建
2
3
4
5
6
7
8
9
# 9.2 懒加载
// ✅ 大列表用 LazyVStack / LazyHStack
ScrollView {
LazyVStack {
ForEach(items) { item in
ItemRow(item: item)
}
}
}
2
3
4
5
6
7
8
# 9.3 避免在 body 中做耗时操作
// ❌ 不推荐:在 body 中做耗时操作
var body: some View {
let data = loadFromDisk() // 每次 body 计算都执行
Text(data)
}
// ✅ 推荐:在 onAppear 或 task 中加载
@State private var data: String = ""
var body: some View {
Text(data)
.task {
data = await loadFromDisk()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 十、调试技巧
# 10.1 打印调用栈
// 打印当前调用栈,用于排查问题
func debugCallStack() {
print("📞 Call stack:")
for symbol in Thread.callStackSymbols.prefix(10) {
print(" \(symbol)")
}
}
2
3
4
5
6
7
# 10.2 测量执行时间
func measure<T>(_ label: String, _ block: () -> T) -> T {
let start = CFAbsoluteTimeGetCurrent()
let result = block()
let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
print("⏱️ \(label): \(String(format: "%.1f", elapsed))ms")
return result
}
// 使用
measure("加载数据") {
loadData()
}
2
3
4
5
6
7
8
9
10
11
12
# 10.3 条件编译
#if DEBUG
// 调试代码,Release 不会打包
debugPrint("调试信息")
#endif
#if targetEnvironment(simulator)
// 模拟器专用代码
#endif
2
3
4
5
6
7
8