内存基础
Swift 内存管理的核心是 ARC(自动引用计数) ,而理解内存的第一步是区分值类型 和引用类型------ 这是内存存储的根本差异。
一、值类型 vs 引用类型(内存存储的核心)
这是 Swift 内存最基础、面试最常考的点,直接决定了数据存在哪里、赋值时的行为。
表格
| 特性 | 值类型(Value Type) | 引用类型(Reference Type) |
|---|---|---|
| 包含类型 | Struct、Enum、Tuple、Int/String 等基本类型 | Class、Closure |
| 内存位置 | 栈(Stack) | 堆(Heap) |
| 赋值行为 | 拷贝整个值(深拷贝) | 拷贝引用地址(浅拷贝) |
| 修改影响 | 仅修改自身副本 | 修改所有引用同一对象的变量 |
代码示例(带注释)
Swift
// 1. 值类型:Struct
struct Person {
var name: String
}
var p1 = Person(name: "小明")
var p2 = p1 // 值拷贝:p2 是 p1 的独立副本
p2.name = "小红"
print(p1.name) // 小明(p1 不受影响)
// 2. 引用类型:Class
class Animal {
var name: String
init(name: String) { self.name = name }
}
var a1 = Animal(name: "猫")
var a2 = a1 // 引用拷贝:a1 和 a2 指向堆上的同一个对象
a2.name = "狗"
print(a1.name) // 狗(a1 也被修改)
二、栈(Stack) vs 堆(Heap)
值类型和引用类型分别存在这两个内存区域,理解它们的差异能帮你明白为什么值类型更快、更安全。
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 管理方式 | 系统自动管理(无需开发者操心) | ARC 管理(需注意循环引用) |
| 速度 | 极快(连续内存,直接存取) | 较慢(需分配、查找内存) |
| 存储内容 | 值类型(Struct、Enum 等) | 引用类型的实例(Class 对象) |
| 生命周期 | 作用域结束自动释放(如函数返回) | 引用计数为 0 时释放 |
| 线程安全 | 天然线程安全(每个线程有独立栈) | 需手动处理线程安全 |
三、ARC(自动引用计数)------ 引用类型的内存管家
ARC 只针对引用类型(Class) ,核心原理是:每个对象有一个「引用计数」,计数为 0 时系统自动释放对象。
1. 引用计数的增减
- 增加 :当有一个新的强引用(
var/let默认都是强引用)指向对象时,计数 +1 - 减少 :当强引用被赋值为
nil、或作用域结束时,计数 -1
代码示例
Swift
class Dog {
var name: String
init(name: String) {
self.name = name
print("\(name) 被创建")
}
deinit {
print("\(name) 被释放")
}
}
// 引用计数变化
var dog1: Dog? = Dog(name: "旺财") // 计数 +1 → 1
var dog2 = dog1 // 计数 +1 → 2
dog1 = nil // 计数 -1 → 1
dog2 = nil // 计数 -1 → 0 → 触发 deinit,打印"旺财 被释放"
四、循环引用与解决
ARC 有一个致命问题:两个对象互相强引用 ,导致双方引用计数都不为 0,永远不会被释放 ------ 这就是内存泄漏。
1. 循环引用的产生
Swift
class Person {
var pet: Dog? // 强引用 Dog
}
class Dog {
var owner: Person? // 强引用 Person
}
// 循环引用:person 和 dog 互相持有,计数都为 1,永远不释放
var person: Person? = Person()
var dog: Dog? = Dog()
person?.pet = dog
dog?.owner = person
person = nil // 计数 -1 → 1(因为 dog 还持有它)
dog = nil // 计数 -1 → 1(因为 person 还持有它)
2. 解决方法:weak(弱引用)或 unowned(无主引用)
两者都不增加引用计数,区别在于:
表格
| 特性 | weak(弱引用) | unowned(无主引用) |
|---|---|---|
| 对象释放后 | 自动置为 nil |
不会置 nil(需确保对象存在,否则崩溃) |
| 类型要求 | 必须是可选类型(?) |
可以是非可选类型 |
| 适用场景 | 两个对象生命周期独立(如主人和宠物) | 一个对象生命周期依赖另一个(如视图和控制器) |
用 weak 解决循环引用
Swift
class Person {
var pet: Dog?
}
class Dog {
weak var owner: Person? // 用 weak 修饰,不增加计数
}
// 现在不会循环引用了
var person: Person? = Person()
var dog: Dog? = Dog()
person?.pet = dog
dog?.owner = person
person = nil // 计数 0 → 释放
dog = nil // 计数 0 → 释放
五、总结
- 值类型 (Struct/Enum)存栈,赋值拷贝,系统自动管理;引用类型(Class)存堆,赋值拷贝引用,ARC 管理。
- 栈 快、系统管、线程安全;堆慢、ARC 管、需注意循环引用。
- ARC 靠引用计数释放对象,强引用增减计数。
- 循环引用 用
weak(可选,自动置nil)或unowned(非可选,需确保存在)解决。
Swift 完整的 5 大内存分区
之前重点讲了栈和堆,这里补全 Swift 程序运行时的完整内存布局,从低地址到高地址依次为:
| 内存分区 | 核心作用 | 存储内容 | 生命周期 | 管理方式 |
|---|---|---|---|---|
| 代码区(Text Segment) | 存放程序执行代码 | 编译后的二进制机器指令,只读不可修改 | App 启动时加载,App 终止时释放 | 系统自动管理 |
| 常量区(Constant Segment) | 存放只读常量 | 字符串常量(如let str = "hello"中的"hello")、数字常量、全局常量 |
App 启动时加载,App 终止时释放 | 系统自动管理 |
| 全局 / 静态区(Data Segment) | 存放全局 / 静态变量 | 分两个子区:・已初始化区(Data):有初始值的全局 / 静态变量・未初始化区(BSS):无初始值的全局 / 静态变量 | App 启动时分配,App 终止时释放 | 系统自动管理 |
| 栈区(Stack) | 存放临时局部数据 | 函数参数、局部值类型变量、函数返回地址、寄存器现场 | 函数进入时分配栈帧,函数返回时自动销毁栈帧,先进后出 | 系统自动管理,无额外开销 |
| 堆区(Heap) | 存放动态分配的对象 | 引用类型实例(Class、Closure)、超出栈存储能力的大尺寸值类型、装箱的协议类型值 | 手动申请 / ARC 管理,引用计数为 0 时释放 | ARC 管理,有内存分配 / 销毁的额外开销 |
补充关键细节:
- iOS 主线程默认栈大小为1MB ,子线程默认栈大小为512KB,栈溢出会直接触发崩溃(如递归死循环)
- 堆上的 Swift 对象有固定的 16 字节基础开销:8 字节的 isa 指针(类型信息)+ 8 字节的引用计数相关信息,最小占用 16 字节内存
二、值类型进阶核心:写时复制(COW, Copy-On-Write)
之前提到值类型赋值会做拷贝,这里纠正一个核心误区:Swift 标准库的核心值类型(Array、String、Dictionary、Set、Data),并不是赋值时立刻深拷贝,而是通过 COW 实现延迟拷贝,大幅优化性能。
1. COW 核心原理
- 当你对一个 COW 类型执行赋值操作(
let arr2 = arr1)时,不会立刻拷贝内存,而是让两个变量共享同一块底层内存,同时通过引用计数标记这块内存的共享次数 - 只有当你对其中一个变量执行修改操作时,才会真正开辟新的内存,拷贝原始内容,实现「只读共享,修改拷贝」
- 全程对开发者透明,无需额外代码,既保留了值类型的语义安全,又避免了无意义的内存拷贝
2. 代码演示
Swift
var arr1 = [1,2,3,4,5]
var arr2 = arr1
// 此时arr1和arr2共享同一块底层内存,无拷贝发生
print(arr1.baseAddress!) // 打印原始内存地址
print(arr2.baseAddress!) // 和arr1地址完全相同
// 对arr2执行修改,触发COW,执行真正的拷贝
arr2.append(6)
// 此时arr2的地址发生变化,拥有了独立的内存副本
print(arr1.baseAddress!) // 地址不变
print(arr2.baseAddress!) // 新的内存地址
print(arr1) // [1,2,3,4,5] 不受影响
3. 关键补充
- 自定义的
Struct/Enum默认没有实现 COW,只有 Swift 标准库的核心集合类型内置了 COW - 面试高频考点:COW 是 Swift 值类型高性能的核心,也是 Swift 推荐优先使用 Struct 的重要原因之一
三、纠正核心误区:值类型一定存在栈上吗?
结论:不是。Swift 编译器会做「内存逃逸分析」,尽可能把值类型分配在栈上,但以下场景,值类型会被分配到堆上:
- 结构体 / 枚举尺寸过大,超出栈的合理存储范围
- 值类型包含协议类型(存在性类型,如
any Equatable),编译期无法确定内存尺寸,需要装箱后存到堆上 - 逃逸闭包捕获了值类型的变量,编译器无法确定其生命周期
- 带
indirect修饰的递归枚举(需要通过指针引用自身,只能存堆上)
补充:即使值类型被分配到堆上,它依然保留值类型的语义(赋值拷贝、独立修改),只是存储位置发生了变化。
四、ARC 进阶原理与细节
1. 引用计数的底层存储
Swift 对象的引用计数分为两部分存储:
- 64 位系统下,对象的
isa指针(Non-pointer isa)中,预留了专门的位域存储内联引用计数,操作速度极快 - 当内联引用计数存满时,会使用SideTable(侧边表) 存储额外的引用计数,同时 SideTable 还负责存储弱引用表的相关信息
2. weak 弱引用的底层实现
- 系统维护了一个全局的弱引用表,是一个哈希表结构
- 当你用
weak修饰一个变量时,会把该变量的内存地址注册到弱引用表中,和目标对象绑定 - 当目标对象的引用计数为 0、即将释放时,系统会自动遍历弱引用表,把所有指向该对象的
weak指针全部置为nil - 这也是为什么
weak修饰的变量必须是可选类型:它会在对象释放时自动变为nil
3. unowned 无主引用的进阶细节
unowned分为两种模式,日常开发默认使用安全模式:
| 类型 | 特性 | 适用场景 | 风险 |
|---|---|---|---|
unowned(safe)(默认) |
不增加引用计数,对象释放后,访问会触发运行时断言,直接崩溃 | 两个对象生命周期强绑定,被引用方生命周期 >= 引用方 | 有安全检查,崩溃有明确提示 |
unowned(unsafe) |
不增加引用计数,对象释放后,指针变为野指针,无安全检查 | 极致性能优化场景,极度不推荐日常使用 | 野指针访问会触发不可预期的崩溃,极难排查 |
4. weak vs unowned 终极选择标准
- 当两个对象的生命周期互相独立 ,被引用的对象可能先于引用方释放 → 必须用
weak - 当两个对象的生命周期强绑定 ,被引用的对象生命周期一定大于等于引用方,引用方离开被引用方就没有存在的意义 → 可以用
unowned - 日常开发优先用
weak,unowned的崩溃风险更高
五、开发中最高频的内存泄漏场景与解决方案
之前只讲了两个类互相强引用的基础场景,这里补充开发中 90% 的内存泄漏都来自的场景,也是面试高频考点。
1. 逃逸闭包的循环引用(最常见)
泄漏原理
- 闭包是引用类型,会强引用捕获到的外部变量
- 如果
self持有了这个闭包,同时闭包内部又强引用了self,就会形成「self → 闭包 → self」的闭环,双方引用计数永远无法归零,造成内存泄漏 - 只有逃逸闭包(@escaping) 会有这个风险,非逃逸闭包不会持有 self,无需处理
解决方案:捕获列表(Capture List)
在闭包开头通过[weak self]/[unowned self]声明捕获规则,打破循环引用。
代码示例
Swift
class ViewController: UIViewController {
// self 强引用了闭包
var completionHandler: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
// 错误写法:闭包强引用self,形成循环引用
completionHandler = {
self.view.backgroundColor = .red
}
// 正确写法:捕获列表用weak self,打破循环
completionHandler = { [weak self] in
// 解包self,避免后续频繁写可选链
guard let self = self else { return }
self.view.backgroundColor = .red
}
}
deinit {
print("ViewController 被释放")
}
}
补充:
- 不是所有闭包都需要加
[weak self]:比如Array.sorted(by:)这类非逃逸闭包、GCD 的一次性async执行的闭包,不会被 self 持有,不会形成循环 - 捕获列表可以同时声明多个弱引用:
[weak self, weak delegate = self.delegate]
2. Timer 定时器的循环引用
泄漏原理
Timer创建时会强引用target,同时RunLoop会强引用激活状态的Timer- 如果
self强引用了Timer,同时Timer的target是self,就会形成「self → Timer → RunLoop → self」的闭环 - 很多人会在
deinit里调用timer.invalidate(),但循环引用会导致deinit永远不会执行,定时器永远无法停止
正确解决方案
Swift
class ViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
// 正确写法:iOS10+ 推荐的block API,配合weak self
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self = self else { return }
print("定时器执行")
}
}
// 正确的停止时机:页面消失时停止定时器
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate()
timer = nil
}
deinit {
print("ViewController 被释放")
}
}
3. NotificationCenter 基于 Block 的监听
泄漏原理
NotificationCenter.default.addObserver(forName:object:queue:using:) 这个 API 会返回一个监听者对象,系统会强引用这个对象;如果 block 内部强引用了self,同时self又强引用了这个监听者对象,就会形成循环引用。
解决方案
block 内加[weak self],同时在合适的时机移除监听者。
六、补充关键语法的内存原理
1. inout 关键字的内存本质
很多人误以为inout是引用传递,其实官方定义是写回拷贝(Copy-In Copy-Out),而非 C++ 的地址引用。
- 函数调用时:将参数的值拷贝到函数内的局部内存(Copy-In)
- 函数执行时:修改这个局部副本
- 函数返回时:将修改后的副本写回原变量的内存地址(Copy-Out)
编译器会对全局变量、存储属性、COW 类型做优化,直接转为内存引用,避免拷贝开销。
- 误区纠正:
inout不是传地址,只是实现了「修改外部变量」的语义,和引用传递有本质区别 - 注意:
inout参数不能有默认值,不能是let常量
2. Swift 中的 autoreleasepool
OC 中常用的自动释放池,在 Swift 中依然适用,用于降低大循环中的内存峰值。
- 适用场景:循环内创建大量临时的大对象(如图片处理、大文件读写),避免循环过程中内存持续上涨
- 语法示例:
Swift
// 大循环处理图片,手动加自动释放池
for _ in 0..<1000 {
autoreleasepool {
// 循环内创建的临时对象,会在本次autoreleasepool结束时统一释放
let image = UIImage(contentsOfFile: "大图片路径")
// 图片处理逻辑
}
}
七、deinit 执行规则与注意事项
deinit是对象的析构函数,是判断对象是否正常释放的核心依据,补充关键规则:
- 执行时机:对象引用计数归零后,内存释放之前,在主线程同步执行
- 执行顺序:子类的
deinit先执行,之后自动调用父类的deinit - 禁止操作:
- 不能手动调用
deinit,只能由系统自动触发 - 不能在
deinit里强引用self,会导致对象复活,内存无法释放 - 不能调用异步方法、不能开启新的 RunLoop,因为对象即将被销毁
- 不能手动调用
- 开发技巧:在
deinit里打印日志,快速判断对象是否正常释放,排查内存泄漏
八、内存问题排查工具
日常开发中,可通过 Xcode 自带工具快速定位内存问题:
- Memory Graph Debugger:Xcode 调试栏的内存图标,可视化展示所有对象的引用关系,一键定位循环引用
- Instruments - Leaks:专业的内存泄漏检测工具,可定位泄漏的对象、内存地址和调用堆栈
- Instruments - Allocations:查看内存分配情况,定位内存峰值和异常上涨的原因
补充总结
- Swift 内存管理的核心是值类型与引用类型的区分,值类型优先栈分配、语义安全,引用类型堆分配、ARC 管理
- COW 是 Swift 值类型高性能的核心,实现了「只读共享,修改拷贝」
- ARC 的核心痛点是循环引用,解决方案是
weak和unowned,日常开发优先用weak - 90% 的内存泄漏来自三个场景:类互相强引用、逃逸闭包强引用 self、Timer 循环引用
- 可通过
deinit日志、Memory Graph、Instruments 快速排查内存问题