Swift Copy-On-Write

一.堆栈

栈是一块空间较小但是运行速度很快的内存区域,栈上的内存分配遵循后进先出的原则,通过移动栈的尾指针实现push和pop操作。
堆是内存中的另外一块,空间比栈大很多,但是运行速度比栈要慢。但是堆可以动态分配内存。堆的内存分配比较复杂,系统需要在堆上不断寻找不再需要的内存然后进行回收。在ARC中上述过程是自动的。另外在多线程环境中,多个线程会共享堆内存。为了确保线程安全,堆会对资源进行加锁操作。但是加锁是很耗费性能的,你在堆上所获得的数据安全性实际上是在牺牲性能的代价下得来的。

二.swift中的值类型和引用类型

1.值类型

在Swift中,值类型有两种。一种是定长值类型,比如数值类型Int,Double,Float,还有一些只包含定长值类型的结构体(CGPoint)等等;另外一种叫做变长值类型,比如String,数组,字典等等。定长值类型都会保存在栈上,而变长值类型则会分配堆内存。
值类型的实例(结构体)只会在栈上保存它内部的存储属性,并且通过=赋值的实例彼此的存储是独立的。也就是我们所说的拷贝。如下:

struct Point{
  var x,y:Double
}
let point1 = Point(x:3,y:5)
var point2 = point1

point1和point2会被分配到栈上,并且会分别为point1和point2分配内存空间。在语句point2 = point1的时候,point1进行了拷贝。因为定长值类型的空间是固定的,所以这种拷贝的开销很小。

2.引用类型

引用类型并不会直接保存在栈上,还是以上述Point为例,如果把Point修改为类,并生成两个引用point1和point2,这时系统会在栈上开辟两个指针长度来保存point1和ponit2指针,栈上的指针负责去堆上找对应的对象。point1和point2所指向的实例的存储属性会保存在堆上。
在栈上生成point1指针后,指针内容是空的,接下来会去堆上分配内存,首先会对堆加锁,找到尺寸合适的空间,然后分配目标内存并解除堆的锁定,将堆内存片段的首地址保存在栈的指针中。相比在栈上保存point1和point2的指针,堆上需要的空间更大。除了x和y的空间,在头部还有8个字节的空间,一个用来索引类的类型信息的指针地址,一个用来保存对象的引用计数。当使用=赋值时,栈上会生成point2指针,point1和point2指针指向同一个堆地址
引用类型的赋值不会发生拷贝。所以无论改变point1或者是point2的属性,改变的都是同一块堆地址上的属性。这里要特别说明let和var。swift提供了let和var来限制对象的可变性和不可变性,但是对于某个实例,有意义的是其内部属性。如果你用let声明一个引用类型对象,你只能保证它的指针地址不能被改变,但是不能约束它的内部属性。举个例子:

//这里的Point是Class
//声明point1和point2指向不同的内存地址
let point1 = Point(x:3,y:5)
let point2 = Point(x:5,y:1)
point1 = point2 //发生编译错误,不能修改point1的指针
point1.x = 0 //因为x是用var定义的所以可以修改
 //这里point1.x == 0

我们把多个引用指向同一块内存称为资源共享。不过在实际开发过程中,很多时候我们并不想让point1和point2资源共享,这样会造成很多难以判断的错误。在Swift中,一种新的类型来专门解决共享的问题,就是我们所说的变长值类型。刚才讲值类型的时候有提过定长值类型的内存分配,那么变长值类型是什么样的?这就是我们今天的主题Copy-On-Write。

三.Copy-On-Write

因为栈上的空间是连续的,你总是通过移动栈尾指针去开辟和释放栈内存,而变长值类型中有一些成员在初始化的时候并不能确定它所占用的内存。比如集合类型,你可以随时往里面添加和删除元素,这会导致内存的增加和减少。类似的还有字符串,在内存中储存字符串实际上是存储的每一个字符,所以对于变长值类型并不能把全部内容都保存在栈上。在Swift中用了一种很巧妙的技术来实现变长值类型,那就是Copy-On-Write。

Copy-On-Write故名思议就是写时复制,当我们对变量进行写操作的时候会触发拷贝操作。但是我们也不能在每一次写入的时候都拷贝,思考一下,如果该变量的引用计数只有1,那就没有任何拷贝的必要。所以在拷贝前我们需要检测变量的引用计数是否唯一。在swift中提供了isKnownUniquelyReferenced,它能检查一个类的实例是不是唯一的引用。然而这个方法只能对Swift的类使用,所以对于不是Swift的类我们需要在外面包装一下。下面我们看代码:

import UIKit
//声明swift包装类,用于包装OC对象UIBezierPath
class Box{
    var rawValue:T
    init(rawValue:T) {
        self.rawValue = rawValue
    }
}
struct BezierPath{
    private var _path = Box.init(rawValue: UIBezierPath())
    var pathForReading:Box{
        return _path
    }
    var pathForWriting:Box{
        //mutating 声明的方法可以修改结构体中变量
        //isKnownUniquelyReferenced 检测引用类型的引用是否唯一 但是只对swift类有用 这里我们针对的对象是UIBezierPath 所以我们需要用Swift的类包装一下 在这里我们声明了Box类
        mutating get{
            if !isKnownUniquelyReferenced(&_path){
                _path = Box.init(rawValue: _path.rawValue.copy() as! UIBezierPath)
                print("拷贝")
                return _path
            }
            print("未拷贝")
            return _path
        }
    }
}


class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        var bizer = BezierPath()
        bizer.pathForWriting.rawValue.lineWidth = 3
        //内部Box对象引用计数为1,不拷贝
        bizer.pathForWriting.rawValue.lineWidth = 10

        //Box对象引用计数+1
        //bizer和bizer1会共享内部Box对象
        //要注意这里的赋值把bizer进行了拷贝,但是其内部的引用属性还是指向相同地址。
        var bizer1 = bizer
        
        bizer1.pathForWriting.rawValue.lineWidth = 5
        
        print(bizer.pathForReading.rawValue.lineWidth) //输出10
        // Do any additional setup after loading the view, typically from a nib.
    }
}

相关的地方在代码中都有给出注释,所以在这里就不在赘述。

完!

你可能感兴趣的:(Swift Copy-On-Write)