一、协议的介绍
协议的定义
方式与类
、结构体
和枚举
的定义非常相似:
protocol SomeProtocol {
// 这里是协议的定义部分
}
要让自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:
)分隔。遵循多个协议
时,各协议之间用逗号(,
)分隔。
若是一个类拥有父类,应该将父类名放在遵循的协议名之前,以逗号分隔。
属性
协议可以要求遵循协议的类型提供特定名称和类型的实例属性
或类型属性
。协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型
。
- 协议要求一个属性必须
明确
是可读的/可读可写
的,类型声明后加上{ set get }
来表示属性是可读可写的; - 属性要求定义为
变量类型
,即使用var
而不是let。
protocol SomeProtocol {
var mustBeSettable: Int { get set } //可读可写
var doesNotNeedToBeSettable: Int { get } //可读
}
方法
在协议中定义方法
,只需要定义当前方法的名称、参数列表和返回值
。类遵循了协议,必须实现协议中的方法。
协议中也可以定义初始化方法
,当实现初始化器时,必须使用required
关键字。
如果一个协议只能被类实现
,需要协议继承自AnyObject
。如果此时结构体遵守该协议,会报错。
mutating 在方法中改变方法所属的实例。
protocol Togglable {
mutating func toggle()
}
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()
注意:实现协议中的
mutating
方法时,若是类类型
,则不用写mutating
关键字。而对于结构体和枚举
,则必须写mutating
关键字。
二、协议作为类型
尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用
。使用场景如下:
1、作为函数、方法或构造器中的参数类型或返回值类型;
2、作为常量、变量或属性的类型;
3、作为数组、字典或其他容器中的元素类型。
首先,以下代码,通过继承基类实现的方式,如下:
class Shape{
var area: Double{
get{
return 0
}
}
}
class Circle: Shape{
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
override var area: Double{
get{
return radius * radius * 3.14
}
}
}
class Rectangle: Shape{
var width, height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
override var area: Double{
get{
return width * height
}
}
}
var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10.0, 20.0)
var shapes: [Shape] = [circle, rectangle]
for shape in shapes{
print(shape.area)
}
//打印结果:314.0 200.0
改为协议的方式实现,如下:
//1、将Shape改为protocol类型;
//2、删除实现该协议的类的协议方法的前缀override
protocol Shape{
var area: Double{get}
}
思考:shapes数组的内存是什么情况?
1、如果,元素指定的Shape是类
时,数组中存储的都是引用类型
的地址。
2、如果,元素指定的Shape是协议
时,数组中存储的是什么?
通过协议代码分析
示例1:
protocol MyProtocol {
func teach()
}
extension MyProtocol{
func teach(){ print("MyProtocol") }
}
class MyClass1: MyProtocol{
func teach(){ print("MyClass1") }
}
let object: MyProtocol = MyClass1()
object.teach()
let object1: MyClass1 = MyClass1()
object1.teach()
//打印结果:
//MyClass1
//MyClass1
通过SIL,我们可以看到一个新的结构witness_table
,也叫PWT(协议目录表)
。
打印结果分析:
对象为MyProtocol类型
时,方法teach的调用在底层是通过witness_method
调用,即通过PWT
(协议目录表)获取对应的函数地址,其内部也是通过类的函数表
查找进行调用。
对象为MyClass类型
时,方法teach的调用在底层是通过类的函数表
来查找函数,主要是基于类的实际类型。
示例2,修改示例1代码:
protocol MyProtocol {
//func teach()
}
//打印结果:
//MyProtocol
//MyClass1
查看SIL,其中已经没有teach方法。
如果没有声明在Protocol中的函数,只是通过Extension提供了一个默认实现
,在Extension中声明的方法是静态调用,其函数地址在编译过程中就已经确定了,对于遵守协议的类来说,这种方法是无法重写
的。
打印不同的原因:MyProtocol协议
扩展
中实现的teach方法不能被类重写
,相当于这是两个方法,并不是同一个
。
第一个打印MyProtocol
,是因为调用的是协议扩展
中的teach方法,这个方法的地址是在编译时期就已经确定的,即通过静态函数地址调度;
第二个打印MyClass
,同上个例子一样,是类的函数表
调用。
示例3,再次修改代码:
protocol MyProtocol {
func teach()
}
extension MyProtocol{
func teach(){ print("MyProtocol") }
}
class MyClass1: MyProtocol{
//func teach(){ print("MyClass1") }
}
let object: MyProtocol = MyClass1()
object.teach()
let object1: MyClass1 = MyClass1()
object1.teach()
//打印结果:
//MyProtocol
//MyProtocol
以上可以理解为protocol增加
可选
实现方法,也可以通过@objc和optional
实现。
三、PWT内存
通过研究函数调度,我们知道V-Table
是存储在metadata
中的,那么协议的PWT
存储在哪里呢?
代码:
protocol Shape {
var area: Double {get}
}
class Circle: Shape{
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
var area: Double{
get{
return radius * radius * 3.14
}
}
}
var circle1: Shape = Circle(10.0)
print(MemoryLayout.size(ofValue: circle1))
print(MemoryLayout.stride(ofValue: circle1))
var circle2: Circle = Circle(10.0)
print(MemoryLayout.size(ofValue: circle2))
print(MemoryLayout.stride(ofValue: circle2))
//打印结果:
//40 40
//8 8
circle1
的打印都是40
,先LLDB
尝试一下。
接着看看
SIL
,系统通过调用init_existential_addr
读取之前声明的circle1
变量,而circle1却是通过调用load指令
读取的。
SIL官方文档对
init_existential_addr
的解释如下:
其中的existential container
是编译器生成的一种特殊的数据类型,也用于管理遵守了相同协议
的协议类型。因为这些数据类型的内存空间尺寸不同,使用existential container
进行管理可以实现存储一致性
对应的,以上代码可以理解为:使用了包含Circle的existential container
来初始化circle引用的内存。通俗来说就是将circle
包装后,存入existential container
初始化的内存。
仿写内存结构:
// HeapObject结构体(Swift类的本质)
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
// %T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }
struct protocolData {
//24 * i8 :因为是8字节读取,所以写成3个指针,正好24字节
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt,即协议的方法列表
var pwt: UnsafeRawPointer
}
// 2、定义协议+类
protocol Shape {
var area: Double {get}
}
class Circle: Shape{
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
var area: Double{
get{
return radius * radius * 3.14
}
}
}
//对象类型为协议
var circle: Shape = Circle(10.0)
// 3、将circle强转为protocolData结构体
withUnsafePointer(to: &circle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
//打印结果:
//protocolData(value1: 0x00000001005d2900, value2: 0x0000000000000000,
//value3: 0x0000000000000000, type: 0x0000000100008278, pwt: 0x0000000100004020)
运行LLDB:
总结:
PWT
存储在一个existential container
容器中,该容器的大致结构是{heapObject, metadata, PWT
}。
下面我们分别来分析一下struct和class的pwt的内存管理方式。
struct和协议
新建struct实现协议,struct包含3个属性:
struct Rectangle: Shape{
var width, height: Int
var width1 = 30
init(_ width: Int, _ height: Int) {
self.width = width
self.height = height
}
var area: Double{
get{
return Double(width * height)
}
}
}
var rectangle: Shape = Rectangle.init(1, 2)
withUnsafePointer(to: &rectangle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
//打印结果:protocolData(value1: 0x0000000000000001, value2: 0x0000000000000002,
//value3: 0x000000000000001e, type: 0x00000001000041c8, pwt: 0x0000000100004040)
观察结果:三个value
,分别保存了三个属性的值
。
修改struct为4个属性,如下:
此时,
value1
是一个堆区地址
,这个地址里面存储的是struct
各个属性的值
。
总结:针对
协议
,对象底层的存储结构
如下:
1、前24个字节
,主要用于存储遵循了协议
的class/struct
的属性值
。如果24字节不够存储
,会在堆区
开辟一个内存空间用于存储,24字节
中的前8个字节
存储堆区地址
。即,如果超出24
,是直接分配堆区空间
,然后存储值
,并不是先存储值,然后发现不够再分配堆区空间。
2、后16个字节
,分别用于存储 存放metadata
(目的是为了找到Value Witness Table
值目录表)、pwt
(协议目录表)。
3.2 class—写时复制(copy on write)
修改上面代码,将Rectangle
改为class
,声明一个数组存储circle 和 rectangle对象。
protocol Shape {
var area: Double {get}
}
class Circle: Shape{
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
var area: Double{
get{
return radius * radius * 3.14
}
}
}
class Rectangle: Shape{
var width, height: Int
init(_ width: Int, _ height: Int) {
self.width = width
self.height = height
}
var area: Double{
get{
return Double(width * height)
}
}
}
var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10, 20)
//所谓的多态:根据具体的类来决定调度的方法
var shapes: [Shape] = [circle, rectangle]
//这里能区分不同area的原因是因为 在protocol中存放了pwt(协议目录表),可以根据这个表来正确调用对应的实现方法(pwt中也是通过class_method查找,
//同时在运行过程中也记录了metadata,在pwt中通过metadata查找V-Table,从而完成当前方法的调用)
for shape in shapes{
print(shape.area)
}
//打印结果:314.0 200.0
继续回到struct
的例子,将其赋值给另一个变量,其内存存放的是否是一样的?
//对象类型为协议
var rectangle1: Shape = Rectangle(10, 20)
//将其赋值给另一个协议变量
var rectangle2: Shape = rectangle
withUnsafePointer(to: &rectangle1) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
withUnsafePointer(to: &rectangle2) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
//打印结果是一致的
两个协议变量内存
存放的东西是一样
的。
继续,修改rectangle1
的width
属性的值(需要将width属性声明到protocol
),修改后的代码如下:
//修改协议
protocol Shape {
var area: Double {get}
var width: Int {get set}
}
rectangle2.width = 50
通过lldb调试发现,在rectangle2变量修改width
之后,其存储数据的堆区地址
发生了变化。这就是所谓的写时复制:当复制时,并没有值的修改,所以两个变量指向同一个堆区内存,当第二个变量修改了属性值时,会将原本堆区内存的值拷贝到一个新的堆区内存,并进行值的修改。
继续,如果将struct
修改为class
,lldb调试结果如下,属性值修改前后,堆区地址
并没有变化。
也就是说,以上分析,符合对值类型
和引用类型
的理解:
-
值类型
: 在传递过程中并不共享状态; -
引用类型
: 在传递过程中共享状态。
四、Value Buffer
struct
结构体中24字节
官方叫法是Value Buffer
。
Value Buffer
用来存储当前的值
,如果超过
存储的最大容量的话会开辟一块堆
空间。
针对值类型来说在赋值时会先拷贝heapobject地址(Copy on write)
。在修改时会先检测引用计数,如果引用计数大于1,此时开辟新的堆空间把要修改的内容拷贝到新的堆空间
(这么做为了提升性能)。
Value Buffer
在容器existential container
中的位置:
总结:
-
class
、struct
、enum
都可以遵守协议,有以下几点说明:
1.1 多个协议之间需要使用逗号
分隔;
1.2 如果class
中有superClass
,一般放在协议之前
。 - 协议中可以添加
属性
,有以下两点说明:
2.1 属性必须明确是可读(get)/可读可写(get + set)
的;
2.2 属性使用var
修饰。 - 协议中可以定义
方法
,
3.1 定义方法时,只需要定义当前方法的名称+参数列表+返回值
,其具体实现可以通过协议的extension实现
,或者在遵守协议时实现
;
3.2. 协议中也可以定义初始化方法
,当实现初始化器时,必须使用required
关键字。 - 如果协议只能被
class实现
,需要协议继承自AnyObject
。 - 协议也可以作为
类型
,有以下三种场景:
5.1 作为函数、方法或者初始化程序中的参数
类型或者返回值
;
5.2 作为常量
、变量
或属性
的类型;
5.3 作为数组
、字典
或者其他容器
中项目的类型。 - 协议的底层存储结构:
24字节valueBuffer + vwt(8字节) + pwt(8字节)
,
6.1前24个字节
,官方称为Value Buffer
,主要用于存储遵循了协议的class/struct的属性值
;
6.2 如果超过Value Buffer
最大容量。值类型
采用copy-write
;引用类型
则是使用同一个堆区
地址;
6.3后16个字节
分别用于存储vwt(值目录表)
、pwt(协议目录表)
。