本文主要分析protocol的用法及底层存储结构
协议的基本用法
- 【语法格式】:协议的语法格式
//协议的语法格式
protocol MyProtocol {
//body
}
-
class、struct、enum
都可以遵守协议,如果需要遵守多个协议
,可以使用逗号分隔
//1-2、class、struct、enum都可以遵守协议,如果需要遵守多个协议,可以使用逗号分隔
struct CJLTeacher: Protocol1, Protocol2 {
//body
}
- 如果class中有
superClass
,一般是放在遵守的协议之前
//1-3、如果class中有superClass,一般是放在遵守的协议之前
struct CJLTeacher: NSObject, Protocol1, Protocol2 {
//body
}
协议中添加属性
- 协议中可以添加属性,但是需要注意一下几点:
-
- 协议同时要求一个
属性必须
明确是可读的/可读可写的
- 协议同时要求一个
-
- 属性要求定义为
变量属性
,即使用var
而不是let
- 属性要求定义为
-
protocol CJLTest {
var age: Int {get set}
}
协议中定义方法
- 在协议中定义方法,只需要定义当前方法的名称、参数列表和返回值
- 在具体的类中遵守协议,并实现协议中的方法
protocol MyProtocol {
func doSomething()
static func teach()
}
class CJLTeacher: MyProtocol{
func doSomething() {
print("CJLTeacher doSomething")
}
static func teach() {
print("teach")
}
}
var t = CJLTeacher()
t.doSomething()
CJLTeacher.teach()
- 协议中也可以
定义初始化方法
,当实现初始化器时,必须使用required
关键字
protocol MyProtocol {
init(age: Int)
}
class CJLTeacher: MyProtocol{
var age: Int
required init(age: Int) {
self.age = age
}
}
- 如果一个协议只能被
类
实现,需要协议继承自AnyObject
。如果此时结构体
遵守该协议,会报错
协议进阶 - 将协议作为类型
协议除了上述的基本用法,还有以下几种用法:
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
对于数组来说,当前的大小是固定的,因为当前存放的都是引用类型
(即占8
字节),其存储结构如下所示
通过协议实现
- 上述代码的实现是通过继承基类,即
基类中的area必须有一个默认实现
,也可以通过协议
来替代当前代码的书写方式
//2-2、通过协议实现:area必须有一个默认实现
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: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
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
当数组中的元素指定的Shape是类时,数组中存储的都是引用类型的地址,那么问题来了,如果数组指定的Shape是一个协议时,数组中存储的是什么?
-
- 如果Shape协议提供了一个默认实现,此时的打印是什么?
protocol Shape {
}
extension Shape{
var area: Double {
get{return 0}
}
}
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: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
var circle: Shape = Circle.init(10.0)
print(circle.area)
0.0
打印0.0
的原因是因为在Extension中声明的方法是静态调用
,即在编译链接后当前方法的地址就已经确定了,我们是无法重写
的。这个可以通过SIL代码来验证
协议示例代码分析
下面通过一个简单的代码来分析SIL
- 【示例1】:下面代码的打印结果是什么?
protocol MyProtocol {
func teach()
}
extension MyProtocol{
func teach(){ print("MyProtocol") }
}
class MyClass: MyProtocol{
func teach(){ print("MyClass") }
}
let object: MyProtocol = MyClass()
object.teach()
let object1: MyClass = MyClass()
object1.teach()
MyClass
MyClass
打印一样的原因是因为在MyProtocol
协议中有teach方法的声明
-
查看
SIL
中两种方式的调用有什么不同?定义为
MyProtocol
类型的对象object
,方法teach的调用在底层是通过witness_method
调用,即通过PWT(协议目录表)获取对应的函数地址
,其内部也是通过类的函数表查找进行调用
-
定义为
MyClass
类型的对象object1
,方法teach的调用在底层是通过类的函数表
来查找函数,主要是基于类的实际类型其中,协议目录表和函数表如下所示
查看协议中
teach
方法具体实现的SIL代码,在内部调用
的是MyClass类的函数表中的teach方法
【示例2】:如果去掉
MyProtocol
协议中teach方法的声明,打印结果是什么?
//如果去掉协议中的声明呢?打印结果是什么
protocol MyProtocol {
}
extension MyProtocol{
func teach(){ print("MyProtocol") }
}
class MyClass: MyProtocol{
func teach(){ print("MyClass") }
}
let object: MyProtocol = MyClass()
object.teach()
let object1: MyClass = MyClass()
object1.teach()
MyProtocol
MyClass
打印不一致的根本原因是MyProtocol
协议扩展中实现的teach方法不能被类重写,相当于这是两个方法,并不是同一个
- 查看底层的SIL代码
第一个打印
MyProtocol
,是因为调用的是协议扩展中的teach方法,这个方法的地址是在编译时期就已经确定的,即通过静态函数地址调度
-
第二个打印
MyClass
,同上个例子一样,是类的函数表调用查看SIL中的
witness_table
,其中已经没有teach方法 声明在
Protocol
中的方法,在底层会存储在PWT
,PWT中的方法也是通过class_method
,去类的V-Table
中找到对应的方法的调度。如果没有声明在
Protocol
中的函数,只是通过Extension
提供了一个默认实现,其函数地址在编译过程中就已经确定了,对于遵守协议的类来说,这种方法是无法重写的
协议的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 circle: Shape = Circle(10.0)
print(MemoryLayout.size(ofValue: circle))
print(MemoryLayout.stride(ofValue: circle))
var circle1: Circle = Circle(10.0)
print(MemoryLayout.size(ofValue: circle1))
print(MemoryLayout.stride(ofValue: circle1))
40
40
8
8
首先通过
lldb
调试如下-
查看对应的SIL代码,比往常的代码多了一步
init_existential_addr
,可以理解为:使用了包含Circle
的existential container
来初始化circle
引用的内存。通俗来说就是将circle
包装了存入existential container
初始化的内存其中,SIL官方文档对
init_existential_addr
的解释如下其中的
existential container
是编译器生成的一种特殊的数据类型,也用于管理遵守了相同协议的协议类型。因为这些数据类型的内存空间尺寸不同,使用existential container
进行管理可以实现存储一致性 通过
IR
代码,分析如下
define i32 @main(i32 %0, i8** %1) #0 {
entry:
%2 = bitcast i8** %1 to i8*
; s4main6CircleCMa 等价于 type metadata accessor for main.Circle
%3 = call swiftcc %swift.metadata_response @"$s4main6CircleCMa"(i64 0) #7
%4 = extractvalue %swift.metadata_response %3, 0
; s4main6CircleCyACSdcfC 等价于 main.Circle.__allocating_init(Swift.Double) -> main.Circle
%5 = call swiftcc %T4main6CircleC* @"$s4main6CircleCyACSdcfC"(double 1.000000e+01, %swift.type* swiftself %4)
; 往一个内存中存储
; i32 0, i32 1 结构体不偏移,并选择第二个字段,相当于将metadata放入 T4main5ShapeP结构体的%swift.type*中 ==> type { [24 x i8], metadata, i8** }
store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"$s4main6circleAA5Shape_pvp", i32 0, i32 1), align 8
; s4main6CircleCAA5ShapeAAWP 等价于 protocol witness table for main.Circle : main.Shape in main 协议目录表,将其放入了 T4main5ShapeP 结构体的i8**中 ==> type { [24 x i8], metadata, PWT }
store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"$s4main6CircleCAA5ShapeAAWP", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"$s4main6circleAA5Shape_pvp", i32 0, i32 2), align 8
; s4main6circleAA5Shape_pvp 等价于 main.circle : main.Shape, 将%5放入了 %T4main6CircleC** 中,即 type <{ %swift.refcounted, %TSd }>,相当于将HeapObject放入T4main6CircleC中 ==> type { HeapObject, metadata, PWT }
; 将 %T4main6CircleC* %5 实例对象地址 放入了 %T4main6CircleC** 二级指针里,也就意味着实例对象占用8字节,所以放入结构体中就是占用8字节的大小
store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"$s4main6circleAA5Shape_pvp" to %T4main6CircleC**), align 8
.....
仿写结构
然后通过上述的分析,仿写整个内部结构
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个指针
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt
var pwt: UnsafeRawPointer
}
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)
withUnsafePointer(to: &circle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
protocolData(value1: 0x0000000100550100, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x0000000100008180, pwt: 0x0000000100004028)
lldb调试如下,其中value1
是HeapObject
,type
是metadata
而0x0000000100004028
可以通过nm + xcrun
来验证确实是 PWT
如果将class改成 struct呢?
- 如果其中的
类改成Struct
呢?如下所示
protocol Shape {
var area: Double {get}
}
struct Rectangle: Shape{
var width, height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
//对象类型为协议
var rectangle: Shape = Rectangle(10.0, 20.0)
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个指针
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt
var pwt: UnsafeRawPointer
}
//将circle强转为protocolData结构体
withUnsafePointer(to: &rectangle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
protocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x0000000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)
针对打印结果的lldb调试如下,value1
存储10
,value2
存储20
- 查看其IR代码
define i32 @main(i32 %0, i8** %1) #0 {
entry:
%2 = bitcast i8** %1 to i8*
; 占用16字节
%3 = call swiftcc { double, double } @"$s4main9RectangleVyACSd_SdtcfC"(double 1.000000e+01, double 2.000000e+01)
%4 = extractvalue { double, double } %3, 0
%5 = extractvalue { double, double } %3, 1
; 指针类型是 <{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*
; 第一个索引:i32 0 表示需要跨越全局变量 ,其实就是 <{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>的首地址
; 第二个索引:i32 1 选择结构体的第二个字段
; 存储到结构体的type,即metadata
store %swift.type* bitcast (i64* getelementptr inbounds (<{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>, <{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>* @"$s4main9RectangleVMf", i32 0, i32 1) to %swift.type*), %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"$s4main9rectangleAA5Shape_pvp", i32 0, i32 1), align 8
; 使用 s4main9RectangleVAA5ShapeAAWP 结构体来存储
store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"$s4main9RectangleVAA5ShapeAAWP", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"$s4main9rectangleAA5Shape_pvp", i32 0, i32 2), align 8
; 将double值放入内存中,有偏移,%4 、%5分别的偏移是0、1,是针对 T4main5ShapeP 结构体的偏移
store double %4, double* getelementptr inbounds (%T4main9RectangleV, %T4main9RectangleV* bitcast (%T4main5ShapeP* @"$s4main9rectangleAA5Shape_pvp" to %T4main9RectangleV*), i32 0, i32 0, i32 0), align 8
store double %5, double* getelementptr inbounds (%T4main9RectangleV, %T4main9RectangleV* bitcast (%T4main5ShapeP* @"$s4main9rectangleAA5Shape_pvp" to %T4main9RectangleV*), i32 0, i32 1, i32 0), align 8
......
如果struct中有3个属性呢?
- 如果struct的结构体属性是3个呢
struct Rectangle: Shape{
var width, height: Double
var width1 = 30.0
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
protocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x403e000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)
从结果中可以看出,是存储在value3
如果struct中有4个属性呢?
struct Rectangle: Shape{
var width, height: Double
var width1 = 30.0
var height1 = 40.0
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
protocolData(value1: 0x0000000100546a50, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)
其中value1
是一个堆区地址
,堆区地址中存储了4个属性的值
协议底层存储结构总结
所以针对协议,其底层的存储结构如图所示:
1、
前24
个字节,主要用于存储遵循了协议的class/struct的属性值
,如果24字节不够存储,会在堆区开辟一个内存空间用于存储,24字节中的前8个字节存储堆区地址(如果超出24,是直接分配堆区空间,然后存储值
,并不是先存储值,然后发现不够再分配堆区空间)2、
后16
个字节分别用于存储vwt
(值目录表)、pwt
(协议目录表)
继续分析
回到下面这个例子中,其中for-in循环
能区分不同的area的原因主要是因为 protocol
的pwt
,pwt
其内部也是通过class_method
查找,同时在运行过程中存储了metadata
,所以可以根据metadata找到对应的v-table,从而完成方法的调用
//2-7、回到2-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
}
}
}
class Rectangle: Shape{
var width, height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
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]
//这里能区分不同area的原因是因为 在protocol中存放了pwt(协议目录表),可以根据这个表来正确调用对应的实现方法(pwt中也是通过class_method查找,同时在运行过程中也记录了metadata,在pwt中通过metadata查找V-Table,从而完成当前方法的调用)
for shape in shapes{
print(shape.area)
}
- 继续回到
struct
的例子,将其赋值给另一个变量,其内存存放的是否是一样的?
protocol Shape {
var area: Double {get}
}
struct Rectangle: Shape{
var width, height: Double
var width1 = 30.0
var height1 = 40.0
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
//对象类型为协议
var rectangle: Shape = Rectangle(10.0, 20.0)
//将其赋值给另一个协议变量
var rectangle1: Shape = rectangle
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个指针
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt
var pwt: UnsafeRawPointer
}
//将circle强转为protocolData结构体
withUnsafePointer(to: &rectangle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
打印结果如下,两个协议变量内存存放的东西是一样
的
- 如果
修改rectangle1的width属性的值
(需要将width属性声明到protocol),修改后的代码如下
protocol Shape {
var width: Double {get set}
var area: Double {get}
}
struct Rectangle: Shape{
var width: Double
// var width, height: Double
var height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
//对象类型为协议
var rectangle: Shape = Rectangle(10.0, 20.0)
//将其赋值给另一个协议变量
var rectangle1: Shape = rectangle
//查看其内存结构体
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个指针
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt
var pwt: UnsafeRawPointer
}
//将circle强转为protocolData结构体
withUnsafePointer(to: &rectangle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
withUnsafePointer(to: &rectangle1) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
rectangle1.width = 50.0
通过lldb调试发现,在rectangle1
变量修改width
之后,其存储数据的堆区地址发生了变化。这就是所谓的写时复制
【当复制时,并没有值的修改,所以两个变量指向同一个堆区内存,当第二个变量修改了属性值时,会将原本堆区内存的值拷贝到一个新的堆区内存,并进行值的修改】
疑问1:如果将struct修改为class,是否也是写时复制?
如果上述例子中,遵循协议的是类(即struct 改成 class),是否也是写时复制
呢?
class Rectangle: Shape{
var width: Double
// var width, height: Double
var height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
lldb调试结果如下,属性值修改前后,堆区地址并没有变化
,符合对值类型和引用类型的理解
值类型
在 传递过程中 并不共享状态
引用类型
在 传递过程中共享状态
问题:如果超过24字节,是先存储到value1后发现不够再分配堆区,还是直接分配?
如下所示,struct中定义4个属性
protocol Shape {
var area: Double {get}
}
class Rectangle: Shape{
var width: Double
var height: Double
var width1: Double
var height1: Double
init(_ width: Double, _ height: Double, _ width1: Double, _ height1: Double) {
self.width = width
self.height = height
self.width1 = width1
self.height1 = height1
}
var area: Double{
get{
return width * height
}
}
}
var rectangle: Shape = Rectangle(10.0, 20.0)
- 查看其IR代码,从代码中可以看出,是
先分配堆区空间,再将属性值存储到堆区空间中
疑问3:如果是存储的值类型是String呢?
如下所示,存储的值类型是String类型,查看其底层存储情况
protocol Shape {
var area: Double {get}
}
struct Rectangle: Shape{
var height: String
init(_ height: String) {
self.height = height
}
var area: Double{
get{
return 0
}
}
}
var rectangle: Shape = Rectangle("CJL")
//查看其内存结构体
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个指针
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt
var pwt: UnsafeRawPointer
}
//将circle强转为protocolData结构体
withUnsafePointer(to: &rectangle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
查看其IR代码
lldb调试如下,底层也是
通过value存储
总结
协议在底层的存储结构体如下:
前面的24字节,官方称为
Value Buffer
,用来存储当前的值-
如果超过
Value Buffer
最大容量(24字节)值类型
采用copy-write
,即拷贝时拷贝content整体,当修改值时,会先检查引用计数,如果引用计数大于1,会开辟新的堆的内存空间
,然后将修改的值放入新的空间中,其目的是为了提高内存的利用率,降低堆区的内存消耗,从而实现性能的提升
引用类型
则是使用同一个堆区地址
,因为其拷贝变量与原变量是共享状态
总结
-
class、struct、enum都可以遵守协议,有以下几点说明:
1、多个协议之间需要使
用逗号分隔
2、如果class中有
superClass
,一般放在协议之前
-
协议中可以添加
属性
,有以下两点说明:1、属性必须明确是
可读(get)/可读可写(get + set)
的2、属性使用
var
修饰
协议中可以
定义方法
,只需要定义当前方法的名称+参数列表+返回值
,其具体实现可以通过协议的extension实现
,或者在遵守协议时实现
协议中也可以
定义初始化方法
,当实现初始化器时,必须使用required
关键字如果协议只能被class实现,需要协议继承自
AnyObject
-
协议也可以作为类型,有以下三种场景:
1、作为
函数、方法或者初始化
程序中的参数类型或者返回值
2、作为
常量、变量或属性的类型
3、作为
数组、字典或
者其他容器
中项目的类型
-
协议的底层存储结构:
24字节valueBuffer + vwt(8字节) + pwt(8字节)
1、
前24
个字节,官方称为Value Buffer
,主要用于存储遵循了协议的class/struct的属性值
-
2、如果超过
Value Buffer
最大容量(1)
值类型
采用copy-write
(2)
引用类型
则是使用同一个堆区地址
3、
后16
个字节分别用于存储vwt
(值目录表)、pwt
(协议目录表)