iOS 开发手册IOS DEVELOPMENT

CHAPTER 09体验底线 · PERFORMANCE

内存与性能优化

再好看的功能,卡顿、崩溃、发热都会让用户卸载。本章讲透 ARC 与循环引用、值类型内存布局、Instruments 分析、启动优化、列表滚动性能与能耗。

开始阅读 难度高阶 先修全部前置章节 约2.5小时
09.1

内存模型:ARC

Swift 用自动引用计数(ARC)管理 class 实例:每有一个强引用,计数 +1;归零时实例被释放。值类型(struct/enum)不走 ARC,它们随作用域拷贝与销毁。

引用类型才需要关心 ARC

struct/enum 是值语义,不存在"引用计数"概念,也就没有循环引用问题。绝大多数内存 Bug 来自 class 之间的相互强引用。

09.2

循环引用与 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 vs unowned

不确定捕获对象生命周期时用 weak(安全,需判空);能 100% 保证"被捕获对象活得比闭包久"时用 unowned(省一次可选解包)。用错 unowned 会在对象已释放时崩溃。

09.3

值类型内存布局

理解"栈 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。

09.4

Instruments 分析

Instruments 是性能诊断的总入口。选对模板,问题一目了然:Time Profiler 看 CPU 热点,Allocations 看内存分配与泄漏,Leaks 直接标红泄漏对象。

模板查什么
Time Profiler主线程卡顿、CPU 热点函数
Allocations内存峰值、异常大对象、分配频率
LeaksARC 未能释放的循环引用对象
Energy LogCPU/定位/网络引起的耗电
先用工具再优化

不要凭直觉优化。先用 Instruments 找到真实瓶颈(二八定律:20% 代码占 80% 时间),把力气花在热点上,否则是无效优化。

09.5

启动优化

冷启动分 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 内可达标平滑体验。

09.6

列表滚动性能

列表滑动掉帧是最常见体验问题。根因通常是:cell 里做主线程解码/网络、离屏渲染(过多圆角阴影)、反复创建对象。逐条对照优化。

渲染

减少离屏渲染

避免大量 cell 同时 cornerRadius+masksToBounds,用预渲染图片或矢量路径。

线程

图片后台解码

大图解码放到后台,主线程只赋值;用异步图像库默认就做了这件事。

复用

正确复用 cell

用 dequeueReusableCell 复用;在 prepareForReuse 里重置状态,避免残留。

SwiftUI 列表陷阱

List 里内嵌复杂 ForEach + 重计算会在每次刷新时重建。把昂贵计算移出 body,用 .id 稳定标识,必要时用 LazyVStack 懒加载。

09.7

能耗与后台

耗电来自 CPU 峰值、频繁网络、常驻定位与后台唤醒。优化体验同时尊重电量,是专业素养。

  • 批量网络。合并请求、用后台传输会话,避免频繁小包唤醒射频。
  • 定位按需。不需要持续导航时用 requestLocation() 单次获取,而非 startUpdatingLocation() 常驻。
  • 后台任务。用 BGTaskScheduler 把可延迟工作(刷新、清洗)调度到系统合适时机。
  • 降频 CPU。避免主线程长时间高负载运算,必要时降到后台队列并分片。
09.8

常见陷阱与 FAQ

deinit 没被调用就是泄漏吗?

基本是。如果 VC/对象本该释放却没走 deinit,几乎一定有未被打破的强引用环(常见:闭包捕获 self、delegate 用强引用、Timer 强持有 target)。用 Instruments Leaks 或打印 deinit 验证。

为什么列表滑快了就掉帧?

主线程每帧只有约 16ms(60fps)/8ms(120fps)预算。超出即掉帧。用 Time Profiler 抓主线程热点,通常是 cell 配置里的同步解码、Auto Layout 约束过多或离屏渲染。

weak 一定要包 Optional 吗?

weak 因对象可能先释放,必须是可选类型;unowned 是非可选,但要求被引用对象生命周期更长,否则访问已释放对象会崩溃。拿不准就用 weak。

09.9

实战练习与资源

延伸资源