Files
knowledge-kit/Chapter1 - iOS/1.121.md
2025-06-23 01:18:55 +08:00

17 KiB
Raw Blame History

内存管理

弱引用

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 方法的
      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 是不会触发的。
      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 只能用在类实例上。比如:

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、eeer 表示主动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 引用是一个不持有对象引用的引用。这意味着它不会增加对象的引用计数。当对象不再被拥有时(即其引用计数为 0weak 引用会自动设置为 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(&copyOfStep) // update the original value step = copyOfStep print(step) // 2