协议为方法、属性、以及其他特定的任务需求或功能定义蓝图。协议可被类、结构体、或枚举类型采纳以提供所需功能的具体实现。满足了协议中需求的任意类型都叫做遵循了该协议。
除了指定遵循类型必须实现的要求外,你可以扩展一个协议以实现其中的一些需求或实现一个符合类型的可以利用的附加功能。
协议基本语法
协议定义
定义协议的方式和类、结构体、枚举类型相似,使用protocol
来声明协议
protocol SomeProtocol {
// protocol definition goes here
}
遵循协议
在Swift中, class
、struct
、enum
都可以遵循协议
在自定义类型声明时,将协议名放在类型名的冒号之后来表示该类型采纳一个特定的协议。多个协议可以用逗号分开列出:
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
若一个类拥有父类,将这个父类名放在其采纳的协议名之前,并用逗号分隔:
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
同样,协议也可以遵循协议
protocol SomeProtocol: Protocol1
如果你一个协议只想让类可以遵循这个协议,那么你可以让协议继承自AnyObject
当你遵循一个协议时,你必须实现协议中的所有没有没有实现的计算属性和方法,否则编译会报错。
协议里面添加属性
协议可以要求所有遵循该协议的类型提供特定名字和类型的实例属性或类型属性。在协议里面定义一个属性必须明确是可读的或可读的和可写的,同时这个属性要求定义为变量属性,在属性名称前面使用var
关键字。可读写的属性使用 { get set }
来写在声明后面来明确,使用 { get }
来明确可读的属性。
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
你也可以在协议中定义类型属性,在定义类型属性时往前面添加static
关键字就可以了。
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
协议里面定义方法
协议里面定义类方法和实例方法
协议可以要求采纳的类型实现指定的实例方法和类方法。这些方法作为协议定义的一部分,书写方式与正常实例和类方法的方式完全相同,但是不需要大括号和方法的主体。允许变量拥有参数,与正常的方法使用同样的规则。但在协议的定义中,方法参数不能定义默认值。
当协议中定义类型方法时,你总要在其之前添加static
关键字。即使在类实现时,类型方法要求使用class
或static
作为关键字前缀。
protocol SomeProtocol {
static func someTypeMethod()
}
添加实例方法则不需要在前面加上static
关键字。
protocol RandomNumberGenerator {
func random() -> Double
}
协议里面定义异变方法
有时一个方法需要改变其所属的实例。在方法的func
关键字之前使用mutating
关键字,来表示在该方法可以改变其所属的实例,以及该实例的所有属性。这允许结构体和枚举类型能采用相应协议并满足方法要求。
在下面Togglable
协议的定义中, toggle()
方法使用mutating
关键字标记,来表明该方法在调用时会改变遵循该协议的实例的状态:
protocol Togglable {
mutating func toggle()
}
现在定义一个名为OnOffSwitch
的枚举。这个枚举在两种状态间改变,即枚举成员On
和Off
。该枚举的toggle
实现使用了mutating
关键字,以满足Togglable
协议需求:
enum OnOffSwitch: Togglable {
case off, on
mutating func toggle() {
switch self {
case .off:
self = .on
case .on:
self = .off
}
}
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
协议里面定义初始化方法
协议可以要求遵循协议的类型实现指定的初始化器。你可以通过实现指定初始化器或便捷初始化器来使遵循该协议的类满足协议的初始化器要求。在这两种情况下,你都必须使用required
关键字修饰初始化器的实现:
protocol SomeProtocol {
init(someParameter: Int)
}
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
由于final
的类不会有子类,如果协议初始化器实现的类使用了final
标记,你就不需要使用required
来修饰了。因为这样的类不能被继承子类。
如果一个子类重写了父类指定的初始化器,并且遵循协议实现了初始化器要求,那么就要为这个初始化器的实现添加required
和override
两个修饰符:
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
// initializer implementation goes here
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// "required" from SomeProtocol conformance; "override" from SomeSuperClass
required override init() {
// initializer implementation goes here
}
}
协议扩展
使用协议扩展提供默认实现
你可以使用协议扩展来给协议的任意方法或者计算属性要求提供默认实现。如果遵循类型给这个协议的要求提供了它自己的实现,那么它就会替代扩展中提供的默认实现。
protocol MyProtocol{
var age: Int{get}
func test()
}
extension MyProtocol{
var age: Int {return 10}
func test(){}
}
给协议扩展添加限制
当你定义一个协议扩展,你可以明确遵循类型必须在扩展的方法和属性可用之前满足的限制。在扩展协议名字后边使用where
分句来写这些限制。比如说,你可以给Collection
定义一个扩展来应用于任意元素遵循上面MyProtocol
协议的集合。
extension Collection where Iterator.Element: MyProtocol {
var textualDescription: String {
let itemsAsText = self.map { $0.textualDescription }
return "[" + itemsAsText.joined(separator: ", ") + "]"
}
}
和OC交互
如果OC类需要遵循协议,需要在协议前面增加关键字@objc
,如果想使用OC的optionl
的属性,需要在方法或者属性添加关键字@objc optional
。
@objc protocol MyProtocl {
//可选的
@objc optional func func1()
var age: Int { get }
}
协议的方法调度
我们在之前的方法篇中了解过,类的方法通过VTable
来调度,而结构体和枚举类型的方法是通过静态派发的方式。那么协议里面的方法是通过什么方式来派发的呢?
protocol Incrementable {
func increment(by: Int)
}
class LGTeacher: Incrementable {
func increment(by: Int) {
print(by)
}
}
let t: LGTeacher = LGTeacher()
t.increment(by: 20)
接下来我们转成sil文件看一下:
可以看到,此时的
increment(by: Int)
方法还是一个class_method
,因此我们可以知道这个方法还是通过VTable
来调度的。我们再LGTeacher
里面的sil_vtable
里也找到了这个方法。
如果我们把变量t
声明为协议类型,那么这时候方法是如何调度的呢?
let t: Incrementable = LGTeacher()
转成sil文件后,对应的main
函数里面的代码如下:
可以看到
increment
函数不再是class_method
,此时变成了witness_method
。那这个witness_method
是什么呢?我们去swift的sil文档里面查看一下:
文档里面的意思是,从一个遵循协议的类型去寻找协议方法的实现,会通过
witness_method
这种方式去调度,如果协议使用@objc
修饰,会变成objc
的调用方式。
同时,我们在sil里面找到witness_table
,里面包含了协议方法increment
。
接下来我们通过寻找
increment
的名称,然后搜索一下,探索是如何找到LGTeacher
类里面的increment
方法。
可以看到,通过
witness_table
里面方法名称,直接从遵循这个协议的类里面,找到这个方法的具体实现,同时通过这个类的V_table
去调度这个方法。
通过上面的分析我们可以对协议的方法调用做以下总结:
-
如果实例对象的静态类型就是确定的类型,那么这个协议方法通过
VTalbel
进行调度。 -
如果实例对象的静态类型是协议类型,那么这个协议方法通过
witness_table
中对应的协议方法,然后通过协议方法去查找遵守协议的类的VTable
进行调度。
结构体遵循协议
上面分析的是类遵循协议后的方法调度,那如果是结构体struct
呢?
把上面的代码改成struct后,我们再进入到sil文件里面探个究竟:
可以发现,和类一样,也是先通过
witness_table
里面协议方法去寻找遵循协议的结构体里的的方法实现,但是由于结构体里面的方法是静态派发,没有VTable
,因此会直接去调用结构体里面的方法。
协议在extension中提供了默认方法实现
如果在一个协议中声明了方法,然后再extension
中实现了该方法。那会是什么情况呢?
protocol Incrementable {
func increment(by: Int)
}
extension Incrementable {
func increment(by: Int) {
print("Extension Increment")
}
}
class LGTeacher: Incrementable {
func increment(by: Int) {
print("LGTeacher Increment")
}
}
class LGStudent: Incrementable {}
let t1: LGTeacher = LGTeacher()
let t: Incrementable = LGTeacher()
let s: LGStudent = LGStudent()
let s1: Incrementable = LGStudent()
t.increment(by: 10)
t1.increment(by: 20)
s.increment(by: 10)
s1.increment(by: 20)
//打印结果
LGTeacher Increment
LGTeacher Increment
Extension Increment
Extension Increment
可以看到,当遵守协议的类实现了协议方法,那么会去走
VTable
调用方法。如果遵循协议的类没有实现协议方法,而且这个协议的extension
提供了方法的默认实现,那么这个类会通过witness_table
直接去静态调用extension
里面的方法实现。
协议中没有声明方法
如果在协议中没有声明方法,然后在extension
中声明方法,那又会怎么样呢?
protocol Incrementable {
}
extension Incrementable {
func increment(by: Int) {
print("Extension Increment")
}
}
class LGTeacher: Incrementable {
func increment(by: Int) {
print("LGTeacher Increment")
}
}
class LGStudent: Incrementable {}
let t1: LGTeacher = LGTeacher()
let t: Incrementable = LGTeacher()
let s: LGStudent = LGStudent()
let s1: Incrementable = LGStudent()
t.increment(by: 10)
t1.increment(by: 20)
s.increment(by: 10)
s1.increment(by: 20)
//打印结果
Extension Increment
LGTeacher Increment
Extension Increment
Extension Increment
打印出来sil文件查看
可以看到,当协议里面没有声明方法时,
witness_table
里面没有任何方法,所以无法通过 witness_table
调用方法。而LGTeacher
类里面有实现方法,所以t1
会通过V_Table
直接派发。LGStudent
里面没有实现方法,所以是通过extension
静态派发的方式来调用方法。
总结
对于确定类型则并且提供了方法实现的和没有遵守协议的时候一样 对于协议类型,则声明之后就需要通过PWT
方法,再根据实例对象的类型和对象类型中是否实现方法决定调度方式 V-Table
派发还是静态派发。
协议中声明 | 协议中未声明 | |
---|---|---|
确定类型 + 实现 | V-Table | V-Table |
确定类型 + 未实现 | 静态派发 | 静态派发 |
协议类型 + 实现 | PWT+V-Table | 静态派发 |
协议类型 + 未实现 | PWT+静态派发 | 静态派发 |
witness_table和类型的关系
- 当一份协议被多个类型遵守的时候
protocol Incrementable {
func increment(by: Int) -> Int
}
class test : Incrementable {
func increment(by: Int) -> Int {
return by + 1
}
}
class test1: Incrementable {
func increment(by: Int) -> Int {
return by + 2
}
}
可以看到,每个遵循协议的类型都会有一个自己的
witness_table
。
- 当一个类遵守多份协议的时候
protocol Incrementable {
func increment(by: Int) -> Int
}
protocol myProtocol {
func test()
}
class test : Incrementable, myProtocol {
func increment(by: Int) -> Int {
return by + 1
}
func test() {
print("123")
}
}
可以看到,一个类遵循多个协议,会为每一个协议增加一个
witness_table
,一个类中witness_table
的数量取决于这个类遵循了多少个协议。
- 当一个类遵循了协议,同时有子类。
protocol Incrementable {
func increment(by: Int) -> Int
}
protocol myProtocol {
func test()
}
class test : Incrementable, myProtocol {
func increment(by: Int) -> Int {
return by + 1
}
func test() {
print("123")
}
}
class test1: test {
override func increment(by: Int) -> Int {
return by + 2
}
}
class test2: test {}
可以看到,这个类遵循一个协议,就会有一个
witness_table
,如果这个类有子类,那么子类和父类会共用一个witness_table
。
协议的内存结构的底层布局
协议的底层存储
我们在上面分析了协议如何调度方法,以及遵循协议的类型和witness_table
的关系,现在我们来看一下协议是怎么存储的,首先我们来看一下遵循协议的类型的内存大小。
protocol Shape {
var area: Double { get }
}
class Circle: Shape {
var radious: Double
init(_ radious: Double) {
selfradious = radious
}
var area: Double {
get {
return radious * radious
}
}
}
var circle: Circle = Circle.init(10.0)
var circle1: Shape = Circle.init(10.0)
var t = type(of: circle1)
print(class_getInstanceSize(Circle.self)) //24
print(class_getInstanceSize((t as! AnyClass))) //24
print(MemoryLayout.size(ofValue: circle)) //8
print(MemoryLayout.size(ofValue: circle1)) //40
print(MemoryLayout<Circle>.size) //8
print(MemoryLayout<Shape>.size) //40
可以看到,遵循协议的具体类的实例变量的内存大小和遵循协议的变量的内存大小是不一样的。所以它们在底层的内存结构也是不一样的。我们先来看一下circle:Cricle
的内存结构。
通过
lldb
命令把属性值转成float
类型,可以看到,属性值正是10。
接下来我们看一下
circle1:Shape
的内存结构:
从上面的内存分析中,我们可以得到协议的大概存储结构:
- 0-7:实例对象的堆空间地址
- 8-23:未知
- 24-31: 实例对象的
metadata
- 32-40: 协议的
protocol witness table
由此,我们可以得到协议类型的大致结构:
struct LGProtocolBox {
var heapObject: UnsafeRawPointer
var unknow1: UnsafeRawPointer
var unknow2: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeRawPointer
}
接着我们把代码转成IR代码看一下:
define i32 @main(i32 %0, i8** %1) #0 {
entry:
// %T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }
%2 = bitcast i8** %1 to i8*
// 创建Circle类的metadata
%3 = call swiftcc %swift.metadata_response @"$s4main6CircleCMa"(i64 0) #4
%4 = extractvalue %swift.metadata_response %3, 0
// 创建一个Circle类的实例对象 s4main6CircleCACycfC == __allocating_init
%5 = call swiftcc %T4main6CircleC* @"$s4main6CircleCACycfC"(%swift.type* swiftself %4)
// 把metadata存放的%T4main5ShapeP数组的第二个成员变量里。
// %T4main5ShapeP = type { [24 x i8], metadata, i8** }
store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"$s4main7circle1AA5Shape_pvp", i32 0, i32 1), align 8
//s4main6CircleCAA5ShapeAAWP = protocol witness table for main.Circle
//把protocol witness table存储到数组的第三个成员变量里
// %T4main5ShapeP = type { [24 x i8], metadata, witness_table }
store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"$s4main6CircleCAA5ShapeAAWP", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"$s4main7circle1AA5Shape_pvp", i32 0, i32 2), align 8
//把实例对象的地址存放到数组的第一个成员变量里
//%T4main5ShapeP = type { [heapObject, Unknown, Unknown], metadata, witness_table }
store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"$s4main7circle1AA5Shape_pvp" to %T4main6CircleC**), align 8
ret i32 0
}
接下来我们看一下witness_table的内存结构
//s4main6CircleCAA5ShapeAAWP = protocol witness table for main.Circle,
//witness_table的内存结构
@"$s4main6CircleCAA5ShapeAAWP" = hidden constant [2 x i8*] [i8* bitcast (%swift.protocol_conformance_descriptor* @"$s4main6CircleCAA5ShapeAAMc" to i8*), i8* bitcast (double (%T4main6CircleC**, %swift.type*, i8**)* @"$s4main6CircleCAA5ShapeA2aDP4areaSdvgTW" to i8*)], align 8
这里面存储了两个变量,一个是
protocol_conformance_descriptor
,一个是遵循了shape
协议实现了area
属性的protocol witness
由此我们可以得到witness_tabel
的大概数据结构
struct TargetWitnessTable {
var protocol_conformance_descriptor: UnsafeRawPointer
var protocol_witness: UnsafeRawPointer
}
struct LGProtocolBox {
var heapObject: UnsafeRawPointer
var unknow1: UnsafeRawPointer
var unknow2: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}
接下来,我们去源码里面寻找witness_tabel
的具体细节。首先我们全局搜索TargetWitnessTable
,找到的代码如下:
TargetWitnessTable
里面有一个TargetProtocolConformanceDescriptor
类型的description
属性。我们再去查看TargetProtocolConformanceDescriptor
类。
可以看到,这个类里面有四个属性:
TargetRelativeContextPointer<Runtime, TargetProtocolDescriptor> Protocol;
TargetTypeReference<Runtime> TypeRef;
RelativeDirectPointer<const TargetWitnessTable<Runtime>> WitnessTablePattern;
ConformanceFlags Flags;
接下来我们对这四个属性进行分析,我们先看protocol
属性,这是一个指向TargetProtocolDescriptor
类型的TargetRelativeContextPointer
。我们先看一下TargetRelativeContextPointer
。
可以看到其实和 # Swift进阶(四)—— 指针中的
TargetRelativeDirectPointer
其实是一样的功能就是获取相对地址绝对地址,可以直接用 TargetRelativeDirectPointer
代替。
接下来我们看一下TargetProtocolDescriptor
,代码如下:
我们把
TargetProtocolDescriptor
还原成下面的结构体:
struct TargetProtocolDescriptor {
var flags: UInt32
var parent: TargetRelativeDirectPointer<UnsafeRawPointer>
var name: TargetRelativeDirectPointer<CChar>
var NumRequirementsInSignature: UInt32
var NumRequirements: UInt32
var AssociatedTypeNames: TargetRelativeDirectPointer<CChar>
}
witness_table的内存结构
通过上面的分析我们可以把witness_table
的内存结构还原出来,代码如下:
struct LGProtocolBox {
var heapObject: UnsafeRawPointer
var unknow1: UnsafeRawPointer
var unknow2: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}
struct TargetWitnessTable {
var protocol_conformance_descriptor: UnsafePointer<TargetProtocolConformanceDescriptor>
var protocol_witness: UnsafeRawPointer
}
struct TargetProtocolConformanceDescriptor {
var ptotocolDesc: TargetRelativeDirectPointer<TargetProtocolDescriptor>
var typeRef: UnsafeRawPointer
var WitnessTablePattern: UnsafeRawPointer
var flags: UInt32
}
struct TargetProtocolDescriptor {
var flags: UInt32
var parent: TargetRelativeDirectPointer<UnsafeRawPointer>
var name: TargetRelativeDirectPointer<CChar>
var NumRequirementsInSignature: UInt32
var NumRequirements: UInt32
var AssociatedTypeNames: TargetRelativeDirectPointer<CChar>
}
struct TargetRelativeDirectPointer<Pointee> {
var offset: Int32
mutating func getmeasureRelativeOffset() -> UnsafeMutablePointer<Pointee> {
let offset = self.offset
return withUnsafePointer(to: &self) { p in
return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self))
}
}
}
接下来我们来验证witness_table
的内存结构。代码如下:
var circle: Shape = Circle.init(10.0)
withUnsafePointer(to: &circle) { ptr **in**
ptr.withMemoryRebound(to: LGProtocolBox.**self**, capacity: 1) { pointer **in**
print(pointer.pointee)
let descPtr = pointer.pointee.witness_table.pointee.protocol_conformance_descriptor.pointee.ptotocolDesc.getmeasureRelativeOffset()
print("协议名称:\(String(cString: descPtr.pointee.name.getmeasureRelativeOffset()))")
print("协议方法的数量:\(descPtr.pointee.NumRequirements)")
print("witnessMethod:\(pointer.pointee.witness_table.pointee.protocol_witness)")
}
}
打印结果如下:
我们在分析 IR 代码的时候,应该有注意到
TargetWitnessTable
的 protocol_witness
,这一个其实存储的就是我们的 witnessMethod
,在上面的 IR 代码中其实已经写的很清楚了,但我们还是来验证一下。
- 在终端使用
nm -p <可执行文件> | grep <内存地址>
打印出这个方法的符号信息。 - 接着用
xcrun swift-demangle <符号信息>
还原这个符号信息。 还原结果如下:
所以,这个协议见证表(witness_table)的本质其实就是 TargetWitnessTable。第一个元素存储的是一个 descriptor,记录协议的一些描述信息,例如名称和方法的个数等。那么从第二个元素的指针开始存储的就是函数的指针。
从上面的IR代码中,我们知道witness_table 变量是一个连续的内存空间,所以这个 witness_table
变量存放的可能是很多个协议的见证表。
存放多个协议见证表的因素取决于变量的静态类型,如果这个变量的类型是协议组合类型,那么 witness_table 存放的就是协议组合中所有协议的见证表,如果这个变量的类型是指定单独的某个协议,那么 witness_table 存放的只有这个协议的见证表。
Existential Container
我们在上面的代码中知道了witness_table
的内存结构,那么存储了witness_table
的LGProtocolBox
又是什么东西呢?
在Swift里面,它有一个名称叫做Existential Container(存在容器)。这是编译器生成的一种特殊的数据类型,用于管理遵守了相同协议的协议类型,因为这些类型的内存大小不一致,所以通过当前的 Existential Container 统一管理。规则如下:
- 对于小容量的数据,直接存储在 Value Buffer
- 对于大容量的数据,通过堆区分配,存储堆空间的地址
那这个Existential Container(存在容器) 是怎么实现的呢?我们通过代码来观察一下。
通过上面分析,我们知道,当遵循协议的类型是引用类型的时候,它的第一个内存存储的是实例对象的堆空间地址。那当遵循协议的类型是值类型时,那这个Existential Container(存在容器) 是怎么存储的呢?
首先,我们定义一个遵循shape协议的struct类。然后给这个结构体添加属性。代码如下:
protocol Shape {
var area: Double { get }
}
struct Circle: Shape {
var radious: Double
var width: Double = 20
var height: Double = 30
init_ radious: Double) {
self.radious = radious
}
var area: Double {
get {
return 10.0
}
}
func getter() {
print( #function)
}
}
var circle: Shape = Circle.init(10.0)
print(MemoryLayout.size(ofValue: circle))
//打印结果
40
可以看到,遵循协议的结构体实例对象的内存大小和类实例对象一样,都是40。然后我们去查看一下内存结构。
可以看到,和引用类型的内存结构有所不一样,第二个8字节和第三个8字节都有存储值。而且第一个8字节里面存储的也不是引用地址。通过
expr
命令解析。结果如下:
可以看到,里面刚好存储的是值类型的3个属性值。而这就是上面讲的Existential Container(存在容器) 的第一条管理规则:对于小容量的数据,直接存储在 Value Buffer 。
如果我们给Circle结构体再增加一个属性值,会变成什么样呢?结果如下:
我们发现,当结构体的属性值比较多的时候,它的内存结构又变了。变成和类实例对象的内存结构一样。
通过分析第一个字节存储的内存地址,我们发现,当值类型的属性比较多的时候,编译器会专门在堆区分配一个空间用来存储这些属性值,同时把这个堆空间的内存地址存储在实例对象的内存结构中,这就是上面讲的Existential Container(存在容器) 的第二条管理规则:对于大容量的数据,通过堆区分配,存储堆空间的地址。
写时复制
上面讲过,当值类型的数据比较大的时候,会在堆空间分配存储空间用来存储,那这样还会保持值类型的特性吗?
protocol Shape {
var area: Double {get}
var radious: Double{get set}
}
class Circle: Shape{
var radious: Double
var width: Double = 20
var height: Double = 30
var height1: Double = 50
init(_ radius: Double) {
self.radius = radius
}
var area: Double{
get{
return radius * radius * 3.14
}
}
var area1: Double{
get{
return radius * radius * 3.14
}
}
}
var circle1 : Shape = Circle.init(10.0)
var c = circle1
c.radious = 20
print(circle1.radious) // 10.0
print(c.radious)//20.0
可以看到,当c
的radius
属性值改变的时候,circle
的属性值没变。还是保持着值类型的特性,那这个是怎么实现的呢?我们通过内存结构来看一下。
我们先把断点打在
c
的radius
属性值未发生改变的地方,然后看下c
和circle
有没有发生变化。
当
c
的radius
属性值未发生改变时,c
和circle
存储的堆空间地址都是一样的。
然后,我们对c
的radius
属性值进行修改,会发生什么呢?
可以看到,当对
c
的radius
属性值进行修改后,c
存储的堆空间地址发生了改变,而这个新的堆空间地址存放着修改后的属性值。
针对遵循协议的拥有比较大的数据的值类型,Swift采用了一种写时复制的技术,即会去判断这个堆空间的引用计数,当引用计数大于2的时候,也就是有多个实例变量在引用这个堆空间的地址,当其中一个实例变量发生属性值改变的时候,就会在堆空间重新复制一个新的存储空间,并把这个新的空间地址,传给要改变属性值的实例变量,用来保持修改后的实例值,这样做除了会保持值类型的特性外,还减少了内存分配的消耗,避免了创建了一些没有用的存储空间。