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<T>{
    var rawValue:T
    init(rawValue:T) {
        self.rawValue = rawValue
    }
}
struct BezierPath{
    private var _path = Box.init(rawValue: UIBezierPath())
    var pathForReading:Box<UIBezierPath>{
        return _path
    }
    var pathForWriting:Box<UIBezierPath>{
        //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.
    }
}

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

完!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容