Swift 内存管理

2020-10-01  本文已影响0人  西风那个吹呀吹
  1. 强应用(strong reference):默认情况下,引用都是强引用
  1. 弱引用(weak reference):通过weak定义弱引用
  • 必须是可选类型的var,因为实例销毁后,ARC会自动将弱引用设置为nil
  • ARC自动给弱引用设置nil时,不会触发属性观察器
  1. 无主引用(unowned reference):通过unowned定义无主引用
  • 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似OC中的unsafe_unretained
  • 试图在实例销毁后访问无主引用,会产生运行时错误(野指针)

weak/unowned的使用限制

因为一般只有类实例放堆空间,结构体、枚举一般都是不放在堆空间的

class Cat {}
protocol Actions :AnyObject {}

weak var c0: Cat?
weak var c1: AnyObject?
weak var c3: Actions?

unowned var c4: Cat?
unowned var c5: AnyObject?
unowned var c6: Actions?

上面代码编译都是没问题的。AnyObject是可以代表任意类类型,协议Actions也是可以的,因为它后面是Actions :AnyObject,意思就是它的协议只能被类类型遵守。
若协议Actions后面的冒号去掉,c3c6是编译不通过的,因为此协议有可能被结构体、枚举遵守,而weak、unowned只能用在类实例上面,所以编译器提前抛出错误,Swift是强安全语言。


Autoreleasepool

在Swift中,Autoreleasepool是保留的,变成了一个全局的函数:

public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result

使用:

class Cat {
    var name: String?
    init(name:String?) {
        self.name = name;
    }
    func eat() {}
}
autoreleasepool {
    let cat = Cat(name: "zhangsan")
    cat.eat()
}

内存开销较大的场景(比如数千对经纬度数据在地图上绘制公交路线轨迹),可以使用自动释放池。


循环引用(Reference Cycle)

weak在实例销毁的时候又设置了一遍weak应用为nil,所以,性能上多了一丢丢消耗

  1. 在生命周期中可能会变为nil的对象,使用weak
  2. 初始化赋值后再也不会改变为nil的对象,使用unowned

闭包的循环引用

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

运行后,打印结果只有1跟2,没有deinit,说明p对象一直没有被销毁,仔细看,问题出在这里:

p.fn = {
        p.run()
    }

对象p里的fn方法强引用了闭包,而闭包里也强引用了对象p,两者形成循环引用,对象p也就无法释放销毁了。
我们在p.fn处打断点,进入汇编,看看是否有强引用(retain):

强应用下引用计数器变化
我们注释掉上面代码里的p.fn = { p.run() },再看汇编:
注释后的引用计数

可以看出,p.fn = { p.run() }里,闭包对p对象进行了强引用也就是retain操作,构成了引用计数始终为1的情况,无法释放对象。

func test() {
    let p = Person()
    p.fn = {
        [weak p] in
        p?.run()
    }
}

func test() {
    let p = Person()
    p.fn = {
        [unowned p] in
        p.run()
    }
}

func test() {
    let p:Person? = Person()
    p?.fn = {
        [weak p] in
        p?.run()
    }
}

func test() {
    let p:Person? = Person()
    p?.fn = {
        [unowned p] in
        p?.run()
    }
}

注意:weak弱引用必须是可选类型,所以,对象p后面跟上?
若是unowned修饰pp后面不用跟?,因为p本身就是非可选类型,unowned默认情况下也就是非可选类型,是跟着p走的

class Person {
    var fn:((Int) -> ())?
    func run() {
        print("run")
    }
    deinit {
        print("deinit")
    }
}
func test() {
    let p = Person()
    p.fn = {
        [weak wp = p](age) in
        wp?.run()
    }
}

[weak p]是捕获列表,(age)是参数列表,捕获列表一般是写在参数列表前面的,in后面的就是函数体。

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

这段代码,会打印deinit,说明对象p释放了。
为什么呢?按说对象p有个强引用fn引用了闭包表达式,闭包表达式里也强应用了self,两者形成循环应用,无法释放对象p

因为fnlazy修饰的,也就是说,在未调用p.fn的时候是没有值的,也就说它后面的闭包表达不存在,自然就无法引用self,也就不能造成循环引用。
当第一次调用p.fn()后,才会触发fn的初始化,创建闭包表达式赋值给fn,这里就形成了循环引用。
解决循环引用:

lazy var fn:(() -> ()) = {
        [weak weakSelf = self] in
        weakSelf?.run()
    }

lazy var fn:(() -> ()) = {
        [unowned weakSelf = self] in
        weakSelf.run()
    }

一般用weak,因为weakunowned安全

class Person {
    var age: Int = 10
    lazy var getAge: Int = {
        self.age
    }()
    deinit {
        print("deinit")
    }
}
func test() {
    let p = Person()
    print(p.getAge)
}
test()

打印结果:10 deinit


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

  1. 至少一个是写入操作
  2. 它们访问的是同一块内存
  3. 它们的访问时间重叠(比如在同一个函数内)
//不存在内存访问冲突
func plus(_ num: inout Int) -> Int {
    num + 1
}
var number = 1
number = plus(&number)

//存在内存访问冲突
var step = 1
func increment(_ num: inout Int) {
    //此处编译没问题,但运行报错
    //Simultaneous accesses to 0x100008178, but modification requires exclusive access
    num += step
}
increment(&step)

increment函数内的num += step产生内存冲突,因为num虽然是形参,但外面传的值还是step的内存地址,+=这里就造成了同一时间对同一份内存进行既读又写的操作,所以造成内存冲突。
上例代码解决内存冲突:

var step = 1
func increment(_ num: inout Int) {
    num += step
}
var temp = step
increment(&temp)
step = temp

下面代码也是存在内存冲突:

func sum(_ x: inout Int, _ y: inout Int) {
    x = x + y
}

struct AA {
    var x: Int = 0
    var y: Int = 0
    mutating func add(a: inout AA) {
        sum(&a.x, &a.y)
    }
}
var aa = AA(x: 1, y: 2)
var bb = AA(x: 1, y: 3)
sum(&aa.x, &aa.y) //编译没问题,但内存冲突,运行报错Simultaneous accesses to 0x100008190, but modification requires exclusive access.

这句语句运行报错,是因为aa.x aa.y虽然是两个不同的变量,内存地址也不一样,但是它们是一个整体,都在结构体实例aa的内存空间内,访问它们两个也就是同时访问同一个结构体内存。
所以上面代码存在内存访问冲突。
元组也一样,元组内的不同变量访问,其实也是访问同一块元组内存,只是它们内部变量的地址不同而已,外部存储变量的元组内存空间还是同一份。

  1. 只访问实例存储属性,不是计算属性或者类属性
  2. 结构体是局部变量而非全局变量
  3. 结构体要么没有被闭包捕获要么只被非逃逸闭包捕获
//没问题
func test() {
    var aa = AA(x: 1, y: 2)
    sum(&aa.x, &aa.y)
}

指针

  1. UnsafePointer<Pointee>类似const Pointee *<Pointee>代表泛型)
  2. UnsafeMutablePointer<Pointee>类似Pointee *
  3. UnsafeRawPointer类似const void *
  4. UnsafeMutableRawPointer类似void *
上一篇下一篇

猜你喜欢

热点阅读