【锁定直播】字节、华为云、阿里云等技术专家讨论如何将大模型接入 AIOps 解决实际问题,戳>>> 了解详情
写点什么

【基本功】深入剖析 Swift 性能优化

  • 2020-02-25
  • 本文字数:11138 字

    阅读完需:约 37 分钟

【基本功】深入剖析Swift性能优化

简介

2014 年,苹果公司在 WWDC 上发布 Swift 这一新的编程语言。经过几年的发展,Swift 已经成为 iOS 开发语言的“中流砥柱”,Swift 提供了非常灵活的高级别特性,例如协议、闭包、泛型等,并且 Swift 还进一步开发了强大的 SIL(Swift Intermediate Language)用于对编译器进行优化,使得 Swift 相比 Objective-C 运行更快性能更优,Swift 内部如何实现性能的优化,我们本文就进行一下解读,希望能对大家有所启发和帮助。


针对 Swift 性能提升这一问题,我们可以从概念上拆分为两个部分:


  1. 编译器:Swift 编译器进行的性能优化,从阶段分为编译期和运行期,内容分为时间优化和空间优化。

  2. 开发者:通过使用合适的数据结构和关键字,帮助编译器获取更多信息,进行优化。


下面我们将从这两个角度切入,对 Swift 性能优化进行分析。通过了解编译器对不同数据结构处理的内部实现,来选择最合适的算法机制,并利用编译器的优化特性,编写高性能的程序。

理解 Swift 的性能

理解 Swift 的性能,首先要清楚 Swift 的数据结构,组件关系和编译运行方式。


  • 数据结构


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


  • 组件关系


组件关系可以分为:inheritanceprotocolsgenerics


  • 方法分派方式


方法分派方式可以分为Static dispatchDynamic dispatch


要在开发中提高 Swift 性能,需要开发者去了解这几种数据结构和组件关系以及它们的内部实现,从而通过选择最合适的抽象机制来提升性能。


首先我们对于性能标准进行一个概念陈述,性能标准涵盖三个标准:



性能指标


  • Allocation

  • Reference counting

  • Method dispatch


接下来,我们会分别对这几个指标进行说明。

Allocation

内存分配可以分为堆区栈区,在栈的内存分配速度要高于堆,结构体和类在堆栈分配是不同的。


Stack


基本数据类型和结构体默认在栈区,栈区内存是连续的,通过出栈入栈进行分配和销毁,速度很快,高于堆区。


我们通过一些例子进行说明:


//示例 1// Allocation// Structstruct Point { var x, y:Double func draw() { … }}let point1 = Point(x:0, y:0) //进行point1初始化,开辟栈内存var point2 = point1 //初始化point2,拷贝point1内容,开辟新内存point2.x = 5 //对point2的操作不会影响point1// use `point1`// use `point2`
复制代码



结构体的内存分配


以上结构体的内存是在栈区分配的,内部的变量也是内联在栈区。将point1赋值给point2实际操作是在栈区进行了一份拷贝,产生了新的内存消耗point2,这使得point1point2是完全独立的两个实例,它们之间的操作互不影响。在使用point1point2之后,会进行销毁。


Heap


高级的数据结构,比如类,分配在堆区。初始化时查找没有使用的内存块,销毁时再从内存块中清除。因为堆区可能存在多线程的操作问题,为了保证线程安全,需要进行加锁操作,因此也是一种性能消耗。


// Allocation// Classclass Point { var x, y:Double func draw() { … }}let point1 = Point(x:0, y:0) //在堆区分配内存,栈区只是存储地址指针let point2 = point1 //不产生新的实例,而是对point2增加对堆区内存引用的指针point2.x = 5 //因为point1和point2是一个实例,所以point1的值也会被修改// use `point1`// use `point2`

复制代码



Class 实例内存分配


以上我们初始化了一个Class类型,在栈区分配一块内存,但是和结构体直接在栈内存储数值不同,我们只在栈区存储了对象的指针,指针指向的对象的内存是分配在堆区的。需要注意的是,为了管理对象内存,在堆区初始化时,除了分配属性内存(这里是 Double 类型的 x,y),还会有额外的两个字段,分别是typerefCount,这个包含了typerefCount和实际属性的结构被称为blue box


