Swift之深入解析协议Protocol的底层原理

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift之深入解析协议Protocol的底层原理相关的知识,希望对你有一定的参考价值。

一、Swift 协议

① 概念
  • 协议规定了用来实现某一特定功能所必需的方法和属性。
  • 任意能够满足协议要求的类型被称为遵循(conform)这个协议。
  • 类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。
② 语法
  • 协议的语法格式如下:
	protocol SomeProtocol {
	    // protocol definition goes here
	}
  • 要使类遵循某个协议,需要在类型名称后加上协议名称,中间以冒号:分隔,作为类型定义的一部分。遵循多个协议时,各协议之间用逗号“,”分隔。
	struct SomeStructure: FirstProtocol, AnotherProtocol {
	    // 结构体内容
	}
  • 如果类在遵循协议的同时拥有父类,应该将父类名放在协议名之前,以逗号分隔:
	class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
	    // 类的内容
	}
③ 属性
  • 协议用于指定特定的实例属性或类属性,而不用指定是存储型属性或计算型属性。此外还必须指明是只读的还是可读可写的。
  • 遵循协议的类型只要包含协议规定的读写属性就可以,比如协议规定属性是可读的,遵循的类型可以声明为读写的,反之不行。

在这里插入图片描述

  • 协议中的通常用var 来声明变量属性,在类型声明后加上{ set get }来表示属性是可读可写的,只读属性则用{ get }来表示。

在这里插入图片描述

  • 协议要求遵循该协议的类型提供特定名字和类型的实例属性或类型属性(并不具体说明是存储属性还是计算属性,只要求有特定的名称和类型);
	protocol SomeProtocol {
	    static var name: String {get}
	    var age: Int { get set}
	}
	
	struct Hotpot: SomeProtocol {
	    static var name: String {
	        get {
	            "hotpot"
	        }
	    }
	    
	    var age: Int = 18
	}
	
	struct Cat: SomeProtocol {
	    var myAge: Int
	    static var name: String  = "hotpot"
	    var age: Int {
	        get {
	            18
	        }
	        set {
	            myAge = newValue + 1
	        }
	    }
	}
④ 方法
  • 协议中可以定义实例方法和类方法,只需要定义当前方法的名称,参数列表和返回值,不需要大括号和方法的主体。
	protocol SomeProtocol {
	    func something() -> String
	    static func doSomething()
	    func myThing()
	}
	
	extension SomeProtocol {
	    func something() -> String {
	        return "something"
	    }
	    
	    static func doSomething() {
	        print("doSomething")
	    }
	}
	
	struct Hotpot: SomeProtocol {
	    func myThing() {
	        print("myThing")
	    }
	}
	
	var hotpot = Hotpot()
	
	Hotpot.doSomething()
	hotpot.something()
	hotpot.myThing()
  • 在协议的定义中,方法参数不能定义默认值:

在这里插入图片描述

  • 协议中定义初始化方法,在类中实现初始化器的时候必须使用 required 关键字(final 类除外);由于 final 的类不会有子类,如果协议初始化器实现的类使用了 final 标记,就不需要使用 required 来修饰。

在这里插入图片描述

  • 协议要求只被能类遵循,加上 AnyObject:

在这里插入图片描述

	protocol SomeProtocol {
	    init(age: Int)
	}
	
	struct Hotpot: SomeProtocol {
	    var age: Int
	    init(age: Int) {
	        self.age = age
	    }
	}
	
	class Cat: SomeProtocol {
	    var age: Int
	    required init(age: Int) {
	        self.age = age
	    }
	}
⑤ 协议作为类型
  • 作为函数、方法或初始化程序中的参数类型或返回类型;
  • 作为常量、变量或属性的类型;
  • 作为数组、字典或其他容器中项目的类型。
⑥ 常量、变量或属性的类型
	protocol SomeProtocol {
	    func test()
	}
	
	extension SomeProtocol {
	    func test() {
	        print("SomeProtocol test()")
	    }
	}
	
	class HotpotCat: SomeProtocol {
	    func test() {
	        print("HotpotCat test()")
	    }
	}
	
	let object: SomeProtocol = HotpotCat()
	object.test()
	
	let object1: HotpotCat = HotpotCat()
	object1.test()
  • 输出结果如下:
	HotpotCat test()
	HotpotCat test()
  • SIL 代码分析如下:

