Swift 值类型和引用类型的内存管理
Posted qianchia
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift 值类型和引用类型的内存管理相关的知识,希望对你有一定的参考价值。
1、内存分配
1.1 值类型的内存分配
在 Swift 中定长的值类型都是保存在栈上的,操作时不会涉及堆上的内存。变长的值类型(字符串、集合类型是可变长度的值类型)会分配堆内存。
- 这相当于一个 “福利”,意味着你可以使用值类型更快速的完成一个方法的执行。
- 值类型的实例只会保存其内部的存储属性,并且通过 “=” 赋值的实例彼此的存储是独立的。
- 值类型的赋值是拷贝的,对于定长的值类型来说,由于所需的内存空间是固定的,所以这种拷贝的开销是在常数时间内完成的。
struct Point { var x: Double var y: Double }
let point1 = Point(x: 3, y: 5) var point2 = point1 print(point1) // Point(x: 3.0, y: 5.0) print(point2) // Point(x: 3.0, y: 5.0)
上面的示例在栈上的实际分配如下图。
栈 point1 x: 3.0 y: 5.0 point2 x: 3.0 y: 5.0
如果尝试修改
point2
的属性,只会修改point2
在栈上的地址中保存的x
值,不会影响point1
的值。point2.x = 5 print(point1) // Point(x: 3.0, y: 5.0) print(point2) // Point(x: 5.0, y: 5.0)
栈 point1 x: 3.0 y: 5.0 point2 x: 5.0 y: 5.0
1.2 引用类型的内存分配
引用类型的存储属性不会直接保存在栈上,系统会在栈上开辟空间用来保存实例的指针,栈上的指针负责去堆上找到相应的对象。
- 引用类型的赋值不会发生 “拷贝”,当你尝试修改示例的值的时候,实例的指针会 “指引” 你来到堆上,然后修改堆上的内容。
下面把
Point
的定义修改成类。class Point { var x: Double var y: Double init(x: Double, y: Double) { self.x = x self.y = y } }
let point1 = Point(x: 3, y: 5) let point2 = point1 print(point1.x, point1.y) // 3.0 5.0 print(point2.x, point2.y) // 3.0 5.0
因为
Point
是类,所以Point
的存储属性不能直接保存在栈上,系统会在栈上开辟两个指针的长度用来保存point1
和point2
的指针,栈上的指针负责去堆上找到对应的对象,point1
和point2
两个实例的存储属性会保存在堆上。当使用 “=” 进行赋值时,栈上会生成一个
point2
的指针,point2
指针与point1
指针指向堆的同一地址。栈 堆 point1 [ ] --| |--> 类型信息 point2 [ ] --| 引用计数 x: 3 y: 5
在栈上生成
point1
和point2
的指针后,指针的内容是空的,接下来会去堆上分配内存,首先会对堆加锁,找到尺寸合适的内存空间,然后分配目标内存并解除堆的锁定,将堆中内存片段的首地址保存在栈上的指针中。相比在栈上保存
point1
和point2
,堆上需要的内存空间要更大,除了保存x
和y
的空间,在头部还需要两个 8 字节的空间,一个用来索引类的类型信息的指针地址,一个用来保存对象的 “引用计数”。当尝试修改
point2
的值的时候,point2
的指针会 “指引” 你来到堆上,然后修改堆上的内容,这个时候point1
也被修改了。point2.x = 5 print(point1.x, point1.y) // 5.0 5.0 print(point2.x, point2.y) // 5.0 5.0
我们称
point1
和point2
之间的这种关系为 “共享”。“共享” 是引用类型的特性,在很多时候会给人带来困扰,“共享” 形态出现的根本原因是我们无法保证一个引用类型的对象的不可变性。
2、可变性和不可变性
在 Swift 中对象的可变性与不可变性是通过关键字
let
和var
来限制的。Swift 语言默认的状态是不可变性,在很多地方有体现。
- 比如方法在传入实参时会进行拷贝,拷贝后的参数是不可变的。
- 或者当你使用
var
关键字定义的对象如果没有改变时,编译器会提醒你把var
修改为let
。
2.1 引用类型的可变性和不可变性
对于引用类型的对象,当你需要一个不可变的对象的时候,你无法通过关键字来控制其属性的不可变性。
当你创建一个
Point
类的实例,你希望它是不可变的,所以使用let
关键字声明,但是let
只能约束栈上的内容,也就是说,即便你对一个类型实例使用了let
关键字,也只能保证它的指针地址不发生变化,但是不能约束它的属性不发生变化。。class Point { var x: Double var y: Double init(x: Double, y: Double) { self.x = x self.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.0 point1 = 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: Double let y: Double init(x: Double, y: Double) { self.x = x self.y = y } }
let point1 = Point(x: 3, y: 5) print(point1.x, point1.y) // 3.0 5.0 point1.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: Double var y: Double }
let point1 = Point(x: 3, y: 5) print(point1.x, point1.y) // 3.0 5.0 point1.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.0 point2.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: Double init(price: Double) { self.price = price } } // 商品 class Merchandise { var tag: Tag var description: String init(tag: Tag, description: String) { self.tag = tag self.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
这个例子中所描述的情景就是 “共享”, 你修改了你需要的部分(土豆的价格),但是引起了意料之外的其它改变(番茄的价格),这是由于番茄和土豆共享了一个标签实例。
语意上的共享在真实的内存环境中是由内存地址引起的。上例中的对象都是引用类型,由于我们只创建了三个对象,所以系统会在堆上分配三块内存地址,分别保存
tomato
、potato
和tag
。栈 堆 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 tag description price: 8.0 tag price: 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 tag description price: 8.0 tag price: 3.0 patoto Tag -----> Copied tag description 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 red case blue case green } enum Shape: String { case circle case square case triangle }
let imageArray = ["redsquare": UIImage(named: "redsquare"), ...] func searchImage(color: Color, shape: Shape) -> UIImage { let key = color.rawValue + shape.rawValue return imageArray[key]!! }
一个变长的值类型实际会把内存保存在堆上,因此创建一个变长值类型时不可避免的会对堆加锁并分配内存,我们使用缓存的目的之一就是避免过多的堆内存操作,在上例中我们习惯性的把
String
作为字典的键值,但是String
是变长的值类型,在searchImage
中生成key
的时候会触发堆上的内存分配。如果想继续提升
searchImage
的性能,可以使用定长值类型作为键值,这样在合成键值时将不会访问堆上的内存。要注意的一点是你所使用的定长值类型必须满足Hashable
协议才能作为字典的键值。enum Color: Equatable { case red case blue case green } enum Shape: Equatable { case circle case square case triangle } struct PrivateKey: Hashable { var color: Color = .red var shape: Shape = .circle internal 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 值类型和引用类型的内存管理的主要内容,如果未能解决你的问题,请参考以下文章