Swift 中有两大数据类型, 即值类型 和 引用类型, 除了简单的数据类型外, 字符串, 数组, 字典, 结构体, 枚举 都是值类型, 类 当然属于引用类型的.
Swift 为集合类型提供了一个 Copy-On-Write 技术, 直译为写时复制, 简称 COW, 接下来我们来一一验证这种机制.
准备工作
由于网上很多有关获取内存地址的方法是打印出来,今天使用打印方式只针对 基本数据类型和自定义结构体, 使用 lldb 命令 fr v -R [objc] 来查看集合类型内存结构。
查看地址两种方式
-
lldb命令fr v -R [objc] -
打印内存地址
// 打印内存地址 func printAddress(of object: UnsafeRawPointer) { let addr = Int(bitPattern: object) let str = String(format: "%p", addr) print(str) }
验证过程
1. Array
-
示例代码 :
import Foundation // 1.1 let list = [1,2,3,4,5] // 1.2 var newList = list // 1.3 newList[2]=3 print("end")如下图在第 14, 17, 19行分别打上断点, 我们给断点分别编号为
1,2,3, 代码执行到断点1, 使用命令查看list的内存情况, 执行到断点2查看newList的内存情况, 执行到断点3时再查看newList的内存情况.断点1时list的内存地址, 如下图为0x0000000107e3aae0.断点2时newList的内存地址, 如下图为0x0000000107e3aae0.此时,是把数组
list赋值给newList构建新的数组, 从以上查看内存地址上看, 两个数组内存地址相同, 共享内存空间, 并没有发生copy操作.断点3时newList的内存地址, 如下图为0x0000000100705dd0.我们发现, 当改变
newList的元素时,即使我们赋值与原来的值相同, 它的内存地址仍然发生了变化, 此时我们再去看list内存, 还是原来的, 此处我就不放图了, 自己看一眼就可以了😄. 如果我们在1.3改变元素操作是改变的list, 结果会怎样呢, 经测试, 是list发生了改变, 指向的新的内存空间, 也就是说, 哪一个变化, 哪一个就重新分配内存空间.原来的不变.
- 小结 : 这种多个变量共享一块内存空间, 当其中一个发生
变化时才分配新的内存空间的技术称之为写时复制
Swift 官方文档说明:
-
Structures and Enumerations Are Value Types
A value type is a type whose value is copied when it’s assigned to a variable or constant, or when it’s passed to a function.
You’ve actually been using value types extensively throughout the previous chapters. In fact, all of the basic types in Swift—integers, floating-point numbers, Booleans, strings, arrays and dictionaries—are value types, and are implemented as structures behind the scenes.
All structures and enumerations are value types in Swift. This means that any structure and enumeration instances you create—and any value types they have as properties—are always copied when they’re passed around in your code.
NOTE
Collections defined by the standard library like arrays, dictionaries, and strings use an optimization to reduce the performance cost of copying. Instead of making a copy immediately, these collections share the memory where the elements are stored between the original instance and any copies. If one of the copies of the collection is modified, the elements are copied just before the modification. The behavior you see in your code is always as if a copy took place immediately.
-
文档说的很清楚, Swift 中的所有基本类型——整数、浮点数、布尔值、字符串、数组和字典——都是值类型.
-
标准库定义的集合(如数组, 字典, 字符串, Set)使用了
COW技术来降低复制的性能成本.
那么接下来我们来继续验证 String, Dictionary, Set 和 Int/Double单一数据类型 以及自定义结构体类型,
2. String
-
如下图, 我们还按照上述方法去打断点依次查看内存, 发现
String类型也符合这种特点.
3. Dictionary
-
如下三张图, 由于字典的数据比较多, 图片较大, 三步分三张图展示, 我们还按照上述方法去打断点依次查看内存, 发现 在
1.3执行之前,a和b的内存地址相同, 之后b地址发生了变化, 所以Dictionary类型也符合这种特点.
4. Set
-
如下三张图, 由于字典的数据比较多, 图片较大, 三步分三张图展示, 我们还按照上述方法去打断点依次查看内存, 发现 在
1.3执行之前,a和b的内存地址相同, 之后b地址发生了变化, 所以Set类型也符合这种特点.
5. 基本单一数据类型(Int / Double):
- 如下图, 基本单一类型采用打印的方式查看实际地址, 从打印结果看它们不符合这种特点.
6. 自定义结构体 (Struct)
-
示例代码
struct LGPerson { var name: String init(name: String) { self.name = name } } var Jack = LGPerson(name: "Jack") printAddress(&(Jack)) var Andy = Jack printAddress(&(Andy)) -
如下图, 从打印结果看, 和基本数据类型一样, 赋值时直接发生了
copy操作, 对于自定义的结构体并不能支持Copy-On-Write技术.
验证小结
- 标准库定义的集合(如数组, 字典, 字符串, Set)使用了
COW技术, 有效降低了内存的开销. - 基本数据类型不支持
COW, 如 Int, 自定义 struct 等.
struct 中的 class 成员
那么问题来了, 如果结构体中有一个成员类型是 class, 当赋值进行 copy 的时候, 这个成员会不会也 copy 一份新的呢, 如果不会, 我们该怎么实现呢.
struct 中的 class 成员是否被 copy
-
示例:
class Dog: NSObject { var age: Int init(age: Int) { self.age = age } } struct LGPerson { var name: String var dog: Dog init(name: String, dog: Dog) { self.name = name self.dog = dog } } let dog1 = Dog(age: 2) var Jack = LGPerson(name: "Jack", dog: dog1) print(Jack.dog.description,"\n") print("Jack-dog age: \((Jack.dog).age)\n") var Andy = Jack Andy.name = "Andy" print(Andy.dog.description,"\n") Andy.dog.age = 5 print(Jack.dog.description,"\n") print(Andy.dog.description,"\n") print("Jack-dog age: \((Jack.dog).age)") print("Andy-dog age: \((Andy.dog).age)") -
执行结果:
-
分析: 很明显,
dog1并没有随着Jack赋值给Andy而被copy一份新的数据. 当Andy.dog.age = 5执行时,Jack.dog.age的值也随之改变了, 这显然不是我们想要的结果, 我们想要的是dog也是一份新的数据, 最好是像Copy-On-Write这种特性.只有在需要时才进行复制操作.
小结 : struct 的 class 成员不会被 copy
自定义结构体如何使用 COW
自定义实现COW初级版本
```
class Dog: NSObject {
var age: Int
init(age: Int) {
self.age = age
}
}
struct LGPerson {
var name: String
var dog: Dog
init(name: String, dog: Dog) {
self.name = name
self.dog = dog
}
var dogAge: Int {
get {return dog.age}
set {
if !isKnownUniquelyReferenced(&dog) {
dog = Dog(age: newValue)
return
}
dog.age = newValue
}
}
}
let dog1 = Dog(age: 2)
var Jack = LGPerson(name: "Jack", dog: dog1)
print(Jack.dog.description,"\n")
print("Jack-dog age: \((Jack.dog).age)\n")
var Andy = Jack
Andy.name = "Andy"
print(Andy.dog.description,"\n")
Andy.dogAge = 5
print(Jack.dog.description,"\n")
print(Andy.dog.description,"\n")
print("Jack-dog age: \((Jack.dog).age)")
print("Andy-dog age: \((Andy.dog).age)")
```
-
执行结果:
-
分析:
- 通过结果可以看出来, 当通过
Andy.dogAge = 5改变了dog.age的值之后,Andy的dog成员生成了一个新的对象.达到了我们的预期结果, 实现了Copy-On-Write特性.
- 通过结果可以看出来, 当通过
-
注意:- 存在问题: 但是这里边还有一个问题, 如果用
Andy.dog.age = 5改变Andy的dog成员的age属性, 结果仍然不是我们想要的, 会和我们第一次验证时一样,dog对象没有被copy. - 解决办法: 可以把
dog成员设置成只读的private(set) var dog: Dog, 自己在内部不要这样直接设置age属性就可以.
- 存在问题: 但是这里边还有一个问题, 如果用
自定义实现COW进阶版本
-
上面设置为只读属性, 虽然可以解决问题, 但是还是不彻底, 接下来我们来看一个, 更彻底, 更灵活的处理方法.
-
用一个引用类型去实现一个拥有 Copy-on-Write 特性的泛型值类型T, 把
Dog改为结构体.final class Ref<T> { var val: T init(_ v: T) {val = v} } struct Dog { var age: Int init(age: Int) { self.age = age } } struct LGPerson<T> { private var ref: Ref<T> var name: String init(name: String, _ ref: T) { self.name = name self.ref = Ref(ref) } var dog: T { get {return ref.val} set { if !isKnownUniquelyReferenced(&ref) { ref = Ref(newValue) return } ref.val = newValue } } } let dog1 = Dog(age: 2) var Jack = LGPerson(name: "Jack", dog1) //print(Jack.dog.description,"\n") print("Jack-dog age: \((Jack.dog).age)\n") var Andy = Jack Andy.name = "Andy" //print(Andy.dog.description,"\n") Andy.dog.age = 5 //print(Jack.dog.description,"\n") //print(Andy.dog.description,"\n") print("Jack-dog age: \((Jack.dog).age)") print("Andy-dog age: \((Andy.dog).age)")
Swift 标准库中大量使用了 Copy-On-Write 技术。
其实这里是文档中的一个例子😄😄😄, 可以移步去看一下官方解释;
Advice: Use copy-on-write semantics for large values
总结
-
Copy-On-Write是一种用来优化占用内存大的值类型的拷贝操作的机制; -
对于
Int,Double等基本类型的值类型,它们在赋值的时候就会发生拷贝; -
对于集合类型, 如
Set,Array,Dictionary,String类型, 当赋值的时候不会发生拷贝, 只有在某个被修改之后才会发生拷贝; -
对于自定义的数据类型不会自动实现
Copy-On-Write, 可以根据自己的实际情况实现.