mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
644 lines
17 KiB
Markdown
644 lines
17 KiB
Markdown
# Swift 内存管理
|
||
|
||
|
||
## 弱引用
|
||
Swift 和 OC 都是通过引用计数方式来管理内存的。
|
||
|
||
Swift 的 ARC 存在3种情况:
|
||
|
||
- 强引用(strong reference)。默认情况下,都是强引用。
|
||
当一个强指针离开作用域后,会自动释放对象,调用 deinit 方法。
|
||
```swift
|
||
class Person {
|
||
deinit {
|
||
print("Person deinit")
|
||
}
|
||
}
|
||
|
||
func test () {
|
||
let p: Person = Person()
|
||
}
|
||
|
||
print("1")
|
||
test()
|
||
print("2")
|
||
// console
|
||
1
|
||
Person deinit
|
||
2
|
||
```
|
||
|
||
- 弱引用(weak reference)。通过 weak 定义弱引用。**必须是可选类型,因为实例销毁后,ARC 会自动将弱引用设置为 nil**。
|
||
```swift
|
||
weak var p: Person? = Person()
|
||
```
|
||
- 弱引用如果被设置为 nil,是不会触发属性观察器的 willSet、didSet 方法的
|
||
```swift
|
||
class Dog {
|
||
deinit {
|
||
print("Dog deinit")
|
||
}
|
||
}
|
||
|
||
class Person {
|
||
weak var dog: Dog? {
|
||
willSet {
|
||
print("willSet")
|
||
}
|
||
didSet {
|
||
print("didSet")
|
||
}
|
||
}
|
||
deinit {
|
||
print("Person deinit")
|
||
}
|
||
}
|
||
|
||
func test () {
|
||
let p: Person = Person()
|
||
p.dog = Dog()
|
||
print(p)
|
||
}
|
||
|
||
|
||
print("1")
|
||
test()
|
||
print("2")
|
||
// console
|
||
1
|
||
willSet // 这里的触发是 test 方法里,给 person 对象设置了 dog 属性时触发的。但是 weak 指针设置为 nil 的时候没有触发属性观察器
|
||
didSet
|
||
Dog deinit
|
||
SwiftDemo.Person
|
||
Person deinit
|
||
2
|
||
```
|
||
换一种写法。可以发现在 init 方法里面,属性观察器 willSet、didSet 是不会触发的。
|
||
```swift
|
||
class Dog {
|
||
deinit {
|
||
print("Dog deinit")
|
||
}
|
||
}
|
||
|
||
class Person {
|
||
|
||
weak var dog: Dog? {
|
||
willSet {
|
||
print("willSet")
|
||
}
|
||
didSet {
|
||
print("didSet")
|
||
}
|
||
}
|
||
|
||
init (dog: Dog?) {
|
||
self.dog = dog
|
||
}
|
||
deinit {
|
||
print("Person deinit")
|
||
}
|
||
}
|
||
|
||
func test () {
|
||
let p: Person = Person(dog: Dog())
|
||
print(p)
|
||
}
|
||
|
||
|
||
print("1")
|
||
test()
|
||
print("2")
|
||
// console
|
||
1
|
||
Dog deinit
|
||
SwiftDemo.Person
|
||
Person deinit
|
||
2
|
||
```
|
||
- 无主引用(unowned reference)。通过 unowned 定义无主引用
|
||
- 不会产生强引用,非可选类型。实例销毁后仍然存储着实例的内存地址,类似 OC 的 `unsafe_retained`
|
||
- 如果在实例销毁后访问无主引用,会产生野指针错误
|
||
|
||
|
||
weak、unowned 只能用在类实例上。比如:
|
||
```swift
|
||
protocol Liveavle: AnyObject { }
|
||
class Person { }
|
||
|
||
weak var p1: Person?
|
||
weak var p2: AnyObject?
|
||
weak var p3: Liveavle?
|
||
|
||
unowned var p4: Person?
|
||
unowned var p5: AnyObject?
|
||
unowned var p6: Liveavle?
|
||
```
|
||
|
||
|
||
## 循环引用
|
||
weak、unowned 都能解决循环引用问题。但是 weak 由于当对象释放后,会把指针设置为 nil。所以 unowned 会比 weak 的性能更好。
|
||
- 在生命周期中对象可能会变为 nil,推荐使用 weak
|
||
- 初始化赋值后再也不会变为 nil 的对象,推荐使用 unowned
|
||
|
||
## 闭包的循环引用
|
||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftBlockRetainCycle.png" style="zoom:40%">
|
||
上面的代码会发生循环引用,会导致局部变量的 p 无法释放(看不到 Person 的 deinit 方法调用)
|
||
|
||
解法:
|
||
- **在闭包表达式的捕获列表声明 weak 或者 unowned 引用,解决循环引用的问题**
|
||
因为在闭包里,声明的捕获列表中将 p 用 weak 修饰,所以可以为 nil。p 使用到的地方必须用 `p?.run()`
|
||
```swift
|
||
class Person {
|
||
var fn:(() -> ())?
|
||
func run () { print("run") }
|
||
deinit { print("deinit") }
|
||
}
|
||
|
||
func test () {
|
||
let p = Person()
|
||
p.fn = {
|
||
[weak p] in
|
||
p?.run()
|
||
}
|
||
}
|
||
|
||
test()
|
||
// deinit
|
||
```
|
||
另一种写法
|
||
```swift
|
||
p.fn = {
|
||
[unowned p] in
|
||
p.run()
|
||
}
|
||
```
|
||
-
|
||
```swift
|
||
class Person {
|
||
lazy var fn:() -> () = {
|
||
self.run()
|
||
}
|
||
func run () { print("run") }
|
||
deinit { print("deinit") }
|
||
}
|
||
```
|
||
|
||
## @escaping
|
||
|
||
- 非逃逸闭包、逃逸闭包,一般都是当作参数传递给函数
|
||
- 非逃逸闭包:闭包调用发生在函数结束前,闭包调用在函数作用域内
|
||
- 逃逸闭包:闭包油可能在函数结束后调用, 闭包调用逃离了函数的作用域,需要通过 `@eascaping` 声明
|
||
|
||
```swift
|
||
typealias Fn = () -> ()
|
||
var globalFn:Fn?
|
||
func setFn(_ fn: @escaping Fn) {
|
||
globalFn = fn
|
||
}
|
||
setFn {
|
||
print("Hello world")
|
||
}
|
||
globalFn?() // Hello world
|
||
```
|
||
|
||
|
||
|
||
注意点:逃逸闭包不可以捕获 `inout` 参数
|
||
|
||
```swift
|
||
typealias Fn = () -> ()
|
||
func other1(_ fn: Fn) {
|
||
fn()
|
||
}
|
||
|
||
func other2(_ fn: @escaping Fn) {
|
||
fn()
|
||
}
|
||
|
||
func test(value: inout Int) -> Fn {
|
||
other1 {
|
||
value += 1
|
||
}
|
||
other2 { // compile error:Escaping closure captures 'inout' parameter 'value'
|
||
value += 1
|
||
}
|
||
func add() {
|
||
value += 1
|
||
}
|
||
return add // compile error:Escaping closure captures 'inout' parameter 'value'
|
||
}
|
||
```
|
||
|
||
原因:因为 `inout` 参数的本质是要求函数在调用期间直接操作变量的内存地址,而逃逸闭包可能会在函数返回后的任何时刻调用(不确定),这时 `inout` 参数所在的内存地址可能已经不再有效或者已经被其他值覆盖。因此,允许逃逸闭包捕获 `inout` 参数会导致潜在的数据不一致和安全问题。
|
||
|
||
|
||
|
||
|
||
|
||
## 内存访问冲突
|
||
|
||
Confilicting Access to Memory, 内存访问冲突发生在:
|
||
|
||
- 至少一个是写入操作
|
||
- 它们访问的是同一块内存
|
||
- 它们的访问时间重叠(比如在同一个函数内)
|
||
|
||
|
||
|
||
Demo1:
|
||
|
||
```swift
|
||
var step = 1
|
||
func increament(_ num: inout Int) {
|
||
num += step
|
||
}
|
||
increament(&step)
|
||
```
|
||
|
||
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftConflictingAccessToMemoryDemo.png" style="zoom:25%">
|
||
|
||
解决办法就是打破3个条件之一。显然不可以换函数,只有改变「同时访问一块内存地址」这个条件了
|
||
|
||
```swift
|
||
var step = 1
|
||
func increament(_ num: inout Int) {
|
||
num += step
|
||
}
|
||
var stepCopy = step
|
||
increament(&stepCopy)
|
||
step = stepCopy
|
||
```
|
||
|
||
|
||
|
||
Demo2:
|
||
|
||
```swift
|
||
func balance(_ x: inout Int, _ y: inout Int) {
|
||
let sum = x + y
|
||
x = sum/2
|
||
y = sum - x
|
||
}
|
||
|
||
var num1 = 1
|
||
var num2 = 2
|
||
balance(&num1, &num2) //
|
||
balance(&num1, &num1) // compile error: Inout arguments are not allowed to alias each other
|
||
```
|
||
|
||
Demo3: 下面代码虽然看着传入的是不同内存地址,但是 health 和 power 都属于元祖,还是同一个内存地址。
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftConflictingAccessToMemoryDemo2.png" style="zoom:25%">
|
||
|
||
|
||
|
||
如何解决?
|
||
|
||
Swift 规定以下 case,就说明重叠访问结构体的属性就是安全的
|
||
|
||
- 只访问实例存储属性,不是计算属性或者类属性
|
||
- 结构体是局部变量而非全局变量
|
||
- 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获
|
||
|
||
```swift
|
||
func balance(_ x: inout Int, _ y: inout Int) {
|
||
let sum = x + y
|
||
x = sum/2
|
||
y = sum - x
|
||
}
|
||
func testConflictingAccessToMemory() {
|
||
var tumple = (health: 100, power: 100)
|
||
balance(&tumple.health, &tumple.power)
|
||
}
|
||
testConflictingAccessToMemory()
|
||
```
|
||
|
||
|
||
|
||
|
||
|
||
## 指针
|
||
|
||
Swift 也有专门的指针类型,都被定义为 Unsafe(不安全的),有:
|
||
|
||
- `UnsafePointer<Person>` 类似于 `const Person *`
|
||
- `UnsafeMutablePointer<Person>` 类似于 `Person *`
|
||
- `UnsafeRawPonter` 类似于 `const void *`
|
||
- `UnsafeMutableRawPonter` 类似于 `void *`
|
||
|
||
|
||
|
||
Demo1
|
||
|
||
因为 changeValue1 的参数是不可变的指针,所以方法内部去修改值,编译器会报错。
|
||
|
||
```swift
|
||
var age = 27
|
||
func changeValue1(_ num: UnsafePointer<Int>) {
|
||
num.pointee = 28 // compile error: Cannot assign to property: 'pointee' is a get-only property
|
||
}
|
||
func changeValue2(_ num: UnsafeMutablePointer<Int>) {
|
||
num.pointee = 28
|
||
}
|
||
changeValue1(&age)
|
||
print(age)
|
||
changeValue2(&age)
|
||
print(age)
|
||
```
|
||
|
||
- `changeValue1` 的参数是不可变的指针,所以方法内部去修改值,编译器会报错
|
||
- `changeValue2` 的参数是可变的指针,所以方法内部去修改值,编译没问题
|
||
- 指针加了泛型,访问真实的值可以通过 `指针.pointee` 去访问
|
||
|
||
|
||
|
||
Demo2
|
||
|
||
```swift
|
||
func changeValue3(_ num: UnsafeRawPointer) {
|
||
let value = num.load(as: Int.self)
|
||
print("value is \(value)")
|
||
}
|
||
func changeValue4(_ num: UnsafeMutableRawPointer) {
|
||
num.storeBytes(of: 30, as: Int.self)
|
||
}
|
||
changeValue3(&age)
|
||
print(age)
|
||
changeValue4(&age)
|
||
print(age)
|
||
// console
|
||
value is 27
|
||
27
|
||
30
|
||
```
|
||
|
||
- `changeValue3` 传递了不可变的原始指针,所以访问内存上的值,需要程序员自己指定访问的数据类型。使用 `指针.load(as: 数据类型.self)` 这种格式
|
||
- `changeValue4` 传递了可变的原始指针,所以修改内存上的值,需要程序员自己指定访问的数据类型。使用 `指针.storeBytes(of: 值, as: 数据类型.self)` 这种格式
|
||
|
||
|
||
|
||
系统使用场景
|
||
|
||
```swift
|
||
import Foundation
|
||
var objcArray = NSArray(objects: 10, 11, 12, 13)
|
||
objcArray.enumerateObjects { element, idx, stop in
|
||
print("element is \(element) at index \(idx)")
|
||
}
|
||
print("--------------------")
|
||
objcArray.enumerateObjects { element, idx, stop in
|
||
if idx == 1 {
|
||
stop.pointee = true
|
||
}
|
||
print("element is \(element) at index \(idx)")
|
||
}
|
||
element is 10 at index 0
|
||
element is 11 at index 1
|
||
element is 12 at index 2
|
||
element is 13 at index 3
|
||
--------------------
|
||
element is 10 at index 0
|
||
element is 11 at index 1
|
||
```
|
||
|
||
tips: 不可以在数组 `enumerateObjects` 方法中使用 break,否则编译器会提示 `Unlabeled 'break' is only allowed inside a loop or switch, a labeled break is required to exit an if or do`
|
||
|
||
|
||
|
||
## 获取某个变量的指针
|
||
|
||
`withUnsafePointer` `withUnsafeMutablePointer` 可以获取到不可变、可变的指针
|
||
|
||
```swift
|
||
var age = 27
|
||
let pointer = withUnsafePointer(to: &age) { pointer in
|
||
let address = UnsafeRawPointer(pointer).load(as: Int.self)
|
||
print("Memory address is \(pointer), the value is \(address)")
|
||
return pointer
|
||
}
|
||
var ptr = withUnsafePointer(to: &age) { $0 }
|
||
print(ptr)
|
||
// console
|
||
Memory address is 0x000000010000c208, the value is 27
|
||
0x000000010000c208
|
||
0x000000010000c208
|
||
```
|
||
|
||
继续添加代码,利用指针修改值,编译器报错 `Cannot assign to property: 'pointee' is a get-only property`
|
||
|
||
```swift
|
||
ptr.pointee = 28 // Cannot assign to property: 'pointee' is a get-only property
|
||
```
|
||
|
||
再修改代码
|
||
|
||
```swift
|
||
var mutablePtr = withUnsafeMutablePointer(to: &age) { $0 }
|
||
mutablePtr.pointee = 28
|
||
print("mutable address is \(mutablePtr), value is \(age)")
|
||
// console
|
||
mutable address is 0x000000010000c218, value is 28
|
||
```
|
||
|
||
说明:
|
||
|
||
```
|
||
let pointer = withUnsafePointer(to: &age) { pointer in
|
||
return pointer
|
||
}
|
||
```
|
||
|
||
等价于下面的写法($0 是第一个参数,return 可以省略)
|
||
|
||
```swift
|
||
let pointer = withUnsafePointer(to: &age) { pointer in
|
||
return $0
|
||
}
|
||
let pointer = withUnsafePointer(to: &age) { $0 }
|
||
```
|
||
|
||
|
||
|
||
那如何获取不可变和可变的 rawPointer
|
||
|
||
```swift
|
||
let rawPointer: UnsafeRawPointer = withUnsafePointer(to: &age) {
|
||
UnsafeRawPointer($0)
|
||
}
|
||
print("Raw pointer address is \(rawPointer), the value is \(rawPointer.load(as: Int.self))")
|
||
|
||
let mutableRawPointer: UnsafeMutableRawPointer = withUnsafeMutablePointer(to: &age) {
|
||
UnsafeMutableRawPointer($0)
|
||
}
|
||
print("Mutable raw pointer address is \(rawPointer), the value is \(rawPointer.load(as: Int.self))")
|
||
|
||
mutableRawPointer.storeBytes(of: 28, as: Int.self)
|
||
print(age)
|
||
|
||
// console
|
||
Raw pointer address is 0x000000010000c218, the value is 27
|
||
Mutable raw pointer address is 0x000000010000c218, the value is 27
|
||
28
|
||
```
|
||
|
||
|
||
|
||
pointer、pointee,英语中 er、ee,er 表示主动,ee 表示被动,分别是:指针、被指向的对象。
|
||
|
||
上述方式获取的都是指针变量的地址值,而不是堆空间对象的地址值。
|
||
|
||
|
||
|
||
|
||
|
||
## 获取堆空间对象的指针
|
||
|
||
先获取 `UnsafeRawPointer`,然后利用 `UnsafeRawPointer(bitPattern:**)` 获取堆空间对象的地址值
|
||
|
||
```swift
|
||
class Person {
|
||
var age: Int
|
||
init(age: Int) {
|
||
self.age = age
|
||
}
|
||
}
|
||
var p: Person = Person(age: 27)
|
||
var ptr1 = withUnsafePointer(to: p) { UnsafeRawPointer($0) }
|
||
var personHeapAddress = ptr1.load(as: UInt.self)
|
||
var ptr2 = UnsafeRawPointer(bitPattern: personHeapAddress)
|
||
print(ptr2)
|
||
print(Mems.ptr(ofRef: p))
|
||
```
|
||
|
||
|
||
|
||
## 创建指针
|
||
|
||
创建内存方法1: `malloc`
|
||
|
||
```swift
|
||
import Foundation
|
||
var ptr = malloc(16)
|
||
print("malloc address is \(ptr)")
|
||
// 存
|
||
ptr?.storeBytes(of: 10, as: Int.self)
|
||
ptr?.storeBytes(of: 20, toByteOffset: 8, as: Int.self)
|
||
|
||
let firstValue = (ptr?.load(as: Int.self))!
|
||
let secondValue = (ptr?.load(fromByteOffset: 8, as: Int.self))!
|
||
print("The first part is \(firstValue), second part is \(secondValue)")
|
||
|
||
free(ptr)
|
||
// console
|
||
malloc address is Optional(0x0000600000008040)
|
||
The first part is 10, second part is 20
|
||
```
|
||
|
||
创建内存方法2: `UnsafeMutableRawPointer.allocate(byteCount: 字节数, alignment: 内存对齐)`
|
||
|
||
```swift
|
||
// 创建
|
||
let ptr: UnsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
|
||
print("malloc address is \(ptr)")
|
||
ptr.storeBytes(of: 10, as: Int.self)
|
||
// ptr.storeBytes(of: 20, toByteOffset: 8, as: Int.self)
|
||
ptr.advanced(by: 8).storeBytes(of: 20, as: Int.self)
|
||
|
||
let firstValue = ptr.load(as: Int.self)
|
||
let secondValue = ptr.load(fromByteOffset: 8, as: Int.self)
|
||
print("The first part is \(firstValue), second part is \(secondValue)")
|
||
// 释放
|
||
ptr.deallocate()
|
||
|
||
// console
|
||
malloc address is 0x0000000100604370
|
||
The first part is 10, second part is 20
|
||
```
|
||
|
||
上面的 `ptr.storeBytes(of: 20, toByteOffset: 8, as: Int.self)` 写法等价于 `ptr.advanced(by: 8).storeBytes(of: 20, as: Int.self)`
|
||
|
||
|
||
|
||
创建内存方法3: `UnsafeMutablePointer<Int>.allocate(capacity: 2)` 创建2* 8 Byte 大小的内存
|
||
|
||
```swift
|
||
import Foundation
|
||
var ptr = UnsafeMutablePointer<Int>.allocate(capacity: 2)
|
||
print("malloc address is \(ptr)")
|
||
// 初始化赋值
|
||
//ptr.pointee = 27
|
||
ptr.initialize(to: 27)
|
||
|
||
ptr.successor().initialize(to: 10)
|
||
|
||
// 访问
|
||
print(ptr.pointee)
|
||
print((ptr + 1).pointee)
|
||
|
||
print(ptr[0])
|
||
print(ptr[1])
|
||
|
||
print(ptr.pointee)
|
||
print(ptr.successor().pointee)
|
||
|
||
ptr.deinitialize(count: 2)
|
||
ptr.deallocate()
|
||
// console
|
||
malloc address is 0x0000000100604190
|
||
27
|
||
10
|
||
27
|
||
10
|
||
27
|
||
10
|
||
```
|
||
|
||
|
||
|
||
## 指针之间的转换
|
||
|
||
`unsafeBitCast` 是忽略数据类型的强制转换,不会因为数据类型的变化而改变原来的内存数据。类似 C++ 中的 `reterpret_cast`
|
||
|
||
|
||
## 内存泄漏
|
||
weak 和 unowned 是两种用于处理引用循环(retain cycles)的关键字,它们主要用在类的属性中,以确保对象之间的引用不会导致内存泄漏。这两种引用类型都用于表示对另一个对象的非拥有(non-owning)引用,但它们在行为上有所不同。
|
||
|
||
weak 引用是一个不持有对象引用的引用。这意味着它不会增加对象的引用计数。当对象不再被拥有时(即其引用计数为 0),weak 引用会自动设置为 nil。
|
||
weak 引用通常用于避免循环引用,特别是在闭包或代理模式中。例如,如果你有一个视图控制器(ViewController)和一个代理(Delegate),并且 ViewController 持有一个对 Delegate 的强引用,那么为了避免循环引用,Delegate 通常会对 ViewController 持有一个 weak 引用。
|
||
|
||
|
||
unowned 引用也是一个不持有对象引用的引用,但它不会在对象被释放时自动设置为 nil。因此,使用 unowned 引用时需要格外小心,因为如果引用的对象被释放了,而你的代码仍然试图访问它,那么你的程序将会崩溃。
|
||
通常,当你确信引用的对象在其生命周期内始终存在时,才会使用 unowned 引用。例如,在一个父对象和子对象的关系中,如果父对象始终在子对象之前存在,并且子对象需要引用父对象,那么子对象可以使用 unowned 引用指向父对象。
|
||
|
||
## inout 参数访问冲突
|
||
|
||
```
|
||
var step = 1
|
||
func increment(_ number: inout Int) {
|
||
number += step
|
||
}
|
||
let rs = increment(&step)
|
||
print(rs)
|
||
```swift
|
||
|
||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftInou tReadWriteError.png" style="zoom:25%">
|
||
|
||
问题本质是:`inout` 关键词代表要对函数的参数进行写操作,而函数内部的实现利用 step 进行反问操作。读写同时存在就有问题。
|
||
|
||
如何解决?产生一个备份,然后调用函数,最后将备份产生的结果覆盖老值
|
||
```
|
||
var step = 1
|
||
func increment(_ number: inout Int) {
|
||
number += step
|
||
}
|
||
// make an explicit copy
|
||
var copyOfStep = step
|
||
// invoke
|
||
increment(©OfStep)
|
||
// update the original value
|
||
step = copyOfStep
|
||
print(step) // 2
|
||
```swift |