1、内存分配

1.1 值类型的内存分配

  • 在 Swift 中定长的值类型都是保存在栈上的,操作时不会涉及堆上的内存。变长的值类型(字符串、集合类型是可变长度的值类型)会分配堆内存。

    • 这相当于一个 “福利”,意味着你可以使用值类型更快速的完成一个方法的执行。
    • 值类型的实例只会保存其内部的存储属性,并且通过 “=” 赋值的实例彼此的存储是独立的。
    • 值类型的赋值是拷贝的,对于定长的值类型来说,由于所需的内存空间是固定的,所以这种拷贝的开销是在常数时间内完成的。
    struct Point {var x: Doublevar y: Double
    }
    let point1 = Point(x: 3, y: 5)
    var point2 = point1print(point1)           // Point(x: 3.0, y: 5.0)
    print(point2)           // Point(x: 3.0, y: 5.0)
  • 上面的示例在栈上的实际分配如下图。

               栈
    point1   x: 3.0y: 5.0
    point2   x: 3.0y: 5.0
  • 如果尝试修改 point2 的属性,只会修改 point2 在栈上的地址中保存的 x 值,不会影响 point1 的值。

    point2.x = 5print(point1)           // Point(x: 3.0, y: 5.0)
    print(point2)           // Point(x: 5.0, y: 5.0)
               栈
    point1   x: 3.0y: 5.0
    point2   x: 5.0y: 5.0

1.2 引用类型的内存分配

  • 引用类型的存储属性不会直接保存在栈上,系统会在栈上开辟空间用来保存实例的指针,栈上的指针负责去堆上找到相应的对象。

    • 引用类型的赋值不会发生 “拷贝”,当你尝试修改示例的值的时候,实例的指针会 “指引” 你来到堆上,然后修改堆上的内容。
  • 下面把 Point 的定义修改成类。

    class Point {var x: Doublevar y: Doubleinit(x: Double, y: Double) {self.x = xself.y = y}
    }
    let point1 = Point(x: 3, y: 5)
    let point2 = point1print(point1.x, point1.y)           // 3.0  5.0
    print(point2.x, point2.y)           // 3.0  5.0
  • 因为 Point 是类,所以 Point 的存储属性不能直接保存在栈上,系统会在栈上开辟两个指针的长度用来保存 point1point2 的指针,栈上的指针负责去堆上找到对应的对象,point1point2 两个实例的存储属性会保存在堆上。

  • 当使用 “=” 进行赋值时,栈上会生成一个 point2 的指针,point2 指针与 point1 指针指向堆的同一地址。

               栈              堆
    point1   [    ] --||-->  类型信息
    point2   [    ] --|     引用计数x: 3y: 5
  • 在栈上生成 point1point2 的指针后,指针的内容是空的,接下来会去堆上分配内存,首先会对堆加锁,找到尺寸合适的内存空间,然后分配目标内存并解除堆的锁定,将堆中内存片段的首地址保存在栈上的指针中。

  • 相比在栈上保存 point1point2,堆上需要的内存空间要更大,除了保存 xy 的空间,在头部还需要两个 8 字节的空间,一个用来索引类的类型信息的指针地址,一个用来保存对象的 “引用计数”。

  • 当尝试修改 point2 的值的时候,point2 的指针会 “指引” 你来到堆上,然后修改堆上的内容,这个时候 point1 也被修改了。

    point2.x = 5print(point1.x, point1.y)           // 5.0  5.0
    print(point2.x, point2.y)           // 5.0  5.0
  • 我们称 point1point2 之间的这种关系为 “共享”。“共享” 是引用类型的特性,在很多时候会给人带来困扰,“共享” 形态出现的根本原因是我们无法保证一个引用类型的对象的不可变性。

2、可变性和不可变性

  • 在 Swift 中对象的可变性与不可变性是通过关键字 letvar 来限制的。

  • Swift 语言默认的状态是不可变性,在很多地方有体现。

    • 比如方法在传入实参时会进行拷贝,拷贝后的参数是不可变的。
    • 或者当你使用 var 关键字定义的对象如果没有改变时,编译器会提醒你把 var 修改为 let

