21-内存管理

105 阅读10分钟

内存管理 (Memory Management)

跟OC一样, Swift也是采取基于引用计数的ARC内存管理方案 (针对堆空间)

Swift的ARC中有3种引用

  • 强引用 (strong reference): 默认情况下,引用都是强引用

  • 弱引用 (weak reference): 通过weak定义弱引用 对于弱引用来说弱引用的变量必须是可选类型的va.因为实例销毁后,ARC会自动将弱引用设置为nil,还有一点ARC自动给弱引用设置nil时,不会触发属性观察器

  • 无主引用 (unowned reference): 通过unowned定义无主引用,无主引用不会产生强引用,实例销毁后仍然存储着实例的内存地址 (类似于OC中的unsafe_unretained),试图在实例销毁后访问无主引用,会产生运行时错误 (野指针),会报这个错误Fatal error: Attempted to read an unowned reference but object 0x0 was already deallocated

weak, unowned 的使用限制

weak, unowned 只能用在类实例上面

protocol Livable : AnyObject {}
class Person {}

weak var p0: Person?
weak var p1: AnyObject?
// 如果协议去掉AnyObject就会报错,因为内存管理只能针对类,对象这些来说
weak var p2: Livable?

unowned var p10: Person?
unowned var p11: AnyObject?
unowned var p12: Livable?

Autoreleasepool

public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result
autoreleasepool {
    let p = MJPerson(age: 20, name: "Jack")
    p.run()
}

循环引用(Reference Cycle)

  • weak、unowned 都能解决循环引用的问题,unowned 要比 weak 少一些性能消耗
  • 在生命周期中可能会变为 nil 的使用 weak
  • 初始化赋值后再也不会变为 nil 的使用 unowned

image.png

闭包的循环引用

闭包表达式默认会对用到的外层对象产生额外的强引用(对外层对象进行了 retain 操作)

下面代码会产生循环引用,导致 Person 对象无法释放(看不到 Person 的 deinit 被调用)

class Person {
    var fn: (() -> ())?
    func run() { print("run") }
    deinit { print("deinit") }
}
func test() {
    let p = Person()
    p.fn = { p.run() }
}
test()

p 强引用着fn,fn强引用着p,导致循环引用。

p.fn = {
    [weak p] in
    p?.run()
}

p.fn = {
    [unowned p] in
    p.run()
}

p.fn = {
  // 可以重新命名
    [weak wp = p, unowned up = p, a = 10 + 20] in
    wp?.run()
}

上面的 [weak p]代表的捕获列表,in 代表的是捕获列表和函数体隔离开 还有一个细节,就是p?.run()代表这个是可选链,p有可能被置为nil,所以要用?,而unowned不会,不需要。

如果捕获列表和参数列表一块出现该怎么写呢?

class Person {
    var fn: ((Int) -> ())?
    func run() { print("run") }
    deinit { print("deinit") }
}

func test() {
    let p = Person()
    p.fn = {
     // 参考下面的写法写
        [weak p](age) in
        p?.run()
    }
}

test()

可以参照上面的写法

如果想在定义闭包属性的同时引用 self,这个闭包必须是 lazy 的(因为在实例初始化完毕之后才能引用 self)

左边的闭包 fn 内部如果用到了实例成员(属性、方法),编译器会强制要求明确写出 self

class Person {
    lazy var fn: (() -> ()) = {
        [weak self] in
        self?.run()
    }
    func run() { print("run") }
    deinit { print("deinit") }
}

因为之前学过的初始化的过程,只有在属性啥都初始化才能用self,所以肯定不能直接对fn里面直接使用self,加上lazy后,是用到才初始化这个方法或者属性,self已经初始化完了,可以用self,但是里面还有循环引用的问题,需要用 weak来处理

class Person {
    var age: Int = 0
    lazy var getAge: Int = {
        self.age
    }()
    deinit { print("deinit") }
}

如果 lazy 属性是闭包调用的结果,那么不用担心循环引用的问题(因为闭包调用后,生命周期就结束了),注意看 lazy var getAge: Int实际上调用的结果。

非逃逸闭包、逃逸闭包

非逃逸闭包、逃逸闭包,一般都是当做参数传递给函数。可以换句话说逃逸闭包和非逃逸闭包一般都是针对于闭包作为函数函数的时候才这么分类。

  • 非逃逸闭包:闭包调用发生在函数结束前,闭包调用在函数作用域内
  • 逃逸闭包:闭包有可能在函数结束后调用,闭包调用逃离了函数的作用域,需要通过 @escaping 声明。就是说函数执行完,传进去的闭包可能还会被执行,就叫做逃逸闭包。
import Dispatch
typealias Fn = () -> ()

// fn 是非逃逸闭包
func test1(_ fn: Fn) { fn() }

// fn 是逃逸闭包
var gFn: Fn?
func test2(_ fn: @escaping Fn) { gFn = fn }

// fn 是逃逸闭包
func test3(_ fn: @escaping Fn) {
    DispatchQueue.global().async {
        fn()
    }
}
class Person {
    var fn: Fn      // fn 是逃逸闭包