在这里插入图片描述

  • object.test() 通过 PWT 来调用,对应的 PWT 如下:
	// 协议目击表记录了test函数
	sil_witness_table hidden HotpotCat: SomeProtocol module main {
	  method #SomeProtocol.test: <Self where Self : SomeProtocol> (Self) -> () -> () : @protocol witness for main.SomeProtocol.test() -> () in conformance main.HotpotCat : main.SomeProtocol in main   // protocol witness for SomeProtocol.test() in conformance HotpotCat
	}
  • 对应的 test() 实现如下:
	// protocol witness for SomeProtocol.test() in conformance HotpotCat
	// HotpotCat 遵循了协议并实现了方法之后的test函数的实现
	sil private [transparent] [thunk] @protocol witness for main.SomeProtocol.test() -> () in conformance main.HotpotCat : main.SomeProtocol in main : $@convention(witness_method: SomeProtocol) (@in_guaranteed HotpotCat) -> () {
	// %0                                             // user: %1
	bb0(%0 : $*HotpotCat):
	  %1 = load %0 : $*HotpotCat                      // users: %2, %3
	  //HotpotCat类的函数表查找test函数(V-Table)
	  %2 = class_method %1 : $HotpotCat, #HotpotCat.test : (HotpotCat) -> () -> (), $@convention(method) (@guaranteed HotpotCat) -> () // user: %3
	  %3 = apply %2(%1) : $@convention(method) (@guaranteed HotpotCat) -> ()
	  %4 = tuple ()                                   // user: %5
	  return %4 : $()                                 // id: %5
	} // end sil function 'protocol witness for main.SomeProtocol.test() -> () in conformance main.HotpotCat : main.SomeProtocol in main'
  • 通过协议目击表调用的方式本质上也是通过 V-Table 查找调用。那么如果 SomeProtocol 协议中的 test 函数注释掉呢?
	protocol SomeProtocol {
		// func test()
	}
  • 输出:
	SomeProtocol test()
	HotpotCat test()
  • 继续 SIL 代码分析:

在这里插入图片描述

  • 可以看到 object.test() 变成了静态调用,由于在 extension 中声明的方法在调度过程中为静态调度,在编译的过程中地址就已确定。在函数执行的过程中就直接拿到地址调用了,类中是没有办法修改的(重写无效),并且 PWT 中已经没有了对应方法的声明:
	sil_witness_table hidden HotpotCat: SomeProtocol module main {
}
  • 如果类中不重写 test() 方法,那么应该也是静态调度,object 和 obejct1 都输出 SomeProtocol test():
	class HotpotCat: SomeProtocol {
	//    func test() {
	//        print("HotpotCat test()")
	//    }
	}
	// 输出
	SomeProtocol test()
	SomeProtocol test()
  • 对应的 SIL 分析如下:

在这里插入图片描述

  • 协议中声明方法,伴随着遵循协议的类生成一张 PWT,协议目击表包含了类对协议的实现(如果类没有实现,则 PWT 实现是静态调度)。这个实现也是通过(V-Table/静态调度)找到类中 extension 方法的实现来调度,也就是说 PWT 中声明方法是和协议中声明方法对应的。
  • 协议中没有声明方法,只是在协议扩展中给了默认实现,在编译过程中地址已经确定了,对于遵守协议的类来说无法重写方法。

二、PWT

  • 了解了协议中声明方法的调用,那么 PWT 存储在哪呢?内存大小一样么?
  • 如下所示:
	protocol Shape {
	    var area: Double{ get }
	}
	
	extension Shape {
	    var area: Double {
	        0
	    }
	}
	
	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)
	print(MemoryLayout.size(ofValue: circle))
	print(MemoryLayout.stride(ofValue: circle))
	
	var circle1: Circle = Circle.init(10.0)
	print(MemoryLayout.size(ofValue: circle1))
	print(MemoryLayout.stride(ofValue: circle1))
  • 输出结果如下:
	40
	40
	8
	8
  • 可以看到声明为 Shape 类型,大小变为了 40。
  • 内存结构分析如下:

在这里插入图片描述

  • 继续分析 SIL:

