Protocol
:所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有这些内容。协议实际上做的事情不过是“关于实现的约定”
- 在
Swift
中协议被赋予更加强大、灵活的功能。相比Objective-C
的协议,Swift
的协议不仅可以被用做代理,也可以用作对接口的抽象,对代码的复用。- 协议规定了用来实现某一特定功能所必需的方法和属性。任意能够满足协议要求的类型被称为遵循(
conform
)协议。- 类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。
基本⽤法
语法格式
protocol MyProtocol { // 属性 // 方法 }
class
,struct
,enum
都可以遵循协议,如果要遵守多个协议,使⽤逗号分隔struct LGTeacher: Protocol1, Protocol2 { // 属性 // 方法 }
如果
class
中有superClass
,⼀般放在遵循的协议之前class LGTeacher: NSObject, MyProtocol{ // 属性 // 方法 }
协议中属性定义的要求
- 属性不能设置默认值
- 属性必须明确是可读或可读可写
- 属性必须使用
var
修饰
协议中方法定义的要求
- 方法不能有方法体
- 方法参数不能设置默认值
遵循协议并实现协议中的属性、方法
protocol MyProtocol {
var age: Int { get }
var name: String { get set }
func doSomething(age: Int) -> Int
static func teach()
}
class LGTeacher: MyProtocol {
let age: Int = 18
var name: String = "Zang"
func doSomething(age: Int = 18) -> Int {
print("LGTeacher doSomething")
return age
}
static func teach() {
print("teach")
}
}
LGTeacher
遵循MyProtocol
协议
- 实现协议的属性,可以设置默认值。只读属性可以选择
let
修饰- 实现协议的方法,参数可以设置默认值
- 协议中使用
static
修饰的类型方法,可以被类或结构体遵循。被类遵循,可以选择将static
替换为class
修饰
func
使用static
或者class
修饰的区别与联系
static
或者class
关键字,都可用于指定类方法class
关键字指定的类方法可以被子类重写static
关键字指定的类方法不能被子类重写
required关键字
协议也可以定义初始化⽅法,当类实现初始化器的时候,必须使⽤
required
关键字
protocol MyProtocol {
init(age: Int)
}
class LGTeacher: MyProtocol{
var age: Int
required init(age: Int) {
self.age = age
}
}
如果当前类被
final
修饰,可以忽略required
关键字,因为被final
修饰的类不能被继承
protocol MyProtocol: AnyObject {
init(age: Int)
}
final class LGTeacher: MyProtocol{
var age: Int
init(age: Int) {
self.age = age
}
}
使用
编译报错final
修饰的类被继承,编译报错
要求协议只能被类遵循
如果要求协议只能被类遵循,可以添加
AnyObject
protocol MyProtocol: AnyObject {
init(age: Int)
}
class LGTeacher: MyProtocol{
var age: Int
required init(age: Int) {
self.age = age
}
}
当
编译报错struct
或enum
遵循MyProtocol
协议,编译报错
将协议作为类型
- 作为函数、⽅法或初始化程序中的参数类型或返回类型
- 作为常量、变量或属性的类型
- 作为数组、字典或其他容器中项⽬的类型
案例1:
MyProtocol
协议定义teach
方法,在MyProtocol
扩展和LGTeacher
中都实现teach
方法,查看案例1
的打印结果:
protocol MyProtocol {
func teach()
}
extension MyProtocol {
func teach() {
print("MyProtocol")
}
}
class LGTeacher: MyProtocol {
func teach() {
print("MyClass")
}
}
let t1: MyProtocol = LGTeacher()
t1.teach()
let t2: LGTeacher = LGTeacher()
t2.teach()
//输出以下内容:
//MyClass
//MyClass
t1
声明MyProtocol
类型,protocol
中定义了teach
⽅法,使用witness_method
调用,通过PWT
协议⽬击表获取对应的函数地址,打印结果MyClass
t2
声明LGTeacher
类型,使用class_method
调用,通过V-table
函数表查找函数,打印结果MyClass
将上述代码生成SIL文件:
mainswiftc -emit-sil main.swift | xcrun swift-demangle
PWT
PWT
协议⽬击表,LGTeacher
遵循MyProtocol
协议,并记录实现的方法
遵循协议后被实现的协议方法
bb0teach
,内部使用class_method
指令通过类的函数表查找函数
案例2:
如果
MyProtocol
是空协议,在MyProtocol
扩展和LGTeacher
中实现teach
方法,查看案例2
的打印结果:
protocol MyProtocol {}
extension MyProtocol {
func teach() {
print("MyProtocol")
}
}
class LGTeacher: MyProtocol {
func teach() {
print("MyClass")
}
}
let t1: MyProtocol = LGTeacher()
t1.teach()
let t2: LGTeacher = LGTeacher()
t2.teach()
//输出以下内容:
//MyProtocol
//MyClass
t1
声明MyProtocol
类型,protocol
中未定义任何⽅法,直接使用extension
静态调用。静态调用在编译链接之后方法地址已经确定,对于遵循协议的类来说⽆法重写,打印结果MyProtocol
t2
声明LGTeacher
类型,使用class_method
调用,通过V-table
函数表查找函数,打印结果MyClass
将上述代码生成SIL文件:
mainswiftc -emit-sil main.swift | xcrun swift-demangle
PWT
PWT
协议⽬击表,虽然LGTeacher
遵循MyProtocol
协议,但没有记录任何方法,同时也找不到上面定义的被实现的协议方法
案例3:
MyProtocol
协议定义teach
方法,仅MyProtocol
扩展实现teach
方法,查看案例3
的打印结果:
protocol MyProtocol {
func teach()
}
extension MyProtocol {
func teach() {
print("MyProtocol")
}
}
class LGTeacher: MyProtocol {}
let t1: MyProtocol = LGTeacher()
t1.teach()
let t2: LGTeacher = LGTeacher()
t2.teach()
//输出以下内容:
//MyProtocol
//MyProtocol
t1
声明MyProtocol
类型,protocol
中定义了teach
⽅法,使用witness_method
调用,通过PWT
协议⽬击表获取对应的函数地址,打印结果MyProtocol
t2
声明LGTeacher
类型,由于类中没有实现teach
方法,直接使用extension
静态调⽤,打印结果MyProtocol
将上述代码生成SIL文件:
mainswiftc -emit-sil main.swift | xcrun swift-demangle
PWT
PWT
协议⽬击表,LGTeacher
遵循MyProtocol
协议,并记录实现的方法
遵循协议后被实现的协议方法
bb0teach
,内部使用extension
静态调用
PWT
案例1
定义
Circle
类遵循Shape
协议,声明Shape
类型和Circle
类型实例变量,打印各自的大小和步长
protocol Shape{
var area: Double{ get }
}
class Circle: Shape {
var radious: Double
init(_ radious: Double) {
self.radious = radious
}
var area: Double{
get{
return radious * radious * 3.14
}
}
}
var shape: Shape = Circle.init(10.0)
print("shape of size:\(MemoryLayout.size(ofValue: shape))")
print("shape of stride:\(MemoryLayout.stride(ofValue: shape))")
var circle: Circle = Circle.init(10.0)
print("circle of size:\(MemoryLayout.size(ofValue: circle))")
print("circle of stride:\(MemoryLayout.stride(ofValue: circle))")
//输出以下内容:
//shape of size:40
//shape of stride:40
//circle of size:8
//circle of stride:8
shape
声明为Shape
类型,大小和步长占40字节
。circle
声明为Circle
类型,大小和步长只占8字节
使用
lldblldb
调式,能看到的内容有限,无法得出结论
将上述代码生成SIL文件:
mainswiftc -emit-sil main.swift | xcrun swift-demangle
通过官方文档,查看
init_existential_addrinit_existential_addr
的作用
Existential Container
是编译器⽣成的⼀种特殊数据类型,⽤于管理遵守了相同协议的协议类型。因为这些数据类型的内存空间尺⼨不同,使⽤Extential Container
进⾏管理可以实现存储⼀致性如果想进一步分析,明确
init_existential_addr
存储的到底是什么,需要将SIL
代码再降⼀级,通过IR
代码观察
将上述代码生成IR文件:
IRswiftc -emit-ir main.swift | xcrun swift-demangle
%T4main5ShapeP
是一个结构体,有一个24 x i8
的数组,占24字节
。有一个%swift.type*
的指针和一个i8**
的二级指针%T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }
%4
存储的是metadata
%4 = extractvalue %swift.metadata_response %3, 0
%5
存储的是HeapObject
,它调用了__allocating_init
,传入了一个double
类型和一个metadata
%5 = call swiftcc %T4main6CircleC* @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 1.000000e+01, %swift.type* swiftself %4)
图中第一句代码,获取结构体本身,拿到第一个元素,然后将
%4
也就是metadata
,存储到结构体的第一个元素中store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.shape : main.Shape", i32 0, i32 1), align 8
图中第二句代码,获取结构体本身,拿到第二个元素,然后将
PWT
协议目击表,存储到结构体的第二个元素中。代码中@"protocol witness table for main.Circle : main.Shape in main"
就是PWT
store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Circle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.shape : main.Shape", i32 0, i32 2), align 8
图中第三句代码,实例对象
main.shape
是结构体首地址24 x i8
数组类型,
使用bitcast
将其强转为%T4main6CircleC
二级指针类型,再将%5
也就是HeapObject
存储到强转后的指针中store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"main.shape : main.Shape" to %T4main6CircleC**), align 8
最终可以分析出
%T4main5ShapeP
结构体的元素构成type { HeapObject, metadata, PWT }
使用
Swift
代码还原上述结论定义
HeapObject
结构体
struct HeapObject{
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
定义
ProtocolData
结构体,value1
、value2
、value3
三个属性,各自占8字节
,连续内存空间存储,对应IR
代码中的24 x i8
数组。type
对应metadata
,pwt
对应PWT
协议目击表
struct ProtocolData {
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
var type: UnsafeRawPointer
var pwt: UnsafeRawPointer
}
将实例对象
shape
转换为ProtocolData
结构
withUnsafePointer(to: &shape){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
print(pointer.pointee)
}
}
//输出以下结果:
//ProtocolData(value1: 0x0000000100705910, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x000000010000c3b0, pwt: 0x0000000100008070)
value1
存储HeapObject
地址,value2
、value3
未被使用type
存储的metadata
,实际上是VWT
(Value Witness Table)pwt
存储的PWT
协议目击表进入终端,使用
nm
命令查看pwt
存储的0000000100008070
地址:nm /Users/zang/Library/Developer/Xcode/DerivedData/LGSwiftTest-ezhaqfxldtlovoammsyvcyhxjxoj/Build/Products/Debug/LGSwiftTest | grep 0000000100008070
输出结果:
0000000100008070 S _$s11LGSwiftTest6CircleCAA5ShapeAAWP
使用
xcrun
还原符号:xcrun swift-demangle s11LGSwiftTest6CircleCAA5ShapeAAWP
输出结果:
$s11LGSwiftTest6CircleCAA5ShapeAAWP ---> protocol witness table for LGSwiftTest.Circle : LGSwiftTest.Shape in LGSwiftTest
这也解释了为什么上面看到的
Shape
类型实例对象会占40字节
;其中value1
、value2
、value3
三个属性共占24字节
,type
和pwt
共占16字节
案例2
如果定义的是结构体,遵循
Shape
协议,存储的结构会有哪些变化?
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 shape: Shape = Rectangle.init(10.0, 20.0)
将上述代码生成IR文件:
IRswiftc -emit-ir main.swift | xcrun swift-demangle
图中
%4
、%5
发生了变化,直接将结构体的两个double
属性取出来,存储到%T4main5ShapeP
结构体首地址的24 x i8
数组中
将结构体类型的实例对象
shape
转换为ProtocolData
结构
withUnsafePointer(to: &shape){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
print(pointer.pointee)
}
}
//输出以下结果:
//ProtocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x0000000000000000, type: 0x00000001000040e0, pwt: 0x0000000100004070)
value1
存储width
属性,value2
存储height
属性,value3
未被使用
type
存储的metadata
,实际上是VWT
(Value Witness Table
)
pwt
存储的PWT
协议目击表
案例3
如果结构体大小超过
24字节
,存储的结构会有哪些变化?
protocol Shape{
var area: Double{ get }
}
struct Rectangle: Shape {
var width, height: Double
var width1: Double = 40.0
var height1: Double = 50.0
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
var shape: Shape = Rectangle.init(10.0, 20.0)
在
案例2
的基础上,将结构体增加width1
、height1
两个属性,这时结构体的大小已超过24字节
withUnsafePointer(to: &shape){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
print(pointer.pointee)
}
}
//输出以下结果:
//ProtocolData(value1: 0x0000000103304080, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x0000000100004108, pwt: 0x0000000100004098)
只有
value1
有值,value2
、value3
未被使用
type
和pwt
没有变化
通过
lldblldb
,查看value1
存储的是什么
- 结构体首地址
24字节
,存储的是遵循协议的结构体或类的值- 如果是值类型,大小在
24字节
以内,直接存储值。如果超过24字节
,系统将在堆区分配内存空间,将值存储到堆区,然后将堆区的指针地址存储到前面8字节
,此时value2
、value3
未被使用- 如果是引用类型,直接存储
HeapObject
案例4
声明两个协议类型实例对象,将它们存储到数组。循环打印
shape.area
属性时,它们可以正确调用到各自的实现方法
protocol Shape{
var area: Double{ get }
}
class Circle: Shape {
var radious: Double
init(_ radious: Double) {
self.radious = radious
}
var area: Double{
get{
return radious * radious * 3.14
}
}
}
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 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
理解了前三个案例,对于
案例4
的打印结果应该不会意外;本质上协议容器里存放了PWT
协议目击表和metadata
,在协议目击表内依然使用class_method
查表方式,通过metadata
找到对应的V-table
,最终调用正确的实现方法
案例5
声明
Shape
类型circle
,将circle
赋值给circle1
,这时打印circle
和circle1
的value1
地址,会是相同地址吗?如果修改circle1.radious
属性,他们value1
的地址又会发生怎样的变化?
protocol Shape{
var radious: Double { get set }
var area: Double { get }
}
struct Circle: Shape {
var radious: Double
var radious1: Double = 3
var radious2: Double = 6
var radious3: Double = 9
init(_ radious: Double) {
self.radious = radious
}
var area: Double{
get{
return radious * radious * 3.14
}
}
}
struct ValueData {
var type: HeapObject
var val1: Double
var val2: Double
var val3: Double
var val4: Double
}
var circle: Shape = Circle.init(10.0)
var circle1 = circle
withUnsafePointer(to: &circle){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
print("circle.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
}
}
withUnsafePointer(to: &circle1){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
print("circle1.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
}
}
circle1.radious = 20.0
print("\n----------\n")
withUnsafePointer(to: &circle){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
print("circle.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
}
}
withUnsafePointer(to: &circle1){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
print("circle1.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
}
}
//输出一下结果:
//circle.value1 - 修改前地址:0x0000000100406ae0,值:10.0
//circle1.value1 - 修改前地址:0x0000000100406ae0,值:10.0
//
//----------
//
//circle.value1 - 修改后地址:0x0000000100406ae0,值:10.0
//circle1.value1 - 修改后地址:0x000000010052ed30,值:20.0
在
circle1.radious
修改前,circle
和circle1
的value1
地址是相同的。当circle1.radious
发生改变后,circle1.value1
的地址也发生了改变,这种现象称之为“写时复制”(copy-on-write
)
上述代码为什么会触发的“写时复制”?
Circle
是结构体类型,也就是值类型,所以circle
和circle1
并不共享状态- 当
circle1.radious
发生改变,相当于修改副本,对circle
没有任何影响protocol
结构体中24字节
官方叫法是Value Buffer
,Value Buffer
用来存储当前的值,如果超过Value Buffer
的最大存储容量,系统会开辟堆空间存储值,Value Buffer
存储堆区指针地址- 修改堆空间里的值类型,在修改前会先检测引用计数,如果引用计数大于
1
,此时系统会开辟新的堆空间,把要修改的内容拷贝到新空间内,目的是提升性能- 如果是引用类型
Class
,则不会触发写时复制
案例6
将
案例5
的结构体修改为Class
,查看输出结果的变化
class Circle: Shape {
var radious: Double
init(_ radious: Double) {
self.radious = radious
}
var area: Double{
get{
return radious * radious * 3.14
}
}
}
var circle: Shape = Circle.init(10.0)
var circle1 = circle
withUnsafePointer(to: &circle){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
print("circle.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
}
}
withUnsafePointer(to: &circle1){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
print("circle1.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
}
}
circle1.radious = 20.0
print("\n----------\n")
withUnsafePointer(to: &circle){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
print("circle.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
}
}
withUnsafePointer(to: &circle1){ ptr in
ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
print("circle1.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
}
}
//输出一下结果:
//circle.value1 - 修改前地址:0x0000000103204a20,值:10.0
//circle1.value1 - 修改前地址:0x0000000103204a20,值:10.0
//
//----------
//
//circle.value1 - 修改后地址:0x0000000103204a20,值:20.0
//circle1.value1 - 修改后地址:0x0000000103204a20,值:20.0
Circle
修改为Class
类型,也就是引用类型,所以circle
和circle1
共享状态。当circle1.radious
发生改变,circle
也会随之改变
网友评论