    init(fn: @escaping Fn) {
        self.fn = fn
    }

    func run() {
        // DispatchQueue.global().async 也是一个逃逸闭包
        // 它用到了实例成员(属性、方法),编译器会强制要求明确写出 self
        DispatchQueue.global().async {
            self.fn()
        }
    }
}

上面是逃逸闭包,所以必须写上self,因为逃逸闭包对self的声明周期产生了影响

do 创建局部作用域

do {
var age = 10
}
do {
var age = 10
}

do单独使用,用于创建类似于OC

内存访问冲突 (Conflicting Access to Memory)

内存访问冲突会在两个访问满足下列条件时发生:

  • 至少一个是写入操作
  • 它们访问的是同一块内存
  • 它们的访问时间重叠 (比如在同一个函数内) 发生内存访问冲突可能会在编译时报错,可能在运行时报错
// 不存在内存访问冲突
func plus(_ num: inout Int) -> Int { num + 1 }
var number = 1
number = plus(&number)
// 存在内存访问冲突
// Simultaneous accesses to 0x0, but modification requires exclusive access
var step = 1
func increment(_ num: inout Int) { num += step }
// 这句代码的本质是 step = step + step,在满足上面的条件,而且是访问时间上有重叠
increment(&step)
// 解决内存访问冲突
var copyOfStep = step
increment(&copyOfStep)
step = copyOfStep

下面再看几个内存冲突的例子

func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y
    x = sum / 2
    y = sum - x
}
var num1 = 42
var num2 = 30
balance(&num1, &num2) // OK
// 这句会出问题是因为num1 = sum - num1
balance(&num1, &num1) // Error
struct Player {
    var name: String
    var health: Int
    var energy: Int
    mutating func shareHealth(with teammate: inout Player) {
        balance(&teammate.health, &health)
    }
}
var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria) // OK
// 和上面的num1差不多原因
oscar.shareHealth(with: &oscar) // Error

var tulpe = (health: 10, energy: 20)
// Error,都是访问tulpe,还是同一个内存空间,还会报错
balance(&tulpe.health, &tulpe.energy)

var holly = Player(name: "Holly", health: 10, energy: 10)
// Error,都是访问holly,还是同一个内存空间,还会报错
balance(&holly.health, &holly.energy)

如果下面的条件可以满足,就说明重叠访问结构体的属性是安全的

  1. 你只访问实例存储属性,不是计算属性或者类属性
  2. 结构体是局部变量而非全局变量
  3. 结构体要么没有被闭包捕获要么只被非逃逸闭包捕获
// Ok
func test() {
    var tulpe = (health: 10, energy: 20)
    balance(&tulpe.health, &tulpe.energy)

    var holly = Player(name: "Holly", health: 10, energy: 10)
    balance(&holly.health, &holly.energy)
}
test()

上面的报错搞不懂没问题,后续看到报错知道大概啥原因就行

指针

Swift中也有专门的指针类型,这些都被定性为"Unsafe"(不安全的),常见的有以下4种类型

  • UnsafePointer 类似于 const Pointee *
  • UnsafeMutablePointer 类似于 Pointee *
  • UnsafeRawPointer 类似于 const void *
  • UnsafeMutableRawPointer 类似于 void *

上面四种,第一种可以类比为 const NSString *,第二种可以类比为NSString *,第一种指针指向一个地址后,不能重新指向,而第二个可以重新指向。 第3,4种代表的是不说指针类型,也就是说我不知道指针里面存储的是什么类型的变量的地址,也就是说存和取的时候都需要明确

    var age = 10
    // 用泛型表示指针指向的是Int类型地址,地址指向可变
    func test1(_ ptr: UnsafeMutablePointer<Int>) {
        ptr.pointee += 10
    }
    // 用泛型表示指针指向的是Int类型地址,地址指向不可变
    func test2(_ ptr: UnsafePointer<Int>) {
        print(ptr.pointee)
    }
    test1(&age)
    test2(&age) // 20
    print(age) // 20
    var age = 10
    // 用void*表示可以指向任何内存单元,地址指向可变,用store去存储
    func test3(_ ptr: UnsafeMutableRawPointer) {
        ptr.storeBytes(of: 20, as: Int.self)
    }
    func test4(_ ptr: UnsafeRawPointer) {
    // 用load 去加载
        print(ptr.load(as: Int.self))
    }
    test3(&age)
    test4(&age) // 20
    print(age) // 20

指针应用举例

var arr = NSArray(objects: 11, 22, 33, 44)
arr.enumerateObjects { (obj, idx, stop) in
    print(idx, obj)
    if idx == 2 { // 下标为2就停止遍历
        stop.pointee = true
    }
}
    
var arr = NSArray(objects: 11, 22, 33, 44)
for (idx, obj) in arr.enumerated() {
    print(idx, obj)
    if idx == 2 {
        break
    }
}

获得指向某个变量的指针

 var age = 11

// 获取指向 age 的可变类型指针
var ptr1 = withUnsafeMutablePointer(to: &age) { $0 }
// 获取指向 age 的不可变类型指针
var ptr2 = withUnsafePointer(to: &age) { $0 }

