Swift 协议与泛型的实现

概念简述

数据结构

Swift的数据结构可以大体拆分为:ClassStructEnum

内存分配(Memory Allocation)

每一个进程都有独立的进程空间,进程空间中能够用于内存分配的区域主要分为两种:

  • 栈区(Stack)
  • 堆区(Heap)
  • 对象的内存分配

这里需要借助一下swift编译过程中产生的中间语言 SILSwift Intermediate Language

Swift编程语言是在LLVM上构建,并且使用LLVM IR和LLVM的后端去生成代码。但是Swift编译器还包含新的高级别的中间语言,称为SILSIL会对Swift进行较高级别的语义分析和优化。

image.png

//main.swift

class Person {

    var name: String?

    func doSometing() { }

}



let person: Person = Person()

person.doSometing()

let type: Person.Type = Person.self
复制代码

命令 swiftc -emit-sil

class_method 表示类方法的动态分派是通过sil_vtable来实现。

viewController.swift

class Person {

    var name: String?

    func doSometing() { }

}



class ViewController: UIViewController {



    var person: Person? //在堆区分配内存



    override func viewDidLoad() {

        super.viewDidLoad()

        person = Person()

        person?.doSometing()

    }

}
复制代码

Type Metadata

The Swift runtime keeps a metadata record for every type used in a program, including every instantiation of generic types.

github.com/apple/swift…

Swift运行时为程序中使用的每种类型保存元数据记录,包括泛型类型的每个实例化。

内存中的存储结构

CleanShot 2022-03-25 at 18.13.44@2x.png

对象的前8个字节指向类型信息,也就是指向元类型(metadata)。所以这8个字节就是一个元类型指针。

方法分派方式

一个方法会在运行时被调用或者是一个方法被唤起,是因为编译器有一个计算机制,用来选择正确的方法,然后通过传递参数来唤起它,这个机制通常被成为分派(dispatch),分派就是处理方法调用的过程。方法从书写完成到调用完成,概括上会经历编译期和运行期两个阶段,确定哪个方法被执行,也是在这两个时期进行的。故选择正确方法的阶段,可以分为编译期和运行期,而分派机制通过这两个不同的时期分为两种:

  • 静态分派(static dispatch)
  • 动态分派(dynamic dispatch)

能够在编译期确定执行方法的方式叫做静态分派,无法在编译期确定,只能在运行时去确定执行方法的分派方式叫做动态分派。

Static dispatch更快,而且静态分派可以进行内联等进一步的优化,使得执行更快速,性能更高。

但是对于多态的情况,我们不能在编译期确定最终的类型,这里就用到了Dynamic dispatch动态分派。动态分派的实现是,每种类型都会创建一张表,表内是一个包含了方法指针的数组。动态分派更灵活,但是因为有查表和跳转的操作,并且因为很多特点对于编译器来说并不明确,所以相当于block了编译器的一些后期优化。所以速度慢于Static dispatch

bugs.swift.org/browse/SR-5…

组件关系

在swift中组件关系可以分为inheritanceprotocolsgenerics

  • inheritance 继承
  • protocols 协议
  • generics 泛型

多态(特性)

指允许不同对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。

Swift 协议与泛型的实现

问题:

  • Protocol Type 和 Generic Type 如何实现存储?
  • Protocol Type 和 Generic Type 如何拷贝变量?
  • Protocol Type 和 Generic Type 如何进行方法派发?

协议类型 Protocol Type

例子,这里是通过Protocol Type实现多态,几个类之间没有继承关系

protocol Person {

    func doSometing()

}

struct Person1: Person {

    var x, y: Double

    func doSometing() {}

}

struct Person2: Person {

    var x1, y1, x2, y2: Double

    func doSometing() {}

}



//遵守了Person协议的类型集合,可能是Person1或者Person2

var persons: [Person] = [Person1(x: 1.0, y: 1.0), Person2(x1: 1.0, y1: 1.0, x2: 1.0, y2: 1.0)]