2.1 引用类型的可变性和不可变性

  • 对于引用类型的对象,当你需要一个不可变的对象的时候,你无法通过关键字来控制其属性的不可变性。

  • 当你创建一个 Point 类的实例,你希望它是不可变的,所以使用 let 关键字声明,但是 let 只能约束栈上的内容,也就是说,即便你对一个类型实例使用了 let 关键字,也只能保证它的指针地址不发生变化,但是不能约束它的属性不发生变化。。

    class Point {var x: Doublevar y: Doubleinit(x: Double, y: Double) {self.x = xself.y = y}
    }
    let point1 = Point(x: 3, y: 5)
    let point2 = Point(x: 0, y: 0)print(point1.x, point1.y)           // 3.0  5.0
    print(point2.x, point2.y)           // 0.0  0.0point1 = point2                     // 发生编译错误,不能修改 point1 的指针point1.x = 0                        // 因为 x 属性是使用 var 定义的,所以可以被修改print(point1.x, point1.y)           // 0.0  5.0
    print(point2.x, point2.y)           // 0.0  0.0
  • 如果把所有的属性都设置成不可变的,这的确可以保证引用类型的不可变性,而且有不少语言就是这么设计的。

    class Point {let x: Doublelet y: Doubleinit(x: Double, y: Double) {self.x = xself.y = y}
    }
    let point1 = Point(x: 3, y: 5)print(point1.x, point1.y)           // 3.0  5.0point1.x = 0                        // 发生编译错误,x 属性是不可变的
  • 新的问题是如果你要修改 Point 的属性,你只能重新建一个对象并赋值,这意味着一次没有必要的加锁、寻址与内存回收的过程,大大损耗了系统的性能。

    let point1 = Point(x: 3, y: 5)point1 = Point(x: 0, y: 5)

2.2 值类型的可变性和不可变性

  • 因为值类型的属性保存在栈上,所以可以被 let 关键字所约束。

  • 你可以把一个值类型的属性都声明称 var,保证其灵活性,在需要该类型的实例是一个不可变对象时,使用 let 声明对象,即便对象的属性是可变的,但是对象整体是不可变的,所以不能修改实例的属性。

    struct Point {var x: Doublevar y: Double
    }
    let point1 = Point(x: 3, y: 5)print(point1.x, point1.y)           // 3.0  5.0point1.x = 0                        // 编辑报错,因为 point1 是不可变的
  • 因为赋值时是 “拷贝” 的,所以旧对象的可变性限制不会影响新对象。

    let point1 = Point(x: 3, y: 5)
    var point2 = point1                 // 赋值时发生拷贝print(point1.x, point1.y)           // 3.0  5.0
    print(point2.x, point2.y)           // 3.0  5.0point2.x = 0                        // 编译通过,因为 point2 是可变的print(point1.x, point1.y)           // 0.0  5.0
    print(point2.x, point2.y)           // 0.0  5.0

3、引用类型的共享

  • “共享” 是引用类型的特性,在很多时候会给人带来困扰,“共享” 形态出现的根本原因是我们无法保证一个引用类型的对象的不可变性。

  • 下面展示应用类型中的共享。

    // 标签
    class Tag {var price: Doubleinit(price: Double) {self.price = price}
    }// 商品
    class Merchandise {var tag: Tagvar description: Stringinit(tag: Tag, description: String) {self.tag = tagself.description = description}
    }
    let tag = Tag(price: 8.0)let tomato = Merchandise(tag: tag, description: "tomato")print("tomato: \(tomato.tag.price)")          // tomato: 8.0// 修改标签
    tag.price = 3.0// 新商品
    let potato = Merchandise(tag: tag, description: "potato")print("tomato: \(tomato.tag.price)")          // tomato: 3.0
    print("potato: \(potato.tag.price)")          // potato: 3.0
  • 这个例子中所描述的情景就是 “共享”, 你修改了你需要的部分(土豆的价格),但是引起了意料之外的其它改变(番茄的价格),这是由于番茄和土豆共享了一个标签实例。

  • 语意上的共享在真实的内存环境中是由内存地址引起的。上例中的对象都是引用类型,由于我们只创建了三个对象,所以系统会在堆上分配三块内存地址,分别保存 tomatopotatotag

                  栈                堆
    tamoto   Tag         --|description   |       tag|--> price: 3.0|
    patoto   Tag         --|description
  • 在 OC 时代,并没有如此丰富的值类型可供使用,有很多类型都是引用类型的,因此使用引用类型时需要一个不会产生 “共享” 的安全策略,拷贝就是其中一种。

  • 首先创建一个标签对象,在标签上打上你需要的价格,然后在标签上调用 copy() 方法,将返回的拷贝对象传给商品。

    let tag = Tag(price: 8.0)let tomato = Merchandise(tag: tag.copy(), description: "tomato")print("tomato: \(tomato.tag.price)")          // tomato: 8.0
  • 当你对 tag 执行 copy 后再传给 Merchandise 构造器,内存分配情况如下图。

                  栈                 堆
    tamoto   Tag         -----> Copied tagdescription        price: 8.0tagprice: 8.0
  • 如果有新的商品上架,可以继续使用 “拷贝” 来打标签。

    let tag = Tag(price: 8.0)let tomato = Merchandise(tag: tag.copy(), description: "tomato")print("tomato: \(tomato.tag.price)")          // tomato: 8.0// 修改标签
    tag.price = 3.0// 新商品
    let potato = Merchandise(tag: tag.copy(), description: "potato")print("tomato: \(tomato.tag.price)")          // tomato: 8.0
    print("potato: \(potato.tag.price)")          // potato: 3.0
  • 现在内存中的分配如图。

                  栈                 堆
    tamoto   Tag         -----> Copied tagdescription        price: 8.0tagprice: 3.0patoto   Tag         -----> Copied tagdescription        price: 3.0
  • 这种拷贝叫做 “保护性拷贝”,在保护性拷贝的模式下,不会产生 “共享”。