// 通过 ptr1 修改 age 的值
ptr1.pointee = 22
// 打印 ptr2 指向的值,结果为 22
print(ptr2.pointee) // 22
// 打印 age 的值,结果为 22
print(age) // 22

// 获取指向 age 的可变原始指针
var ptr3 = withUnsafeMutablePointer(to: &age) { UnsafeMutableRawPointer($0) }
// 获取指向 age 的不可变原始指针
var ptr4 = withUnsafePointer(to: &age) { UnsafeRawPointer($0) }

// 通过 ptr3 存储字节数据,将 age 的值修改为 33
ptr3.storeBytes(of: 33, as: Int.self)
// 打印 ptr4 加载的字节数据,并解释为 Int 类型,结果为 33
print(ptr4.load(as: Int.self)) // 33
// 打印 age 的值,结果为 33
print(age) // 33

获得指向堆空间实例的指针

class Person {}
var person = Person()
var ptr = withUnsafePointer(to: &person) { UnsafeRawPointer($0) }
var heapPtr = UnsafeRawPointer(bitPattern: ptr.load(as: UInt.self))
print(heapPtr!)

上面主要是实现的将UnsafePointer类型转化为UnsafeRawPointer类型

创建指针

  var ptr = UnsafeRawPointer(bitPattern: 0x100001234)

上面讲的是通过传入一个变量创建一个 UnsafeRawPointer类型,因为你的地址可能是瞎写的,所以返回值是可选类型

    // 创建
    var ptr = malloc(16)
    // 存
    ptr?.storeBytes(of: 11, as: Int.self)
    // 就是从上面的分配的空间偏移8个字节开始存储
    ptr?.storeBytes(of: 22, toByteOffset: 8, as: Int.self)
    // 取
    print((ptr?.load(as: Int.self))!) // 11
    // 和上面一样偏移8的字节开始取
    print((ptr?.load(fromByteOffset: 8, as: Int.self))!) // 22
    // 销毁
    free(ptr)
    // 直接用分配字节的方式初始化
    var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
    //先用8个字节存储int 11
    ptr.storeBytes(of: 11, as: Int.self)
    // 往后面数8个字节的空间开始存储 22
    ptr.advanced(by: 8).storeBytes(of: 22, as: Int.self)
    print(ptr.load(as: Int.self)) // 11
    // 往后面数8个字节的空间开始数22
    print(ptr.advanced(by: 8).load(as: Int.self)) // 22
    ptr.deallocate()
    // 注意一点,这里明确写了是泛型Int,那么后面的容量就代表的是初始化3个int类型
    var ptr = UnsafeMutablePointer<Int>.allocate(capacity: 3)
    // 前8个字节先存储int 11
    ptr.initialize(to: 11)
    // 往后数8个字节存储 22
    ptr.successor().initialize(to: 22)
     // 再往后数8个字节存储 22
    ptr.successor().successor().initialize(to: 33)
    // 这里注意点我们前面已经知道类型是泛型int了
    print(ptr.pointee) // 11
    // 所以这里的 ptr + 1 就代表直接偏移8个字节
    print((ptr + 1).pointee) // 22
    // 这里的ptr + 2 代表的直接偏移16个字节
    print((ptr + 2).pointee) // 33
    // 类似于数组的取法
    print(ptr[0]) // 11
    print(ptr[1]) // 22
    print(ptr[2]) // 33
    // 这种初始化销毁要用这个方法
    ptr.deinitialize(count: 3)
    ptr.deallocate()
class Person {
    var age: Int
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    deinit { print(name, "deinit") }
   }
// 这里的对象类型和上面的int差不多情况
var ptr = UnsafeMutablePointer<Person>.allocate(capacity: 3)
ptr.initialize(to: Person(age: 10, name: "Jack"))
(ptr + 1).initialize(to: Person(age: 11, name: "Rose"))
(ptr + 2).initialize(to: Person(age: 12, name: "Kate"))
// Jack deinit
// Rose deinit
// Kate deinit
ptr.deinitialize(count: 3)
ptr.deallocate()

指针之间的转换

var ptr = UnsafeMutableRawPointer.allocate(byteCount: 16, alignment: 1)
// 这个是用assumingMemoryBound将 UnsafeMutableRawPointer 类型转为UnsafeMutablePointer 类型,用pointee处理
ptr.assumingMemoryBound(to: Int.self).pointee = 11
(ptr + 8).assumingMemoryBound(to: Double.self).pointee = 22.0
// unsafeBitCast是啥意思呢,只把原来存int的内存地址给double了,其实里面还是按照int的类型存储的
print(unsafeBitCast(ptr, to: UnsafePointer<Int>.self).pointee) // 11
print(unsafeBitCast(ptr + 8, to: UnsafePointer<Double>.self).pointee) // 22.0

ptr.deallocate()
  • unsafeBitCast 是忽略数据类型的强制转换,不会因为数据类型的变化而改变原来的内存数据,类似于 C++ 中的 reinterpret_cast
    class Person {}
    var person = Person()
    var ptr = unsafeBitCast(person, to: UnsafeRawPointer.self)
    print(ptr)