for p in persons {

    p.doSometing()

}
复制代码

init_existential_addr %x 初始化由%x 引用的内存为Existential Container。

可以发现在这种情况下,变量 persons 中存储的元素是一种特殊的数据类型:Existential Container

Existential Container

Existential Container 是编译器生成的一种特殊的数据类型,用于管理遵守了相同协议的协议类型。因为这些数据类型的内存空间尺寸不同,使用 Extential Container 进行管理可以实现存储一致性。

在这里我们可以使用Memlayout这个API去验证一下(本机内存对齐是 8 字节)

可见 Existential Container 类型占用了 5 个内存单元(也称 ,Word)。其结构如下图所示:

CleanShot 2022-03-25 at 16.50.52@2x.png

  • 三个词作为 Value Buffer
  • 一个词作为 Value Witness Table 的索引。
  • 一个词作为 Protocol Witness Table 的索引。

Value Buffer

Value Buffer 占据 3 个词,存储的可能是值,也可能是指针。对于 Small Value(存储空间小于等于 Value Buffer),可以直接内联存储在 Value Buffer 中。对于 Large Value(存储空间大于 Value Buffer),则会在堆区分配内存进行存储,Value Buffer 只存储对应的指针。

Value Witness Table

由于协议类型的具体类型不同,其内存布局也不同,Value Witness Table 则是对协议类型的生命周期进行专项管理,从而处理具体类型的初始化、拷贝、销毁。

Protocol Witness Table

Value Witness Table 管理协议类型的生命周期,Protocol Witness Table 则管理协议类型的方法调用。

在 OOP 中,基于继承关系的多态是通过 Virtual Table 实现的;在 POP 中,没有继承关系,因为无法使用 Virtual Table 实现基于协议的多态,取而代之的是 Protocol Witness Table。

拷贝变量

上面的描述中我们可以了解到

  • 对于 Small Value,直接内联存储在 Existential Container 的 Value Buffer 中。
  • 对于 Large Value,通过堆区分配进行存储,使用 Existential Containter 的 Value Buffer 进行索引。

Indirect Storage With Copy-On-Write

原理:优先使用内存指针,拷贝时仅仅拷贝 Existential Container,当修改值时,先检测引用计数,如果引用计数大于 1,则开辟新的堆区内存。

泛型类型 Generic Type

由Protocol Type实现的多态是动态的多态(Dynamic Polymorphism),那什么是静态多态呢

例子

protocol Person {

    func doSometing()

}

struct Person1: Person {

    var x, y: Double

    func doSometing() {}

}

struct Person2: Person {

    var x1, y1, x2, y2: Double

    func doSometing() {}

}



func todo(person: Person) {

    person.doSometing()

}



let person1 = Person1(x: 1.0, y: 1.0)



todo(person: person1)
复制代码

这个时候通过SIL文件我们可以看到这里还是使用Existential Container数据结构,但是上面我们说得使用Existential Container是为了管理遵守了相同协议的协议类型以及内存对齐,从上面的代码上看这个时候编译器是知道传进去的是Person1类型的对象,故这个时候我们可以使用泛型Generic code来实现。

func todo<T: Person>(person: T) {

    person.doSometing()

}
复制代码

故我们可以根据方法调用时数据类型是否确定可以将多态分为:静态多态(Static Polymorphism)和 动态多态(Dynamic Polymorphism)。

泛型和Protocol Type的区别在于:

  • 泛型支持的是静态多态。
  • 每个调用上下文只有一种类型。
  • 在调用链中会通过类型降级进行类型取代。

Value Buffer

Value Buffer 占据 3 个词,存储的可能是值,也可能是指针。对于 Small Value(存储空间小于等于 Value Buffer),可以直接内联存储在 Value Buffer 中。对于 Large Value(存储空间大于 Value Buffer),则会在堆区分配内存进行存储,Value Buffer 只存储对应的指针。

Value Witness Table