4、变长值类型的拷贝

  • 变长值类型不能像定长值类型那样把全部的内容都保存在栈上,这是因为栈上的内存空间是连续的,你总是通过移动尾指针去开辟和释放栈的内存。在 Swift 中集合类型和字符串类型是值类型的,在栈上保留了变长值类型的身份信息,而变长值类型的内部元素全部保留在堆上。

  • 定长值类型不会发生 “共享” 这很好理解,因为每次赋值都会开辟新的栈内存,但是对于变长的值类型来说是如何处理哪些尾保存内部元素而占用的堆内存呢?苹果在 WWWDC2015 的 414 号视频中揭示了定长值类型的拷贝奥秘:相比定长值类型的 “拷贝” 和引用类型的 “保护性拷贝”,变长值类型的拷贝规则要复杂一些,使用了名为 Copy-on-Write 的技术,从字面上理解就是只有在写入的时候才拷贝。

  • 在 Swift 3.0 中出现了很多 Swift 原生的变长值类型,这些变长值类型在拷贝时使用了 Copy-on-Write 技术以提升性能,比如 Date、Data、Measurement、URL、URLSession、URLComponents、IndexPath。

5、利用引用类型的共享

  • “共享” 并不总是有害的,“共享” 的好处之一是堆上的内存空间得到了复用,尤其是对于内存占用空间较大的对象(比如图片),效果明显。所以如果堆上的对象在 “共享” 状态下不会被修改,那么我们应该对该对象进行复用从而避免在堆上创建重复的对象,此时你需要做的是创建一个对象,然后向对象的引用者传递对象的指针,简单来说,就是利用 “共享” 来实现一个 “缓存” 的策略。

  • 假如你的应用中会用到许多重复的内容,比如用到很多相似的图片,如果你在每个需要的地方都调用 UIImage(named:) 方法,那么会创建很多重复的内容,所以我们需要把所有用到的图片集中创建,然后从中挑选需要的图片。很显然,在这个场景中字典最适合作为缓存图片的容器,把字典的键值作为图片索引信息。这是引用类型的经典用例之一,字典的键值就是每个图片的 “身份信息”,可以看到在这个示例中 “身份信息” 是多么的重要。

    enum Color: String {case redcase bluecase green
    }enum Shape: String {case circlecase squarecase triangle
    }
    let imageArray = ["redsquare": UIImage(named: "redsquare"), ...]func searchImage(color: Color, shape: Shape) -> UIImage {let key = color.rawValue + shape.rawValuereturn imageArray[key]!!
    }
  • 一个变长的值类型实际会把内存保存在堆上,因此创建一个变长值类型时不可避免的会对堆加锁并分配内存,我们使用缓存的目的之一就是避免过多的堆内存操作,在上例中我们习惯性的把 String 作为字典的键值,但是 String 是变长的值类型,在 searchImage 中生成 key 的时候会触发堆上的内存分配。

  • 如果想继续提升 searchImage 的性能,可以使用定长值类型作为键值,这样在合成键值时将不会访问堆上的内存。要注意的一点是你所使用的定长值类型必须满足 Hashable 协议才能作为字典的键值。

    enum Color: Equatable {case redcase bluecase green
    }enum Shape: Equatable {case circlecase squarecase triangle
    }struct PrivateKey: Hashable {var color: Color = .redvar shape: Shape = .circleinternal var hsahValue: Int {return color.hashValue + shape.hashValue}
    }
    let imageArray = [PrivateKey(color: .red, shape: .square): UIImage(named: "redsquare"),PrivateKey(color: .blue, shape: .circle): UIImage(named: "bluecircle")]func searchImage(privateKey: PrivateKey) -> UIImage {return imageArray[privateKey]!!
    }

