一、协议
1.1 协议的用法
class本质上是定义了对象是什么protocol本质上定义了一个对象有哪些行为
class LGTeacher{
var age = 10
var name = "Kody"
}
extension LGTeacher:CustomStringConvertible{
var description: String {
get{
return "LGTeacher-age:\(age) name:\(name)"
}
}
}
class Dog{
var name = "糯米"
var type = "白梗"
}
extension Dog:CustomStringConvertible{
var description: String {
get{
return "Dog-age:\(name) name:\(type)"
}
}
}
1.2 协议的基本语法
- 协议要求属性必须是var类型
- 并且明确 get 或 get set,实现协议者必须实现对应的 get set
- 只声明了 get 并不是说这个属性一定是计算属性
protocol MyProtocal{
var age:Int{ get set }
var name:String{ get }
}
class CTTeacher:MyProtocal{
var age: Int = 18
var name: String
init(_ name:String) {
self.name = name
}
}
协议中的异变方法
- 表示在该方法可以改变其所属的实例以及该实例的所有属性(用于枚举和结构体)
- 在类实现该方法的时候不需要写
mutating关键字
protocol Togglable{
mutating func togglefunc()
}
struct CTTeacher2:Togglable{
mutating func togglefunc() {
}
}
class CTTeacher3:Togglable{
func togglefunc() {
}
}
类在实现协议中的初始化器
- 必须使用
required关键字修饰初始化器的实现 - 类的初始化器前添加
required修饰符来表明所有该类的子类 都 必须实现该初始化器
protocol MyProtocal2{
init(_ age:Int)
}
class CTTeacher4:MyProtocal2{
var age:Int = 0
required init(_ age: Int) {
self.age = age
}
}
//或者前面加 final 表示该类不可继承
final class CTTeacher4:MyProtocal2{
var age:Int = 0
init(_ age: Int) {
self.age = age
}
}
类专用协议
- 通过添加
AnyObject关键字到协议的继承列表,来限制此协议只能被 class类 遵循
protocol MyProtocal3:AnyObject{
}
class CTTeacher5:MyProtocal3{
}
//报错:Non-class type 'CTTeacher6' cannot conform to class protocol 'MyProtocal3'
struct CTTeacher6:MyProtocal3{
}
可选协议
- 如果我们不想强制让遵循协议的类实现,可以使用
optional前缀 - 注意要标识为
@objc
@objc protocol MyProtocal4{
@objc optional func increment(by:Int)
}
1.3 witness_table
再来个例子:
一般类的方法都会在v-table里,那么遵循了协议的方法,会不会也在v-table里?
protocol MyProtocal4{
func increment(by:Int)
}
class CCTeacher:MyProtocal4{
//在不在v-table里面?
func increment(by: Int) {
print(by)
}
}
var t:CCTeacher = CCTeacher()
t.increment(by: 10)
其中increment()方法,编译成sil看一下:
看main函数,是一个class_method调度,也就是在v_table中调度
再看到最后面的v_table:
但如果 var t:CCTeacher = CCTeacher() 改为:var t:MyProtocal4 = CCTeacher()呢?
先看一下main函数:
这次不是class_method调度了,是witness_method调度
再看看v-table,会发现v_table中确实也有increament()方法,但是多了一个witness_table表。
也就是说main函数是在witness_table表中查找对应的方法。文档链接
那再看看看witness_table如何调用到increament()方法的:本质上最终会通过class_method调度,去到CCTeacher类的v_table中调度increament()方法,也就是说witness_table可以理解为一个中间层。 
}
extension Incrementable{
func increment(by: Int){
print("Incrementable")
}
}
class CCTeacher:Incrementable{
func increment(by: Int) {
print("CCTeacher")
}
}
var t:Incrementable = CCTeacher()
t.increment(by: 11)
运行一下:
如果注释掉 protocol 里面的 func increment(by: Int),运行一下:
为啥出现这种情况?编译成sil文件看一下:
1、 protocol 里面的 func increment(by: Int)没注释掉的情况:走的是witness_table调用,最终会到CCTeacher的v_table中找increment方法。
2、protocol 里面的 func increment(by: Int)注释掉了的情况:直接调用了协议扩展中的increment()的函数地址,因为extension声明的函数是默认派发的,也就是在编译时extension中的函数地址已经明确了。
来个例子:
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 circle1:Circle = Circle.init(10.0)
print(class_getInstanceSize(Circle.self)) //16 + 8 = 24
print(MemoryLayout.size(ofValue: circle1)) //8
var circle2:Shape = Circle.init(10.0)
var circleType = type(of: circle2)
print(class_getInstanceSize(circleType as! AnyClass)) //16 + 8 = 24
print(MemoryLayout.size(ofValue: circle2)) //40
可以看出,静态类型不一致(var circle1:Circle 以及 var circle2:Shape),那么变量里面存的东西是不一样的。如果静态类型不一致,变量里到底存了什么?
1、先看静态类型一致的时候(var circle1:Circle)
变量circle存的就是引用对象在堆空间的内存地址
2、看静态类型不一致的时候(var circle1:Shape)
分析到这里,可以初步得出一个结构:
struct CTProtocolBox {
var valueBuffer1: UnsafeRawPointer
var valueBuffer2: UnsafeRawPointer
var valueBuffer3: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeRawPointer
}
以下代码,编译成IR代码来看看
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 circle:Shape = Circle.init(10.0)
截取main函数的部分:
我们仔细看到这个部分:
我们全局查一下s4main6CircleCAA5ShapeAAWP是个什么:
这里面存储了两个值:一个是s4main6CircleCAA5ShapeAAMc,一个是s4main6CircleCAA5ShapeA2aDP4areaSdvgTW,可以通过在终端的xcrun swift-demangle命令来还原一下分别是什么
xcrun swift-demangle s4main6CircleCAA5ShapeAAMc:
protocol conformance descriptor for main.Circle : main.Shape in main
xcrun swift-demangle s4main6CircleCAA5ShapeA2aDP4areaSdvgTW:
protocol witness for main.Shape.area.getter : Swift.Double in conformance main.Circle : main.Shape in main
可以看出来,第一个是一个叫做protocol conformance descriptor这么个类型的玩意儿,第二个是实现的协议方法 area的getter
到这里可以进一步补充协议的数据类型:
struct CTProtocolBox {
var valueBuffer1: UnsafeRawPointer
var valueBuffer2: UnsafeRawPointer
var valueBuffer3: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}
struct TargetWitnessTable{
var protocol_conformance_descriptor: UnsafeRawPointer
var witnessMethod: UnsafeRawPointer
}
那么protocol_conformance_descriptor这个玩意儿里面存了什么呢?查阅源码 swift-source -> Metadata.h -> 搜索 TargetWitnessTable
分析后最终可得出这样协议的结构:
struct CTProtocolBox {
var valueBuffer1: UnsafeRawPointer
var valueBuffer2: UnsafeRawPointer
var valueBuffer3: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}
struct TargetWitnessTable{
var protocol_conformance_descriptor: UnsafeMutablePointer<TargetProtocolConformanceDescriptor>
var witnessMethod: UnsafeRawPointer
}
struct TargetProtocolConformanceDescriptor{
var protocolDesc: 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))
}
}
}
跑个例子看一下:
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.protocolDesc.getmeasureRelativeOffset()
print(String(cString: descPtr.pointee.Name.getmeasureRelativeOffset()))
print(pointer.pointee.witness_table.pointee.witnessMethod)
print("end")
}
}
这个 0x0000000100004130 实现的协议方法可以通过mach-o文件来交叉验证:
命令行输入:nm -p mach-o | grep 0000000100004130
总结:
每个遵循协议的class都有自己的 witnessTable,遵循多少个协议,就有多少个witnessTable,现协议里的函数越多,witnessTable里面存储的函数地址就越多。
1.5 Existential Container 存在容器
上面我们总结了这么个结构出来
struct CTProtocolBox {
var valueBuffer1: UnsafeRawPointer
var valueBuffer2: UnsafeRawPointer
var valueBuffer3: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}
这个玩意儿其实是编译器生成的一种特殊的数据类型,称为 Existential Container存在容器,用于管理遵守了相同协议的协议类型,因为这些类型的内存大小不一致,所以通过Existential Container存在容器进行统一管理。
说人话:
上面的例子中:如果不是class引用类型,而是struct值类型,或者内部不止只有var radious:Double这一个属性,有很多个,在 var circle:Shape = Circle.init(10.0) 中,编译器无法直接推断 circle变量 占用多大内存,也就是说无法为 circle 这个变量分配内存空间,它占用多大内存取决于它的动态类型,也就是 Circle.init(10.0);为了解决这个问题,那统一的,都放在一个40字节的
Existential Container存在容器当中,可以理解为一个中间层。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)
-
1、对于小容量的数据,直接存储在 value Buffer中
我们把它的前24个字节,称为valueBuffer,是一个存放值得区域,一个连续的空间
struct CTProtocolBox {
var valueBuffer1: UnsafeRawPointer
var valueBuffer2: UnsafeRawPointer
var valueBuffer3: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}
例子:
protocol Shape {
var radious: Double {get set}
}
struct Circle: Shape{
var radious: Double
init(_ radious: Double) {
self.radious = radious
}
}
var circle: Shape = Circle.init(10.0)
对于Struct值类型来说,如果它的值能够在valueButter的24字节中放得下,那么就会依次存放在valueBuffer中。
-
2、对于大容量的数据,通过堆区分配,存储堆空间的地址
protocol Shape {
var radious: Double {get set}
}
struct Circle: Shape{
var radious: Double
var width: Double = 20
var height: Double = 30
var height1: Double = 30
init(_ radious: Double) {
self.radious = radious
}
}
var circle: Shape = Circle.init(10.0)
对于Struct值类型来说,如果它的值超过了valueButter的24字节,那么会把所有内容移到堆区存放,并且在valueBuffer中存储其对应的堆区地址。
2、写时复制
针对【存在容器中存储堆空间地址 的情况】,在进行对象赋值后,新对象在修改的时候,会有写时复制的操作。
来个例子:
protocol Shape {
var radious: Double {get set}
var width: Double {get set}
var height: Double {get set}
var height1: Double {get set}
}
struct Circle: Shape{
var radious: Double
var width: Double = 20
var height: Double = 30
var height1: Double = 30
init(_ radious: Double) {
self.radious = radious
}
}
var circle: Shape = Circle.init(10.0)
var circle1: Shape = circle
print("修改前")
circle1.width = 50.0
print("修改后")
把circle赋值给circle1,并且修改circle1,在修改前,内存情况是这样的:
修改了circle1之后,内存情况是这样的:
也就是说
circle1修改前和修改后的内存情况是不一样的,在进行修改的时候,会判断circle的引用计数是不是大于1,如果大于1,会进行写时复制。