关于“幽灵架构”的补充说明2:Struct以及Copy - on -Write

  1. 云栖社区>
  2. 博客>
  3. 正文

关于“幽灵架构”的补充说明2:Struct以及Copy - on -Write

cwift-tal 2016-05-11 14:38:13 浏览499
展开阅读全文

在“幽灵架构”Demo中我把两个数据模型声明成了Struct,苹果WWDC2015的414号视频讲解了非常多关于Struct的优势,其实也是所有值类型的优势。首先Swift标准库中绝大部分是值类型的,值类型的值传递是通过copy的,而作为一门静态语言,Swift要求所有的对象都有明确的类型,明确的类型代表了固定的内存分配,而414号视频也指出在内存中进行定长对象的copy是时间常数的,也就所谓的“cheap”。另外如果值类型的对象中包含引用类型的属性的话,会破坏值类型的特性,出现共享(详情请参考414号视频),因此在Swift中,对于包含引用类型属性的值类型对象间的Copy使用了Copy - on - Write这项技术,最基本的示例如下:

struct BezierPath {
    private var _path = UIBezierPath()

    var pathForReading: UIBezierPath {
        return _path  }

    var pathForWriting: UIBezierPath {
        mutating get {
            _path = _path.copy() as! UIBezierPath
            return _path
        }  
    } 
}

把引用类型的属性声明成private属性,然后向外界暴露两个公开的计算属性,供读取的计算属性返回私有属性本身,供写入的计算属性返回一个copy后的复本。这个版本的代码的问题是虽然保证了整个struct的值类型特性,但是每次写入都需要copy性能并不高,因此在Copy - on - Write中的pathForWriting加入了一个方法:

struct MyWrapper {
    var _object: SomeSwiftObject
    var objectForWriting: SomeSwiftObject {
        mutating get {
            if !isUniquelyReferencedNonObjC(&_object)) {
                _object = _object.copy()
            }
            return _object
        }
    }
}

这里的isUniquelyReferencedNonObjC方法会判断你当前访问的MyWrapper的_object属性是否只有一个引用,如果是通过 let b = a 这样的方法创建的话,那么a的_object和b的_object的引用会指向同一个地址,在读取值的时候不关心_object对象的引用,如果只进行读取的话是不需要进行copy的,如果需要写入b的_objc话需要访问objectForWriting,此时objectForWriting会检查_object的引用,如果如let b = a 的方式创建的b对象,那么b中_object的引用是2,此时会将b的_object对象赋成_object拷贝后的副本,这样在之后继续访问objectForWriting的时候由于b中的_object已经是新的对象了,引用只有1,所以_object会被直接返回。Swift中的很多会包含引用对象的值对象都采用了Copy-on-Write技术,比如我们常用的数组。需要注意的是这里的引用类型的对象必须是Swift对象,如果你使用了一个OC中的引用类型,那么你需要对其进行一个封装。
用法如下:

 final class Box<A> { 
     var unbox: A
    init (_ value: A) { unbox = value } 
}

其中A是非Swift原生对象,在类中声明某个对象的时候使用”Box”,在取值的时候需要调用Box实例的unbox属性获得原始的对象。除了使用“=”会发生copy外,向一个方法中传入值类型时也会发生copy,所以可以安全地操作传入的参数。通常情况下Swift中不会直接操作一个指针,所有的指针都会被标注为“unsafe”,如果想要方法改变传入的参数,可以把该参数声明成inout,然后在传入时参数前加&,&代表传入的是一个地址,inout的形式与直接操作某个对象的指针看起来是相同的,但是inout其实依旧使用了copy,不同的是在方法体结束的时候会把处理后的copy对象的值再赋回给原始的对象,请看下面的例子:

let clo:Int -> Void = { i in print("形参的值\(i)")
}
func method(inout num:Int){
    clo(num)
    num += 1
}
method(&a)
print("a的值:\(a)")

我们知道闭包可以捕获对象,闭包中捕获的是形参对象num,如果闭包num和a指向同一个地址的话,那么在method方法体中clo捕获的应该是a的引用,但其实clo捕获的只是copy后的副本,而已,在a发生变化之前就已经将副本捕获了,导致最后的打印结果不同。

下面来聊聊Struct,所有属性的类型都确定的Struct会被保存在栈上,Swift中每个Struct类型的长度是固定的,好比每个指针都是8字节,所以指针会被保存在栈上。在使用引用类型的时候,每次从栈上取到对应的指针需要根据指针提供的地址信息去堆上寻找引用类型的真实值,定长的Struct的值会被直接保存在栈上,这就省去了寻址的开销,栈的空间是有限的,如果一组值类型的长度超过了栈的空间,那么会被自动转移到堆上。
让我们在playground上写一些示例:

struct StructDemo{
    var a = 1
    var str = ""
}
let strDemo = StructDemo()
sizeofValue(strDemo)
sizeof(StructDemo)

这里Int的长度是8字节,String的长度是24字节,最终StructDemo的长度是32,完全取决于其属性的长度,sizeof和sizeofValue分别打印类型和实例的长度,这两个方法显示的都是栈上的长度,结果是相同的:
这里写图片描述

如果换成用class声明,长度是8。

网友评论

登录后评论
0/500
评论
cwift-tal
+ 关注