Jacky's blog
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)

Jack Yang

编程; 随笔
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)
  • iOS 学习导航
  • 调研

  • other

    • iOS术语
    • swift语法
    • oc语法
    • 原理
    • 项目构建
    • Swift 开发最佳实践
      • 一、闭包捕获与循环引用
        • 1.1 闭包默认强引用
        • 1.2 捕获列表:[weak self] vs [unowned self]
        • [weak self] 示例
        • [unowned self] 示例
        • 1.3 什么时候必须用 [weak self]?
        • 1.4 循环引用检测工具
      • 二、GCD 与异步编程
        • 2.1 常用队列
        • 2.2 异步派发
        • 2.3 async 为什么能解决视图更新警告?
      • 三、SwiftUI 属性包装器
        • 3.1 常用属性包装器对比
        • 3.2 @StateObject vs @ObservedObject
        • 3.3 @AppStorage 在 ObservableObject 中的坑
      • 四、SwiftUI 视图更新机制
        • 4.1 视图更新触发条件
        • 4.2 视图更新过程
        • 4.3 为什么不能在视图更新过程中发布变化?
        • 4.4 onAppear vs onChange vs task
      • 五、内存管理最佳实践
        • 5.1 单例的使用
        • 5.2 避免强引用循环
        • 5.3 deinit 调试技巧
      • 六、错误处理最佳实践
        • 6.1 try? vs try! vs do-catch
        • 6.2 自定义错误类型
      • 七、线程安全最佳实践
        • 7.1 共享资源保护
        • 7.2 主线程更新 UI
      • 八、代码组织最佳实践
        • 8.1 目录结构
        • 8.2 日志工具
        • 8.3 扩展文件命名
      • 九、性能优化最佳实践
        • 9.1 避免不必要的视图更新
        • 9.2 懒加载
        • 9.3 避免在 body 中做耗时操作
      • 十、调试技巧
        • 10.1 打印调用栈
        • 10.2 测量执行时间
        • 10.3 条件编译
      • 参考资料
    • pod
    • xcode的使用
  • 《iOS》
  • other
Jacky
2026-08-26
目录

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 → 循环引用
}
1
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 时安全跳过
}
1
2
3
4

# [unowned self] 示例

// ✅ 确定 self 不会被释放时使用 [unowned self]
UIView.animate(withDuration: 0.3) { [unowned self] in
    self.view.layoutIfNeeded()  // self 一定存在,直接访问
}
1
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")  // 如果没打印,说明可能有循环引用
}
1
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 秒后执行
}
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
1

原因:SwiftUI 的视图更新在当前 runloop 中同步执行,如果此时发布变化,会导致未定义行为。

解决方案:用 DispatchQueue.main.async 将发布变化推迟到下一个 runloop:

DispatchQueue.main.async { [weak self] in
    self?.objectWillChange.send()  // 下一个 runloop 执行,当前视图更新已完成
}
1
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  // 外部拥有,视图只观察
}
1
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()
        }
    }
}
1
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:

  1. @State 属性变化
  2. @ObservedObject / @StateObject / @EnvironmentObject 的 objectWillChange 触发
  3. @Environment 值变化(如 colorScheme)
  4. 父视图重新渲染(子视图可能重新渲染)
  5. @AppStorage 值变化(在视图中)

# 4.2 视图更新过程

当前 runloop
├── 状态变化(@State / objectWillChange)
├── SwiftUI 标记视图为"需要更新"
├── runloop 结束时,批量执行视图更新
│   ├── 计算 body
│   ├── diff(比较新旧视图)
│   └── 渲染
└── runloop 结束
1
2
3
4
5
6
7
8

# 4.3 为什么不能在视图更新过程中发布变化?

如果在视图更新过程中(比如 body 计算、onAppear)调用 objectWillChange.send():

  1. SwiftUI 正在更新视图,此时又收到"对象将要变化"的通知
  2. 可能导致无限循环、重复更新、或未定义行为
  3. 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() {}
}
1
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")  // 确认对象被释放
}
1
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)  // 出错会崩溃
1
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)"
        }
    }
}
1
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
    }
}
1
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 = "新内容" }
    }
}
1
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/              # 资源文件
1
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")
            }
    }
}
1
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
1
2
3
4
5

# 九、性能优化最佳实践

# 9.1 避免不必要的视图更新

// ✅ 使用 Equatable 避免不必要的更新
struct Item: Identifiable, Equatable {
    let id: UUID
    var title: String
}

// ✅ 使用 .id() 强制视图重建
MyView()
    .id(item.id)  // id 变化时强制重建
1
2
3
4
5
6
7
8
9

# 9.2 懒加载

// ✅ 大列表用 LazyVStack / LazyHStack
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item: item)
        }
    }
}
1
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()
        }
}
1
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)")
    }
}
1
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()
}
1
2
3
4
5
6
7
8
9
10
11
12

# 10.3 条件编译

#if DEBUG
// 调试代码,Release 不会打包
debugPrint("调试信息")
#endif

#if targetEnvironment(simulator)
// 模拟器专用代码
#endif
1
2
3
4
5
6
7
8

# 参考资料

  • Swift 官方文档 (opens new window)
  • SwiftUI 官方文档 (opens new window)
  • Swift 内存管理 (opens new window)
  • Grand Central Dispatch (opens new window)
上次更新: 2026/08/26, 21:22:46
项目构建
pod

← 项目构建 pod→

最近更新
01
brew
09-08
02
位运算参考文档
09-06
03
electron
08-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 Jacky | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式