美文网首页
Swift 内存管理

Swift 内存管理

作者: 西风那个吹呀吹 | 来源:发表于2020-10-01 22:34 被阅读0次
    • 跟OC一样,Swift也是采用基于引用计数的ARC内存管理方案(针对堆空间)

    • Swift的ARC中有三种引用

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

    weak/unowned的使用限制

    • 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/unowned都能解决循环引用的问题,unowned要比weak少一些性能消耗

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

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

    闭包的循环引用

    • 闭包表达式默认回对用到的外层对象产生额外的强引用(对外层对象进行了retain操作)
    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的情况,无法释放对象。

    • 在闭包表达式的捕获列表声明weakunowned引用,解决循环引用问题
    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后面的就是函数体。

    • 如果想在定义闭包属性的同时引用self,这个闭包必须是lazy的(因为在实例初始化完毕之后才能引用self
    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安全

    • 如果lazy属性是闭包调用的结果,则不用考虑循环引用的问题(因为闭包调用后,闭包的生命周期就结束了)
    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)
    }
    

    指针

    • Swift中有专门的指针类型,这些都被定性为不安全的(Unsafe),常见有以下4种:
    1. UnsafePointer<Pointee>类似const Pointee *<Pointee>代表泛型)
    2. UnsafeMutablePointer<Pointee>类似Pointee *
    3. UnsafeRawPointer类似const void *
    4. UnsafeMutableRawPointer类似void *

    相关文章

      网友评论

          本文标题:Swift 内存管理

          本文链接:https://www.haomeiwen.com/subject/ouxvuktx.html