Swift进阶六:可变性和内存

写时复制

标准库中,内建集合类型,如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{
    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
    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 {
      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进阶六:可变性和内存)