【基本功】深入剖析Swift效能優化

美團技術團隊發表於2018-11-02

簡介

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
// Struct
struct 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
// Class
class 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 references
struct 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,需要管理雙倍的引用計數。每次將結構體作為引數傳遞給方法或者進行直接拷貝時,都會出現多份引用計數。下圖可以比較直觀的理解:

struct containing many references

備註:包含引用型別的結構體出現Copy的處理方式

Class在拷貝時的處理方式:

use a wrapper 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 dispatchStruct預設使用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或者line
for d in drawables {
 d.draw()
}
複製程式碼

以上通過Protocol Type實現多型,幾個類之間沒有繼承關係,故不能按照慣例藉助V-Table實現動態分派。

如果想了解Vtable和Witness table實現,可以進行點選檢視,這裡不做細節說明。 因為Point和Line的尺寸不同,陣列儲存資料實現一致性儲存,使用了Existential Container。查詢正確的執行方法則使用了 Protoloc Witness Table

look up pwt?

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 action
func drawACopy(local :Drawable) {
 local.draw()
}
let val :Drawable = Point()
drawACopy(val)
複製程式碼

在Swift編譯器中,通過Existential Container實現的虛擬碼如下:

// Protocol Types
// The Existential Container in action
func 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 copy
protocol 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 types
struct 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](img/Swift_Compile_Performance/Swift dispatch method.png)

如果在開發過程中,錯誤的混合了這幾種分派方式,就可能出現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() // 0
C().a() // 1
(C() as A).a() // 0 # We thought return 1。 

// Success cases。
D().a() // 1
(D() as A).a() // 1
E().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元件化建設。

招聘資訊

我們餐飲生態技術部是一個技術氛圍活躍,大牛聚集的地方。新到店緊握真正的大規模SaaS實戰機會,多租戶、資料、安全、開放平臺等全方位的挑戰。業務領域複雜技術挑戰多,技術和業務能力迅速提升,最重要的是,加入我們,你將實現真正通過程式碼來改變行業的夢想。我們歡迎各端人才加入,Java優先。感興趣的同學趕緊傳送簡歷至 zhaoyanan02@meituan.com,我們期待你的到來。

【基本功】深入剖析Swift效能優化

相關文章