内存分配总结


从初始化角度,Class相比Struct需要在堆区分配内存,进行内存管理,使用了指针,有更强大的特性,但是性能较低。


优化方式


对于频繁操作(比如通信软件的内容气泡展示),尽量使用Struct替代Class,因为栈内存分配更快,更安全,操作更快。

Reference counting

Swift 通过引用计数管理堆对象内存,当引用计数为 0 时,Swift 确认没有对象再引用该内存,所以将内存释放。对于引用计数的管理是一个非常高频的间接操作,并且需要考虑线程安全,使得引用计数的操作需要较高的性能消耗。


对于基本数据类型的Struct来说,没有堆内存分配和引用计数的管理,性能更高更安全,但是对于复杂的结构体,如:


// Reference Counting// Struct containing referencesstruct Label { var text:String var font:UIFont func draw() { … }}let label1 = Label(text:"Hi", font:font)  //栈区包含了存储在堆区的指针let label2 = label1 //label2产生新的指针,和label1一样指向同样的string和font地址// use `label1`// use `label2`

复制代码



结构体包含引用类型


这里看到,包含了引用的结构体相比Class,需要管理双倍的引用计数。每次将结构体作为参数传递给方法或者进行直接拷贝时,都会出现多份引用计数。下图可以比较直观的理解:



备注:包含引用类型的结构体出现 Copy 的处理方式


Class 在拷贝时的处理方式:



引用计数总结


  • Class在堆区分配内存,需要使用引用计数器进行内存管理。

  • 基本类型的Struct在栈区分配内存,无引用计数管理。

  • 包含强类型的Struct通过指针管理在堆区的属性,对结构体的拷贝会创建新的栈内存,创建多份引用的指针,Class只会有一份。


优化方式


在使用结构体时:


  1. 通过使用精确类型,例如 UUID 替代 String(UUID 字节长度固定 128 字节,而不是 String 任意长度),这样就可以进行内存内联,在栈内存储 UUID,我们知道,栈内存管理更快更安全,并且不需要引用计数。

  2. Enum 替代 String,在栈内管理内存,无引用计数,并且从语法上对于开发者更友好。

Method Dispatch

我们之前在Static dispatch VS Dynamic dispatch中提到过,能够在编译期确定执行方法的方式叫做静态分派 Static dispatch,无法在编译期确定,只能在运行时去确定执行方法的分派方式叫做动态分派 Dynamic dispatch。


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


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


下面看一段多态代码,以及分析实现方式:


//引用语义实现的多态class Drawable { func draw() {} }class Point :Drawable { var x, y:Double override func draw() { … }}class Line :Drawable { var x1, y1, x2, y2:Double override func draw() { … }}var drawables:[Drawable]for d in drawables { d.draw()}
复制代码



引用语义多态的方法分派流程


Method Dispatch 总结


Class默认使用Dynamic dispatch,因为在编译期几乎每个环节的信息都无法确定,所以阻碍了编译器的优化,比如inlinewhole module inline


使用 Static dispatch 代替 Dynamic dispatch 提升性能


我们知道Static dispatch快于Dynamic dispatch,如何在开发中去尽可能使用Static dispatch


  • inheritance constraints继承约束我们可以使用final关键字去修饰Class,以此生成的Final class,使用Static dispatch

  • access control访问控制 private关键字修饰,使得方法或属性只对当前类可见。编译器会对方法进行Static dispatch


编译器可以通过whole module optimization检查继承关系,对某些没有标记final的类通过计算,如果能在编译期确定执行的方法,则使用Static dispatch


Struct默认使用Static dispatch


Swift 快于 OC 的一个关键是可以消解动态分派。


总结


Swift 提供了更灵活的Struct,用以在内存、引用计数、方法分派等角度去进行性能的优化,在正确的时机选择正确的数据结构,可以使我们的代码性能更快更安全。


延伸


你可能会问Struct如何实现多态呢?答案是protocol oriented programming


以上分析了影响性能的几个标准,那么不同的算法机制ClassProtocol TypesGeneric code,它们在这三方面的表现如何,Protocol TypeGeneric code分别是怎么实现的呢?我们带着这个问题看下去。

Protocol Type