在这里插入图片描述

  • 可以看到取 cirlce 从 load 变成了 init_existential_addr,使用容器包含了 Shape 类型,使用这个类型初始化 circle 变量,相当于对 circle 包装了一层。
	sil-instruction ::= 'init_existential_addr' sil-operand ',' sil-type
	
	%1 = init_existential_addr %0 : $*P, $T
	// %0 must be of a $*P address type for non-class protocol or protocol
	//   composition type P
	// $T must be an AST type that fulfills protocol(s) P
	// %1 will be of type $*T', where T' is the maximally abstract lowering
	//    of type T
  • 通过 IR 分析下到底存储的是什么?
	;{24字节,swift.type指针,二级指针}
	%T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }
	
	;heapobject
	%T4main6CircleC = type <{ %swift.refcounted, %TSd }>
	
	define i32 @main(i32 %0, i8** %1) #0 {
	entry:
	  %2 = bitcast i8** %1 to i8*
	  %3 = call swiftcc %swift.metadata_response @"type metadata accessor for main.Circle"(i64 0) #7
	  %4 = extractvalue %swift.metadata_response %3, 0
	 ;double 1.000000e+0110%swift.type* 类的元数据
	  %5 = call swiftcc %T4main6CircleC* @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 1.000000e+01, %swift.type* swiftself %4)
	  ;metadata存储%4对应 %T4main5ShapeP 结构体的%swift.type*。相当于把metadata放到了结构体中 { [24 x i8], metadata, i8** }
	  store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.circle : main.Shape", i32 0, i32 1), align 8
	  ;PWT地址存储到 %T4main5ShapeP 到 i8** { [24 x i8], metadata, 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.circle : main.Shape", i32 0, i32 2), align 8
	 ;%5(heapObject)放到%T4main6CircleC{ heapObject, metadata, PWT }
	  store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"main.circle : main.Shape" to %T4main6CircleC**), align 8
	  ret i32 0
	}
  • 也就是最终结构是{ heapObject, metadata, PWT },代码还原下:
	struct ProtocolData {
	    // 24字节
	    var value1: UnsafeRawPointer
	    var value2: UnsafeRawPointer
	    var value3: UnsafeRawPointer
	    // metadata
	    var metadata: UnsafeRawPointer // 这里存储为了找到 VWT(Value Witness Table)
	    // pwt
	    var pwt: UnsafeRawPointer
	}
  • 完整代码:
	protocol Shape {
	    var area: Double{ get }
	}
	
	extension Shape {
	    var area: Double {
	        0
	    }
	}
	
	class Circle: Shape {
	    var radious: Double
	
	    init(_ radious: Double) {
	        self.radious = radious
	    }
	
	    var area: Double{
	        get {
	            return radious * radious * 3.14
	        }
	    }
	}
	
	struct ProtocolData {
	    // 24字节
	    var value1: UnsafeRawPointer
	    var value2: UnsafeRawPointer
	    var value3: UnsafeRawPointer
	    // metadata
	    var metadata: UnsafeRawPointer // 这里存储为了找到 VWT(Value Witness Table)
	    // pwt
	    var pwt: UnsafeRawPointer
	}
	
	withUnsafePointer(to: &circle) { ptr  in
	    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1) { protocolPtr in
	        print(protocolPtr.pointee)
	    }
	}
  • 内存结构如下:
    在这里插入图片描述

  • 在这里 PWT 就是 0x0000000100004028,符号表中查看下:

~ nm -p /Users/binxiao/Library/Developer/Xcode/DerivedData/SwiftProtocol-benaiiiiyumfmlauiejfynxbwtfi/Build/Products/Debug/SwiftProtocol | grep 0000000100004028
	0000000100004028 S _$s13SwiftProtocol6CircleCAA5ShapeAAWP
	➜  ~ xcrun swift-demangle s13SwiftProtocol6CircleCAA5ShapeAAWP
	$s13SwiftProtocol6CircleCAA5ShapeAAWP ---> protocol witness table for SwiftProtocol.Circle : SwiftProtocol.Shape in SwiftProtocol~
  • 可以看到确实是 PWT,这里存储 PWT 的目的是调用的时候找到对应的方法,这也就解释了最开始内存大小为 40 的原因。
  • 上面还原的数据结构有 3 个 value,分析完了类,改为结构体再分析下:
	protocol Shape {
	    var area: Double{ get }
	}
	
	extension Shape {
	    var area: Double {
	        0
	    }
	}
	
	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
	        }
	    }
	}
	struct ProtocolData {
	    // 24字节
	    var value1: UnsafeRawPointer
	    var value2: UnsafeRawPointer
	    var value3: UnsafeRawPointer
	    // metadata
	    var metadata: UnsafeRawPointer// 这里存储为了找到 VWT(Value Witness Table)
	    // pwt
	    var pwt: UnsafeRawPointer
	}
	
	var circle: Shape = Rectangle.init(10, 20)
	
	withUnsafePointer(to: &circle) { ptr  in
	    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1) { protocolPtr in
	        print(protocolPtr.pointee)
	    }
	}
  • SIL 分析如下:
	define i32 @main(i32 %0, i8** %1) #0 {
	entry:
	  %2 = bitcast i8** %1 to i8*
	  %3 = call swiftcc { double, double } @"main.Rectangle.init(Swift.Double, Swift.Double) -> main.Rectangle"(double 1.000000e+01, double 2.000000e+01)
	  %4 = extractvalue { double, double } %3, 0
	  %5 = extractvalue { double, double } %3, 1
	  store %swift.type* bitcast (i64* getelementptr inbounds (<{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>, <{ i8**, i64, <以上是关于Swift之深入解析协议Protocol的底层原理的主要内容,如果未能解决你的问题,请参考以下文章

Swift之深入解析“泛型”的底层原理

Swift之深入解析内存管理的底层原理

Swift之深入解析可选类型Optional的底层原理

Swift之深入解析枚举enum的底层原理

iOS开发-Swift进阶之协议Protocol!

iOS之深入解析分类Category的底层原理