Swift 值类型和引用类型的内存管理相关推荐

  1. C# “值类型“和“引用类型“在内存的分配

    在代码中每创建一个变量,程序运行时都会在内存开辟一些空间存储这些值,所以写程序时创建的变量是越少越好(不积小流,无以成江海 -荀子)- 存储方式: "值类型"存储于内存中的&quo ...

  2. 第八回:品味类型---值类型与引用类型(上)-内存有理

    第八回:品味类型---值类型与引用类型(上)-内存有理 http://www.cnblogs.com/anytao/archive/2007/05/23/must_net_08.html 发布日期:2 ...

  3. [你必须知道的.NET] 第八回:品味类型---值类型与引用类型(上)-内存有理

    本文将介绍以下内容: 类型的基本概念 值类型深入 引用类型深入 值类型与引用类型的比较及应用 1. 引言 买了新本本,忙了好几天系统,终于开始了对值类型和引用类型做个全面的讲述了,本系列开篇之时就是因 ...

  4. 【转】[你必须知道的.NET] 第八回:品味类型---值类型与引用类型(上)-内存有理...

    引用自:http://www.cnblogs.com/anytao/category/155694.html 作者:Anytao 本文将介绍以下内容: 类型的基本概念 值类型深入 引用类型深入 值类型 ...

  5. 理解C#值类型与引用类型(收藏)

    从概念上看,值类型直接存储其值,而引用类型存储对其值的引用.这两种类型存储在内存的不同地方.在C#中,我们必须在设计类型的时候就决定类型实例的行为.这种决定非常重要,用<CLR via C#&g ...

  6. 浅谈C#值类型和引用类型

    首先,什么是值类型,什么是引用类型? 在C#中值类型的变量直接存储数据,而引用类型的变量持有的是数据的引用,数据存储在数据堆中. 值类型(value type):byte,short,int,long ...

  7. 理解C#值类型与引用类型

    这篇文章是我几个月前写的,今天进行了比较大的修订,重新发了出来,希望和大家共同探讨,并在此感谢Anytao 的讨论和帮助. 从概念上看,值类型直接存储其值,而引用类型存储对其值的引用.这两种类型存储在 ...

  8. C#值类型和引用类型的不同

    1 C#中有两种类型:   值类型和引用类型:      值类型的变量直接包含他们的数据,引用类型存储对他们的数据的引用,后者称为对象:      简单说:值类型直接存储其值,引用类型存储对值得引用. ...

  9. C# 值类型与引用类型(1)

    1. 主要内容 类型的基本概念 值类型深入 引用类型深入 值类型与引用类型的比较及应用 2. 基本概念 C#中,变量是值还是引用仅取决于其数据类型. C#的基本数据类型都以平台无关的方式来定义,C#的 ...

最新文章

  1. ifconfig命令实例
  2. 这才是实现分布式锁的正确姿势!
  3. boost::intrusive::avl_set用法的测试程序
  4. GitHub多人协作简明教程
  5. 三、项目经理的角色【PMP 】
  6. Java生成随机数的4种方式,以后就用它了!
  7. APP录获取短信+通讯录网站源码
  8. 必须收藏:20个开发技巧教你开发高性能计算代码
  9. Anaconda创建跟别人环境配置一样的虚拟环境(coda env creat -f environment.yml)
  10. ubuntu 環境下 bochs 的安裝
  11. 德州扑克的思考和实验
  12. 数字科技陪伴企业成长|突破封锁,庚顿数据助力中国名牌全球瞩目
  13. 学习Wolff关于分离镜面反射与漫反射的文章
  14. 《SOA中国路线图》可圈可点之处
  15. java web 徐林林_零点起飞学Java Web开发 (徐林林) 高清PDF
  16. 问答网站Stack Overflow的成功之道
  17. 明源云客微信抢房技巧_微信抢房软件开发 - heartdong - OSCHINA - 中文开源技术交流社区...
  18. java爬小说_java爬虫实战开发小说网站
  19. 秒启万台主机,腾讯云云硬盘数据调度架构演进
  20. 锁相环设计与MATLAB仿真

热门文章

  1. PHP学习 文件操作函数的应用--简单网络留言模板
  2. 天堂Lineage(單機版)從零開始架設教學
  3. Oracle中的MERGE语句
  4. [转帖][实用]Linux 释放内存方法
  5. .Net Framework 3.0 概述
  6. 使用Netbeans创建java Web项目
  7. 网站信息统计的简单实现过程
  8. 海思3559A上编译GDB源码操作步骤及简单使用
  9. 提高C++性能的编程技术笔记:标准模板库+测试代码
  10. Ubuntu14.04 64位上配置终端显示git分支名称