这里我们会讨论 Protocol Type 如何存储和拷贝变量,以及方法分派是如何实现的。不通过继承或者引用语义的多态:


protocol Drawable { func draw() }struct Point :Drawable { var x, y:Double func draw() { … }}struct Line :Drawable { var x1, y1, x2, y2:Double func draw() { … }}
var drawables:[Drawable] //遵守了Drawable协议的类型集合,可能是point或者linefor d in drawables { d.draw()}
复制代码


以上通过Protocol Type实现多态,几个类之间没有继承关系,故不能按照惯例借助V-Table实现动态分派。


如果想了解Vtable和Witness table实现,可以进行点击查看,这里不做细节说明。


因为 Point 和 Line 的尺寸不同,数组存储数据实现一致性存储,使用了Existential Container。查找正确的执行方法则使用了 Protoloc Witness Table


Existential Container

Existential Container是一种特殊的内存布局方式,用于管理遵守了相同协议的数据类型Protocol Type,这些数据类型因为不共享同一继承关系(这是V-Table实现的前提),并且内存空间尺寸不同,使用Existential Container进行管理,使其具有存储的一致性。



Existential Container 的构成


结构如下


  • 三个词大小的 valueBuffer 这里介绍一下 valueBuffer 结构,valueBuffer 有三个词,每个词包含 8 个字节,存储的可能是值,也可能是对象的指针。对于 small value(空间小于 valueBuffer),直接存储在 valueBuffer 的地址内, inline valueBuffer,无额外堆内存初始化。当值的数量大于 3 个属性即 large value,或者总尺寸超过 valueBuffer 的占位,就会在堆区开辟内存,将其存储在堆区,valueBuffer 存储内存指针。

  • value witness table 的引用 因为Protocol Type的类型不同,内存空间,初始化方法等都不相同,为了对Protocol Type生命周期进行专项管理,用到了Value Witness Table

  • protocol witness table 的引用 管理Protocol Type的方法分派。


内存分布如下


