17 KiB
内存管理
弱引用
Swift 和 OC 都是通过引用计数方式来管理内存的。
Swift 的 ARC 存在3种情况:
-
强引用(strong reference)。默认情况下,都是强引用。 当一个强指针离开作用域后,会自动释放对象,调用 deinit 方法。
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。
weak var p: Person? = Person()- 弱引用如果被设置为 nil,是不会触发属性观察器的 willSet、didSet 方法的
换一种写法。可以发现在 init 方法里面,属性观察器 willSet、didSet 是不会触发的。
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 2class 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
- 弱引用如果被设置为 nil,是不会触发属性观察器的 willSet、didSet 方法的
-
无主引用(unowned reference)。通过 unowned 定义无主引用
- 不会产生强引用,非可选类型。实例销毁后仍然存储着实例的内存地址,类似 OC 的
unsafe_retained - 如果在实例销毁后访问无主引用,会产生野指针错误
- 不会产生强引用,非可选类型。实例销毁后仍然存储着实例的内存地址,类似 OC 的
weak、unowned 只能用在类实例上。比如:
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
闭包的循环引用
上面的代码会发生循环引用,会导致局部变量的 p 无法释放(看不到 Person 的 deinit 方法调用)
解法:
- 在闭包表达式的捕获列表声明 weak 或者 unowned 引用,解决循环引用的问题
因为在闭包里,声明的捕获列表中将 p 用 weak 修饰,所以可以为 nil。p 使用到的地方必须用
p?.run()
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
另一种写法
p.fn = {
[unowned p] in
p.run()
}
class Person {
lazy var fn:() -> () = {
self.run()
}
func run () { print("run") }
deinit { print("deinit") }
}
@escaping
- 非逃逸闭包、逃逸闭包,一般都是当作参数传递给函数
- 非逃逸闭包:闭包调用发生在函数结束前,闭包调用在函数作用域内
- 逃逸闭包:闭包油可能在函数结束后调用, 闭包调用逃离了函数的作用域,需要通过
@eascaping声明
typealias Fn = () -> ()
var globalFn:Fn?
func setFn(_ fn: @escaping Fn) {
globalFn = fn
}
setFn {
print("Hello world")
}
globalFn?() // Hello world
注意点:逃逸闭包不可以捕获 inout 参数
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:
var step = 1
func increament(_ num: inout Int) {
num += step
}
increament(&step)
解决办法就是打破3个条件之一。显然不可以换函数,只有改变「同时访问一块内存地址」这个条件了
var step = 1
func increament(_ num: inout Int) {
num += step
}
var stepCopy = step
increament(&stepCopy)
step = stepCopy
Demo2:
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 都属于元祖,还是同一个内存地址。
如何解决?
Swift 规定以下 case,就说明重叠访问结构体的属性就是安全的
- 只访问实例存储属性,不是计算属性或者类属性
- 结构体是局部变量而非全局变量
- 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获
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 的参数是不可变的指针,所以方法内部去修改值,编译器会报错。
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
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)这种格式
系统使用场景
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 可以获取到不可变、可变的指针
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
ptr.pointee = 28 // Cannot assign to property: 'pointee' is a get-only property
再修改代码
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 可以省略)
let pointer = withUnsafePointer(to: &age) { pointer in
return $0
}
let pointer = withUnsafePointer(to: &age) { $0 }
那如何获取不可变和可变的 rawPointer
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:**) 获取堆空间对象的地址值
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
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: 内存对齐)
// 创建
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 大小的内存
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