由于协议类型的具体类型不同,其内存布局也不同,Value Witness Table 则是对协议类型的生命周期进行专项管理,从而处理具体类型的初始化、拷贝、销毁。

Protocol Witness Table

Value Witness Table 管理协议类型的生命周期,Protocol Witness Table 则管理协议类型的方法调用。

在 OOP 中,基于继承关系的多态是通过 Virtual Table 实现的;在 POP 中,没有继承关系,因为无法使用 Virtual Table 实现基于协议的多态,取而代之的是 Protocol Witness Table。

拷贝变量

上面的描述中我们可以了解到

  • 对于 Small Value,直接内联存储在 Existential Container 的 Value Buffer 中。
  • 对于 Large Value,通过堆区分配进行存储,使用 Existential Containter 的 Value Buffer 进行索引。

Indirect Storage With Copy-On-Write

原理:优先使用内存指针,拷贝时仅仅拷贝 Existential Container,当修改值时,先检测引用计数,如果引用计数大于 1,则开辟新的堆区内存。

泛型类型 Generic Type

由Protocol Type实现的多态是动态的多态(Dynamic Polymorphism),那什么是静态多态呢

例子

protocol Person {

    func doSometing()

}

struct Person1: Person {

    var x, y: Double

    func doSometing() {}

}

struct Person2: Person {

    var x1, y1, x2, y2: Double

    func doSometing() {}

}



func todo(person: Person) {

    person.doSometing()

}



let person1 = Person1(x: 1.0, y: 1.0)



todo(person: person1)
复制代码

这个时候通过SIL文件我们可以看到这里还是使用Existential Container数据结构,但是上面我们说得使用Existential Container是为了管理遵守了相同协议的协议类型以及内存对齐,从上面的代码上看这个时候编译器是知道传进去的是Person1类型的对象,故这个时候我们可以使用泛型Generic code来实现。

image.png

func todo<T: Person>(person: T) {

    person.doSometing()

}
复制代码

故我们可以根据方法调用时数据类型是否确定可以将多态分为:静态多态(Static Polymorphism)和 动态多态(Dynamic Polymorphism)。

泛型和Protocol Type的区别在于:

  • 泛型支持的是静态多态。
  • 每个调用上下文只有一种类型。
  • 在调用链中会通过类型降级进行类型取代。

方法调用

在方法执行时,Swift将泛型T绑定为调用方使用的具体类型

todo(person: person1) -->todo<T = Person1>(person: person1)

泛型方法调用的具体实现为:

  • 同一种类型的任何实例,都共享同样的实现,即使用同一个Protocol Witness Table。

使用Protocol/Value Witness Table。

  • 每个调用上下文只有一种类型:这里没有使用Existential Container, 而是将Protocol/Value Witness Table作为调用方的额外参数进行传递。
  • 变量初始化和方法调用,都使用传入的VWTPWT来执行。

泛型特化

类型降级后,产生特定类型的方法,为泛型的每个类型创造对应的方法静态多态下进行特定优化 虚函数调用替换为调用函数映射。

例如

func todo<T: Person>(person: T) {

    person.doSometing()

}

todo(person: person1)

转换成

func todo<Person1>(person: Person1) {

    person.doSometing()

}
复制代码

因为是静态多态。所以可以进行很强大的优化,比如进行内联实现,并且通过获取上下文来进行更进一步的优化。从而降低方法数量。优化后可以更精确和具体。

参考资料

llvm.org/devmtg/2015…

github.com/apple/swift…

github.com/apple/swift…

www.jianshu.com/p/c2880460c…

www.rightpoint.com/rplabs/swit…

airspeedvelocity.net/2015/03/26/…

developer.apple.com/documentati…

我们是字节深圳飞书团队,致力于打造高性能、卓越体验的企业协同办公软件,感兴趣的童鞋可以投递简历到[email protected]。 iOS 职位:https://job.toutiao.com/s/Nj3oTKV Android 职位:https://job.toutiao.com/s/NjTyGkf

猜你喜欢

转载自juejin.im/post/7078979970436956167