内存模型:ARC
Swift 用自动引用计数(ARC)管理 class 实例:每有一个强引用,计数 +1;归零时实例被释放。值类型(struct/enum)不走 ARC,它们随作用域拷贝与销毁。
struct/enum 是值语义,不存在"引用计数"概念,也就没有循环引用问题。绝大多数内存 Bug 来自 class 之间的相互强引用。
循环引用与 weak/unowned
当两个 class 实例互相强引用(或闭包强捕获 self),引用计数永远不为 0,内存泄漏。用 weak(可选、可能为 nil)或 unowned(非可选、保证存在)打破环。
class ViewController {
var observer: NSObjectProtocol?
func setup() {
// 闭包默认强捕获 self → 用 [weak self] 打破环
observer = NotificationCenter.default.addObserver(
forName: .didUpdate, object: nil, queue: .main
) { [weak self] _ in
self?.reload() // self 可能已释放,安全可选调用
}
}
deinit { print("VC 释放了") } // 没泄漏时会被调用
}
不确定捕获对象生命周期时用 weak(安全,需判空);能 100% 保证"被捕获对象活得比闭包久"时用 unowned(省一次可选解包)。用错 unowned 会在对象已释放时崩溃。
值类型内存布局
理解"栈 vs 堆""连续内存"有助于写出缓存友好、低分配的代码。struct 通常直接在栈上连续存放,访问快、无引用计数开销。
// 值类型:栈上连续布局,无 ARC 开销
struct Pixel { var r, g, b, a: UInt8 } // 4 字节紧凑排列
var buf = [Pixel](repeating: .init(), count: 1_000_000) // 连续数组,缓存友好
// 引用类型:每个实例在堆上,数组只存指针(间接访问)
class Box { var value: Int = 0 }
var boxes = [Box]() // 存的是指针,访问有间接开销
模型、配置、请求参数默认 struct,既避免循环引用,又提升局部性与性能。仅在需要身份/共享时才用 class。
Instruments 分析
Instruments 是性能诊断的总入口。选对模板,问题一目了然:Time Profiler 看 CPU 热点,Allocations 看内存分配与泄漏,Leaks 直接标红泄漏对象。
| 模板 | 查什么 |
|---|---|
| Time Profiler | 主线程卡顿、CPU 热点函数 |
| Allocations | 内存峰值、异常大对象、分配频率 |
| Leaks | ARC 未能释放的循环引用对象 |
| Energy Log | CPU/定位/网络引起的耗电 |
不要凭直觉优化。先用 Instruments 找到真实瓶颈(二八定律:20% 代码占 80% 时间),把力气花在热点上,否则是无效优化。
启动优化
冷启动分 pre-main(动态链接、静态初始化)与 post-main(AppDelegate/SceneDelegate 到首屏可交互)。优化重点是减少首屏前的同步重活。
// 反例:在 didFinishLaunching 里做同步大活 → 拖慢启动
func application(_ app: UIApplication,
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
setupAnalytics()
preloadHugeDataset() // 同步阻塞,用户干等
return true
}
// 正例:延后到首屏之后、用异步/懒加载
DispatchQueue.global(qos: .utility).async { preloadHugeDataset() }
用 Xcode 的 Launch 模板或 os_signpost 打点,把首屏渲染时间量化。苹果建议冷启动控制在 400ms 内可达标平滑体验。
列表滚动性能
列表滑动掉帧是最常见体验问题。根因通常是:cell 里做主线程解码/网络、离屏渲染(过多圆角阴影)、反复创建对象。逐条对照优化。
减少离屏渲染
避免大量 cell 同时 cornerRadius+masksToBounds,用预渲染图片或矢量路径。
图片后台解码
大图解码放到后台,主线程只赋值;用异步图像库默认就做了这件事。
正确复用 cell
用 dequeueReusableCell 复用;在 prepareForReuse 里重置状态,避免残留。
List 里内嵌复杂 ForEach + 重计算会在每次刷新时重建。把昂贵计算移出 body,用 .id 稳定标识,必要时用 LazyVStack 懒加载。
能耗与后台
耗电来自 CPU 峰值、频繁网络、常驻定位与后台唤醒。优化体验同时尊重电量,是专业素养。
- 批量网络。合并请求、用后台传输会话,避免频繁小包唤醒射频。
- 定位按需。不需要持续导航时用
requestLocation()单次获取,而非startUpdatingLocation()常驻。 - 后台任务。用
BGTaskScheduler把可延迟工作(刷新、清洗)调度到系统合适时机。 - 降频 CPU。避免主线程长时间高负载运算,必要时降到后台队列并分片。
常见陷阱与 FAQ
基本是。如果 VC/对象本该释放却没走 deinit,几乎一定有未被打破的强引用环(常见:闭包捕获 self、delegate 用强引用、Timer 强持有 target)。用 Instruments Leaks 或打印 deinit 验证。
主线程每帧只有约 16ms(60fps)/8ms(120fps)预算。超出即掉帧。用 Time Profiler 抓主线程热点,通常是 cell 配置里的同步解码、Auto Layout 约束过多或离屏渲染。
weak 因对象可能先释放,必须是可选类型;unowned 是非可选,但要求被引用对象生命周期更长,否则访问已释放对象会崩溃。拿不准就用 weak。