美文网首页
Swift进阶六:可变性和内存

Swift进阶六:可变性和内存

作者: Trigger_o | 来源:发表于2022-04-22 16:07 被阅读0次

    写时复制

    标准库中,内建集合类型,如Array,Dictionary 和 Set 这样的集合类型是通过一种叫做写时复制(copy-on-write) 的技术实现的.

    var x = [1,2,3]
    var y = x
    

    当x赋值给y时,对于值类型我们认为数组被复制,但是实际上并非如此。
    Array 结构体含有指向某个内存的引用。两个数组的引用向的是内存中同一个位置,当我们改变 x 的时候,内存才会真的被复制。昂贵的元素复制操作只在必要的时候发生,也就是我们改变这两个变量的时候发生复制。

    x.append(5) 
    y.removeLast()
    x // [1, 2, 3, 5] 
    y // [1, 2]
    

    这种行为就被称为写时复制。它的工作方式是,每当数组被改变,它首先检查它对存储缓冲区的引用是否是唯一的,如果是,则直接改变,如果不是,则先复制数组,然后改变。

    实现写实复制

    swift Foundation的 Data是值类型,和Array一样

    var input: [UInt8] = [0x0b,0xad,0xf0,0x0d]
    var other: [UInt8] = [0x0d]
    var d = Data(bytes: input)
    var e = d d.append(contentsOf: other) 
    d // 5 bytes 
    e // 4 bytes
    

    使用NSMutableData表现就不一样了,f和g都引用同一个地址

    var f = NSMutableData(bytes: &input, length: input.count)
    var g = f f.append(&other, length: other.count) 
    f // <0badf00d 0d> 
    g // <0badf00d 0d>
    f === g // true
    

    显然,直接把NSMutableData封装进结构体并不能实现值语义 ,结构体被持有时只会对NSMutableData进行浅拷贝.

    为了提供高效的写时复制特性,我们需要知道一个对象 (比如这里的 NSMutableData) 是否是唯一的。如果它是唯一引用,那么我们就可以直接原地修改对象。否则,我们需要在修改前创建对象的复制。在 Swift 中,我们可以使用 isKnownUniquelyReferenced 函数来检查某个引用只有一个持有者。如果你将一个 Swift 类的实例传递给这个函数,并且没有其他变量强引用这个对象的话,函数将返回 true。如果还有其他的强引用,则返回 false。不过,对于Objective-C 的类,它会直接返回 false。

    首先创建一个swift类Box来把目标封装进来,Box可以让isKnownUniquelyReferenced生效.

    final class Box<A>{
        var unbox : A
        init(_ value:A){
            unbox = value
        }
    }
    
    var box = Box(NSMutableData.init())
    print(isKnownUniquelyReferenced(&box)) //true
    
    var y = box
    isKnownUniquelyReferenced(&x) // false
    

    现在写一个MyData来实现NSMutableData的写时复制

    struct MyData {
        private var _data: Box<NSMutableData>
        var _dataForWriting: NSMutableData {
            mutating get {
                if !isKnownUniquelyReferenced(&_data) {
                    _data = Box(_data.unbox.mutableCopy() as! NSMutableData)
                    print("Making a copy")
                }
                return _data.unbox
            }
            
        }
        init() {
            _data = Box(NSMutableData())
            
        }
        init(_ data: NSData) {
            _data = Box(data.mutableCopy() as! NSMutableData)
        }
        
        mutating func append(_ byte: UInt8) {
            var mutableByte = byte
            _dataForWriting.append(&mutableByte, length: 1)
        }
    }
    
    //使用
    var bytes = MyData()
    var copy = bytes
    for byte in 0..<5 as CountableRange<UInt8> {
          print("Appending 0x\(String(byte, radix: 16))")
          bytes.append(byte)
    }
    /*  Appending 0x0 
        Making a copy
        Appending 0x1 
        Appending 0x2 
        Appending 0x3 
        Appending 0x4 */
    

    存储数据的是_data,是由Box封装的;
    _dataForWriting用于外部访问数据,并且处理何时进行写时复制,相当于data的前哨站,append方法只是为了方便使用又封装了一次;
    当需要改变_data时,先由_dataForWriting来处理,添加到_dataForWriting中暂存;
    当执行append时,先get _dataForWriting,判断MyData是否有多个持有者,如果不是,那么直接返回_data,如果是有多个持有者,则先把_dataForWriting中的数据拷贝到_data,再返回,返回的data会被添加新的内容
    _dataForWriting存储公共的内容,结构体本身已经实现了写时复制,这里的目的是实现NSMutableData的写时复制.

    当你定义你自己的结构体和类的时候,需要特别注意那些原本就可以复制和可变的行为。结构体应该是具有值语义的。当你在一个结构体中使用类时,我们需要保证它确实是不可变的。如果办不到这一点的话,我们就需要 (像上面那样的) 额外的步骤。或者就干脆使用一个类,这样我们的数据的使用者就不会期望它表现得像一个值。

    闭包和可变性

    首先我们知道闭包是引用类型.

    var i = 0
    func uniqueInteger() -> Int { 
          i += 1
           return i
     }
    
    let otherFunction: () -> Int = uniqueInteger
    

    调用 otherFunction 所发生的事情与我们调用 uniqueInteger 是完全一样的。这对所有的闭包和函数来说都是正确的:如果我们传递这些闭包和函数,它们会以引用的方式存在,并共享同样的状态。
    如果想要拥有多个otherFunction,就得创建多个uniqueInteger,

    func uniqueIntegerProvider() -> () -> Int { 
        var i = 0
         return {
             i += 1
             return i
           }
     }
    

    现在uniqueIntegerProvider不返回i,直接返回闭包.这里即便uniqueIntegerProvider离开了作用域,i依然存在
    这是因为闭包是这样存储数据:
    值类型一般会存储在栈中,当闭包捕获变量或常量时,会被放到堆中,即使定义这些常量和变量的原作用域已经不存在了,闭包仍能够在其函数体内引用和修改这些值.
    对应uniqueIntegerProvider,每次执行闭包的时候,都会重新开辟内存空间.

    内存

    值类型不会有循环引用的问题,它只在被创建的时候或者写时复制的时候会产生一对一的持有

    struct Person { 
        let name: String 
        var parents: [Person] 
    }
    var john = Person(name: "John", parents: []) 
    john.parents = [john]
    john // John, parents: [John, parents: []]
    

    这里相当于把john的值放进数组parents里,john已经被复制了,如果这里是一个类,就会产生循环引用.
    例如:

    var window: Window? = Window() // window: 1 
    var view: View? = View(window: window!) // window: 2, view: 1 
    window?.rootView = view // window: 2, view: 2 
    view = nil // window: 2, view: 1 
    window = nil // window: 1, view: 1
    

    打破循环引用,需要在window和view之间有一个引用需要是weak或者unwoned,需要注意的是,引用计数为0时,变量自然就是nil了,因此weak必须声明为optional.

    除了weak,还有unowned,它不需要声明为可选值,也不会循环引用,但是显然会有一个新问题,那就是得保证unowned引用不能在被销毁后再去访问,也就是生命周期的管理,加入view unowned持有window,就得保证window的生命周期比view长,或者在window被释放后不再访问.

    另外,swift给unowned启用另一套引用计数,当strong引用全部释放后,对象的资源被释放,例如对其他对象的引用,但是对象本身的内存还没释放,这么做是为了不让unowned为nil,也就是为了实现不用声明optional做的特别处理.此时的unowned是僵尸对象,当unowned也释放后,内存才会释放.
    个人认为unowned并不可靠,还是optional,也就是weak的可靠性更强.

    循环引用

    闭包会对捕获的引用类型添加强引用.

    image.png
    如果不考虑其他对象对三者的引用的话,这里就不能弱化window和view之间的引用,因为这样会有一方消失;
    并且swift也不能对闭包weak;
    那么就只能弱化闭包对view的引用了
    这里,view被闭包不捕获,它处于闭包的捕获列表
    window?.onRotate = { [weak view] in 
        print("We now also need to update the view: \(view)") 
    }
    

    捕获列表也可以用来初始化新的变量。比如,如果我们想要用一个 weak 变量来引用窗口,我们可以将它在捕获列表中进行初始化,我们甚至可以定义完全不相关的变量,不过只能在闭包内访问:

    window?.onRotate = { [weak view, weak myWindow=window, x=5*5] in 
          print("We now also need to update the view: \(view)")
           print("Because the window \(myWindow) changed") 
    }
    

    相关文章

      网友评论

          本文标题:Swift进阶六:可变性和内存

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