1. payload_data_0 = 0x0000000000000004,2. payload_data_1 = 0x0000000000000000,3. payload_data_2 = 0x0000000000000000,4. instance_type = 0x000000010d6dc408 ExistentialContainers`type           metadata for ExistentialContainers.Car,5. protocol_witness_0 = 0x000000010d6dc1c0        ExistentialContainers protocol witness table for        ExistentialContainers.Car:ExistentialContainers.Drivable        in ExistentialContainers
复制代码

Protocol Witness Table(PWT)

为了实现Class多态也就是引用语义多态,需要V-Table来实现,但是V-Table的前提是具有同一个父类即共享相同的继承关系,但是对于Protocol Type来说,并不具备此特征,故为了支持Struct的多态,需要用到protocol oriented programming机制,也就是借助Protocol Witness Table来实现(细节可以点击Vtable和witness table实现,每个结构体会创造PWT表,内部包含指针,指向方法具体实现)。



Point and Line PWT

Value Witness Table(VWT)

用于管理任意值的初始化、拷贝、销毁。



VWT use existential container


  • Value Witness Table的结构如上,是用于管理遵守了协议的Protocol Type实例的初始化,拷贝,内存消减和销毁的。

  • Value Witness TableSIL中还可以拆分为%relative_vwtable%absolute_vwtable,我们这里先不做展开。

  • Value Witness TableProtocol Witness Table通过分工,去管理Protocol Type实例的内存管理(初始化,拷贝,销毁)和方法调用。


我们来借助具体的示例进行进一步了解:


// Protocol Types// The Existential Container in actionfunc drawACopy(local :Drawable) { local.draw()}let val :Drawable = Point()drawACopy(val)
复制代码


在 Swift 编译器中,通过Existential Container实现的伪代码如下:


// Protocol Types// The Existential Container in actionfunc drawACopy(local :Drawable) { local.draw()}let val :Drawable = Point()drawACopy(val)
//existential container的伪代码结构struct ExistContDrawable { var valueBuffer:(Int, Int, Int) var vwt:ValueWitnessTable var pwt:DrawableProtocolWitnessTable}
// drawACopy方法生成的伪代码func drawACopy(val:ExistContDrawable) { //将existential container传入 var local = ExistContDrawable() //初始化container let vwt = val.vwt //获取value witness table,用于管理生命周期 let pwt = val.pwt //获取protocol witness table,用于进行方法分派 local.type = type local.pwt = pwt vwt.allocateBufferAndCopyValue(&local, val) //vwt进行生命周期管理,初始化或者拷贝 pwt.draw(vwt.projectBuffer(&local)) //pwt查找方法,这里说一下projectBuffer,因为不同类型在内存中是不同的(small value内联在栈内,large value初始化在堆内,栈持有指针),所以方法的确定也是和类型相关的,我们知道,查找方法时是通过当前对象的地址,通过一定的位移去查找方法地址。 vwt.destructAndDeallocateBuffer(temp) //vwt进行生命周期管理,销毁内存}
复制代码

Protocol Type 存储属性

我们知道,Swift 中Class的实例和属性都存储在堆区,Struct实例在栈区,如果包含指针属性则存储在堆区,Protocol Type如何存储属性?Small Number 通过Existential Container内联实现,大数存在堆区。如何处理 Copy 呢?

Protocol 大数的 Copy 优化

在出现 Copy 情况时:


let aLine = Line(1.0, 1.0, 1.0, 3.0)let pair = Pair(aLine, aLine)let copy = pair
复制代码



Protocol Type Copy Large Number


会将新的Exsitential Container的 valueBuffer 指向同一个 value 即创建指针引用,但是如果要改变值怎么办?我们知道Struct值的修改和Class不同,Copy 是不应该影响原实例的值的。


这里用到了一个技术叫做Indirect Storage With Copy-On-Write,即优先使用内存指针。通过提高内存指针的使用,来降低堆区内存的初始化。降低内存消耗。在需要修改值的时候,会先检测引用计数检测,如果有大于 1 的引用计数,则开辟新内存,创建新的实例。在对内容进行变更的时候,会开启一块新的内存,伪代码如下:


class LineStorage { var x1, y1, x2, y2:Double }struct Line :Drawable { var storage :LineStorage init() { storage = LineStorage(Point(), Point()) } func draw() { … } mutating func move() {   if !isUniquelyReferencedNonObjc(&storage) { //如何存在多份引用,则开启新内存,否则直接修改     storage = LineStorage(storage)   }   storage。start = ...   }}
复制代码


这样实现的目的:通过多份指针去引用同一份地址的成本远远低于开辟多份堆内存。以下对比图:



堆拷贝



Indirect Storage

Protocol Type 多态总结

  1. 支持Protocol Type的动态多态(Dynamic Polymorphism)行为。

  2. 通过使用Witness TableExistential Container来实现。

  3. 对于大数的拷贝可以通过Indirect Storage间接存储来进行优化。


说到动态多态Dynamic Polymorphism,我们就要问了,什么是静态多态Static Polymorphism,看看下面示例:


// Drawing a copyprotocol Drawable { func draw()}func drawACopy(local :Drawable) { local.draw()}
let line = Line()drawACopy(line)// ...let point = Point()drawACopy(point)
复制代码


这种情况我们就可以用到泛型Generic code来实现,进行进一步优化。

泛型

我们接下来会讨论泛型属性的存储方式和泛型方法是如何分派的。泛型和Protocol Type的区别在于:


  • 泛型支持的是静态多态。

  • 每个调用上下文只有一种类型。

  • 查看下面的示例,foobar方法是同一种类型。

  • 在调用链中会通过类型降级进行类型取代。


对于以下示例:


func foo<T:Drawable>(local :T) { bar(local)}func bar<T:Drawable>(local:T) { … }let point = Point()foo(point)
复制代码


分析方法foobar的调用过程:


//调用过程foo(point)-->foo<T = Point>(point)   //在方法执行时,Swift将泛型T绑定为调用方使用的具体类型,这里为Point bar(local) -->bar<T = Point>(local) //在调用内部bar方法时,会使用foo已经绑定的变量类型Point,可以看到,泛型T在这里已经被降级,通过类型Point进行取代
复制代码


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


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

  • 使用 Protocol/Value Witness Table。

  • 每个调用上下文只有一种类型:这里没有使用Existential Container, 而是将Protocol/Value Witness Table作为调用方的额外参数进行传递。

  • 变量初始化和方法调用,都使用传入的VWTPWT来执行。


看到这里,我们并不觉得泛型比Protocol Type有什么更快的特性,泛型如何更快呢?静态多态前提下可以进行进一步的优化,称为特定泛型优化。

泛型特化

  • 静态多态:在调用栈中只有一种类型。Swift 使用只有一种类型的特点,来进行类型降级取代。

  • 类型降级后,产生特定类型的方法。

  • 为泛型的每个类型创造对应的方法。这时候你可能会问,那每一种类型都产生一个新的方法,代码空间岂不爆炸?

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


例如:


func min<T:Comparable>(x:T, y:T) -> T {  return y < x ? y : x}
复制代码


从普通的泛型展开如下,因为要支持所有类型的min方法,所以需要对泛型类型进行计算,包括初始化地址、内存分配、生命周期管理等。除了对 value 的操作,还要对方法进行操作。这是一个非常复杂庞大的工程。


func min<T:Comparable>(x:T, y:T, FTable:FunctionTable) -> T {  let xCopy = FTable.copy(x)  let yCopy = FTable.copy(y)  let m = FTable.lessThan(yCopy, xCopy) ? y :x  FTable.release(x)  FTable.release(y)  return m}
复制代码


在确定入参类型时,比如 Int,编译器可以通过泛型特化,进行类型取代(Type Substitute),优化为:


func min<Int>(x:Int, y:Int) -> Int {  return y < x ? y :x}
复制代码


泛型特化specilization是何时发生的?


在使用特定优化时,调用方需要进行类型推断,这里需要知晓类型的上下文,例如类型的定义和内部方法实现。如果调用方和类型是单独编译的,就无法在调用方推断类型的内部实行,就无法使用特定优化,保证这些代码一起进行编译,这里就用到了whole module optimization。而whole module optimization是对于调用方和被调用方的方法在不同文件时,对其进行泛型特化优化的前提。

泛型进一步优化

特定泛型的进一步优化:


// Pairs in our program using generic typesstruct Pair<T :Drawable> { init(_ f:T, _ s:T) { first = f ; second = s } var first:T var second:T}let pairOfLines = Pair(Line(), Line())// ...
let pairOfPoint = Pair(Point(), Point())
复制代码


在用到多种泛型,且确定泛型类型不会在运行时修改时,就可以对成对泛型的使用进行进一步优化。


优化的方式是将泛型的内存分配由指针指定,变为内存内联,不再有额外的堆初始化消耗。请注意,因为进行了存储内联,已经确定了泛型特定类型的内存分布,泛型的内存内联不能存储不同类型。所以再次强调此种优化只适用于在运行时不会修改泛型类型,即不能同时支持一个方法中包含linepoint两种类型。

whole module optimization

whole module optimization是用于 Swift 编译器的优化机制。可以通过-whole-module-optimization (或 -wmo)进行打开。在 XCode 8 之后默认打开。 Swift Package Manager在 release 模式默认使用whole module optimization。module 是多个文件集合。



没有进行全模块优化


编译器在对源文件进行语法分析之后,会对其进行优化,生成机器码并输出目标文件,之后链接器联合所有的目标文件生成共享库或可执行文件。


whole module optimization通过跨函数优化,可以进行内联等优化操作,对于泛型,可以通过获取类型的具体实现来进行推断优化,进行类型降级方法内联,删除多余方法等操作。



whole module optimizaiton


全模块优化的优势


  • 编译器掌握所有方法的实现,可以进行内联泛型特化等优化,通过计算所有方法的引用,移除多余的引用计数操作。

  • 通过知晓所有的非公共方法,如果这写方法没有被使用,就可以对其进行消除。


如何降低编译时间


和全模块优化相反的是文件优化,即对单个文件进行编译。这样的好处在于可以并行执行,并且对于没有修改的文件不会再次编译。缺点在于编译器无法获知全貌,无法进行深度优化。下面我们分析下全模块优化如何避免没修改的文件再次编译。



避免 recompile


编译器内部运行过程分为:语法分析,类型检查,SIL优化,LLVM后端处理。


语法分析和类型检查一般很快,SIL优化执行了重要的 Swift 特定优化,例如泛型特化和方法内联等,该过程大概占用整个编译时间的三分之一。LLVM后端执行占用了大部分的编译时间,用于运行降级优化和生成代码。


进行全模块优化后,SIL优化会将模块再次拆分为多个部分,LLVM后端通过多线程对这些拆分模块进行处理,对于没有修改的部分,不会进行再处理。这样就避免了修改一小部分,整个大模块进行LLVM后端的再次执行,除此外,使用多线程并行操作也会缩短处理时间。

扩展:Swift 的隐藏“Bug”

Swift 因为方法分派机制问题,所以在设计和优化后,会产生和我们常规理解不太一致的结果,这当然不能算 Bug。但是还是要单独进行说明,避免在开发过程中,因为对机制的掌握不足,造成预期和执行出入导致的问题。


Message dispatch


我们通过上面说明结合Static dispatch VS Dynamic dispatch对方法分派方式有了了解。这里需要对Objective-C的方法分派方式进行说明。


熟悉 OC 的人都知道,OC 采用了运行时机制使用obj_msgSend发送消息,runtime 非常的灵活,我们不仅可以对方法调用采用swizzling,对于对象也可以通过isa-swizzling来扩展功能,应用场景有我们常用的 hook 和大家熟知的KVO


大家在使用 Swift 进行开发时都会问,Swift 是否可以使用 OC 的运行时和消息转发机制呢?答案是可以。


Swift 可以通过关键字dynamic对方法进行标记,这样就会告诉编译器,此方法使用的是 OC 的运行时机制。


注意:我们常见的关键字@ObjC并不会改变 Swift 原有的方法分派机制,关键字@ObjC的作用只是告诉编译器,该段代码对于 OC 可见。


总结来说,Swift 通过dynamic关键字的扩展后,一共包含三种方法分派方式:Static dispatchTable dispatchMessage dispatch。下表为不同的数据结构在不同情况下采取的分派方式:



Swift Dispatch Method


如果在开发过程中,错误的混合了这几种分派方式,就可能出现 Bug,以下我们对这些 Bug 进行分析:


SR-584


此情况是在子类的 extension 中重载父类方法时,出现和预期不同的行为。


class Base:NSObject {    var directProperty:String { return "This is Base" }    var indirectProperty:String { return directProperty }}
class Sub:Base { }
extension Sub { override var directProperty:String { return "This is Sub" }}
复制代码


执行以下代码,直接调用没有问题:


Base().directProperty // “This is Base”Sub().directProperty // “This is Sub”
复制代码


间接调用结果和预期不同:


Base()。indirectProperty // “This is Base”Sub()。indirectProperty // expected "this is Sub",but is “This is Base” <- Unexpected!
复制代码


Base.directProperty前添加dynamic关键字就可以获得”this is Sub”的结果。Swift 在extension 文档中说明,不能在 extension 中重载已经存在的方法。


“Extensions can add new functionality to a type, but they cannot override existing functionality.”


会出现警告:Cannot override a non-dynamic class declaration from an extension



Extension Override Warning


出现这个问题的原因是,NSObject 的 extension 是使用的Message dispatch,而Initial Declaration使用的是Table dispath(查看上图 Swift Dispatch Method)。extension 重载的方法添加在了Message dispatch内,没有修改虚函数表,虚函数表内还是父类的方法,故会执行父类方法。想在 extension 重载方法,需要标明dynamic来使用Message dispatch


SR-103


协议的扩展内实现的方法,无法被遵守类的子类重载:


protocol Greetable {    func sayHi()}extension Greetable {    func sayHi() {        print("Hello")    }}func greetings(greeter:Greetable) {    greeter.sayHi()}
复制代码


现在定义一个遵守了协议的类Person。遵守协议类的子类LoudPerson


class Person:Greetable {}class LoudPerson:Person {    func sayHi() {        print("sub")    }}
复制代码


执行下面代码结果为:


var sub:LoudPerson = LoudPerson()sub.sayHi()  //sub
复制代码


不符合预期的代码:


var sub:Person = LoudPerson()sub.sayHi()  //HellO  <-使用了protocol的默认实现
复制代码


注意,在子类LoudPerson中没有出现override关键字。可以理解为LoudPerson并没有成功注册GreetableWitness table的方法。所以对于声明为Person实际为LoudPerson的实例,会在编译器通过Person去查找,Person没有实现协议方法,则不产生Witness tablesayHi方法是直接调用的。解决办法是在 base 类内实现协议方法,无需实现也要提供默认方法。或者将基类标记为final来避免继承。


进一步通过示例去理解:


// Defined protocol。protocol A {    func a() -> Int}extension A {    func a() -> Int {        return 0    }}
// A class doesn't have implement of the function。class B:A {}
class C:B { func a() -> Int { return 1 }}
// A class has implement of the function。class D:A { func a() -> Int { return 1 }}
class E:D { override func a() -> Int { return 2 }}
// Failure cases。B().a() // 0C().a() // 1(C() as A).a() // 0 # We thought return 1。
// Success cases。D().a() // 1(D() as A).a() // 1E().a() // 2(E() as A).a() // 2
复制代码


其他


我们知道 Class extension 使用的是 Static Dispatch:


class MyClass {}extension MyClass {    func extensionMethod() {}} class SubClass:MyClass {    override func extensionMethod() {}}
复制代码


以上代码会出现错误,提示Declarations in extensions can not be overridden yet

总结

  • 影响程序的性能标准有三种:初始化方式引用指针方法分派

  • 文中对比了两种数据结构:StructClass的在不同标准下的性能表现。Swift 相比 OC 和其它语言强化了结构体的能力,所以在了解以上性能表现的前提下,通过利用结构体可以有效提升性能。

  • 在此基础上,我们还介绍了功能强大的结构体的类:Protocol TypeGeneric。并且介绍了它们如何支持多态以及通过使用有条件限制的泛型如何让程序更快。

参考资料

作者简介

  • 亚男,美团点评 iOS 工程师。2017 年加入美团点评,负责专业版餐饮管家开发,研究编译器原理。目前正积极推动 Swift 组件化建设。


2020-02-25 20:32660

评论

发布
暂无评论
发现更多内容

Week12总结

lggl

从代码到部署微服务实战(一)

万俊峰Kevin

微服务 microservice go-zero Go 语言

关于坚持的思考

.

28天写作

软件架构设计实战

Andy

OpenLookeng连接器-Clickhouse connector性能测试报告

Galaxy数据平台

数据库 数据分析 Clickhouse

他们说飞机很安全,你信吗?

Justin

心理学 概率 28天写作

Week12作业

lggl

案例研究之聊聊 QLExpress 源码 (二)

小诚信驿站

源码分析 小诚信驿站 28天写作 QLExpress源码 聊聊源码

解密Android开发常见误区!耗时两个礼拜,8000字安卓面试长文,详细的Android学习指南

欢喜学安卓

android 程序员 面试 移动开发

解析底层原理!2021年移动开发者未来的出路在哪里,论程序员成长的正确姿势

欢喜学安卓

android 程序员 面试 移动开发

第十二周课后练习

晴空万里

架构师训练营第2期

软件设计原则

架构师训练营 4 期

联想小新潮7000安装CentOS8.8步骤

笑春风

架构入门感悟之十二

笑春风

架构师训练营第 1 期 大作业二

李循律

架构师训练营第 1 期 大作业一

李循律

一款基本靠谱,略微出圈的2021十大科技预测

脑极体

架构师训练营第二周作业

架构师训练营 4 期

学非探其花 要自拔其根

看山

闲聊

架构师训练营第 1 期 - 第 13 周 - 学习总结

wgl

「架构师训练营第 1 期」

极客时间 架构大作业,快递平台架构搭建

博古通今小虾米

极客时间 架构

供应链产品溯源介绍

无誉

区块链 产品 电商 供应链 盘点2020

软件架构知识树

Andy

架构师设计大作业二

小诗

「架构师训练营第 1 期」

上链智能合约系统开发|上链智能合约APP软件开发

系统开发

大作业二

golangboy

架构师训练营第 1 期

赫拉利其人其书之我见(1)

石君

科技 28天写作 简史 社科

十二周作业

水浴清风

架构师训练营第 1 期 - 第 13 周 - 命题作业

wgl

「架构师训练营第 1 期」

十二周总结

水浴清风

第十二周课后练习

落朽

【基本功】深入剖析Swift性能优化_文化 & 方法_美团技术团队_InfoQ精选文章