写时复制
标准库中,内建集合类型,如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的可靠性更强.
循环引用
闭包会对捕获的引用类型添加强引用.
如果不考虑其他对象对三者的引用的话,这里就不能弱化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")
}