淺談 Swift 泛型元編程——建構一個在編譯時就確保了安全的 VFL 助手庫

前言

什麼是在你選擇一門編程語言的時候最能左右你決定的事?前端

有人會說,要寫的越少,語言越好。(並非,PHP 是最好的語言。)git

好吧,這也許是真的。可是要寫的少並非一個能夠在什麼時候何地都獲得同一結果的能夠量化的指標。根據你任務的不一樣,代碼的行數也在上下浮動。程序員

我認爲最好的方法是考察編程語言有多少 primitives(元語)。github

對於一些老式的編程語言而言,他們有的沒有多維數組。這意味着數組並不能包含他們本身。這束縛了一些開發者來發明某些具備遞歸性質的數據結構,同時也限制了語言的表達性。語言的表達性,形式化地講,就是語言的計算能力面試

可是我剛剛提到的這個數組的例子僅僅只和運行時計算能力有關。編譯時計算能力又是怎樣呢?編程

好的。像 C++ 這樣具有顯示編譯過程以及一些「代碼模板」設施的語言是具備進行某些編譯時計算的能力。他們一般是收集源代碼的碎片,而後將他們組織成一段新的代碼。你也許已經聽過一個大詞了:「元編程」。是的,這就是元編程(可是是在編譯時)。而這些語言也包含了 C 和 Swift。swift

C++ 元編程依賴於模板。在 C 中,元編程依賴於一個來自 libobjcext 的特殊頭文件 metamacros.h。在 Swift 中,元編程依賴於泛型。數組

儘管你能夠在這三種語言中作編譯時元編程,其能力又是不一樣的。由於已經有不少文章談論 C++ 模板爲何是圖靈完備(一種計算能力的度量,你能夠簡單認爲它就是「啥都能算」)的了,我不想在這上面浪費個人實踐。我要討論的是 Swift 中的泛型元編程,以及要給 C 中的 metamacros.h 做一個簡單的介紹。這兩種語言的編譯時元編程能力都比 C++ 要弱。他們僅僅只可以實現一個 DFA(肯定性自動機,另外一種計算能力的度量。你能夠簡單的認爲它就是「能計算有限的模式」)上限的編譯時計算設施。安全


案例研究: 在編譯時就確保了安全的 VFL

咱們有許多 Auto Layout 助手庫:Cartography, Masonry, SnapKit... 可是,他們真的好嗎?要是有一個 Swift 版本的 VFL 能在編譯時就確保正確性並且可以和 Xcode 的代碼補全聯動如何?數據結構

老實說,我是一個 VFL 愛好者。你能夠用一行代碼就對不少視圖進行佈局。要是是 Cartography 或者 SnapKit,早就「王婆婆的裹腳又長又臭」了。

因爲原版的 VFL 對於現代 iOS 設計的支持上有一點問題,這主要表如今不能和 layout guide 合做上,你也許也想要咱們立刻要實現的這套 API 可以支持 layout guide。

最後,在個人生產代碼中,我構建了以下的能夠在編譯時就確保了安全的而且支持 layout guide 的 API。

// 建立佈局約束而且裝置入視圖

constrain {
    withVFL(H: view1 - view2)
    
    withVFL(H: view.safeAreaLayoutGuide - view2)
    
    withVFL(H: |-view2)
}

// 僅僅建立佈局約束

let constraints1 = withVFL(V: view1 - view2)

let constraints2 = withVFL(V: view3 - view4, options: .alignAllCenterY)
複製代碼

想象一下在 Cartography 或者 SnapKit 中構建等效的事情須要多少行代碼?想知道我怎麼構建出來的了嗎?

讓我來告訴你。

語法變形

若是咱們將原版的 VFL 語法導入到 Swift 源代碼中而且去除掉字符串字面量的引號,你很快就會發現一些在原版 VFL 中所使用的字符像 [, ], @, () 是不能在 Swift 中用進行操做符重載的。因而我對原版 VFL 語法作了一些變形:

// 原版 VFL: @"|-[view1]-[view2]"
withVFL(H: |-view1 - view2)

// 原版 VFL: @"[view1(200@20)]"
withVFL(H: view1.where(200 ~ 20))

// 原版 VFL: @"V:[view1][view2]"
withVFL(V: view1 | view2)

// 原版 VFL: @"V:|[view1]-[view2]|"
withVFL(V: |view1 - view2|)

// 原版 VFL: @"V:|[view1]-(>=4@200)-[view2]|"
withVFL(V: |view1 - (>=4 ~ 200) - view2|)
複製代碼

探索實現

如何達成咱們的設計?

一個來自直覺的答案就是使用操做符重載。

是的。我已經在個人生產代碼中用操做符重載達成了咱們的設計。可是操做符重載在這裏是如何工做的?我是說,爲何操做符重載能夠承載咱們的設計?

在回答這個問題以前,讓咱們看一些例子。

withVFL(H: |-view1 - view2 - 4)
複製代碼

上例是一個是一個不該該被編譯器接受的非法輸入。相應的原版 VFL 以下:

@"|-[view1]-[view2]-4"
複製代碼

咱們能夠發如今 4 以後缺乏了一個視圖,或者一個 -|

咱們但願咱們的系統能夠經過讓編譯器接受一段輸入來把控正確的輸入,經過讓編譯器拒絕一段輸入來把控錯誤的輸入(由於這就是編譯時就確保了安全的所隱含的意思)。這背後的祕密並非由一個擡頭是「高級軟件開發工程師」的神祕工程師施放的黑魔法,而是簡單的經過匹配用戶輸入與已經定義好了的函數來接受用戶輸入,經過失配用戶輸入和已經定義好了的函數來拒絕用戶輸入。

好比,就像上例中 view1 - view2 拿部分所示,咱們能夠設計以下函數來把控他。

func - (lhs: UIView, rhs: UIView) -> BinarySyntax {
    // Do something really combine these two views together.
}
複製代碼

若是咱們將上述代碼塊中的 UIViewBinarySyntax 看做兩個狀態,那麼咱們就能夠在咱們的系統中引入狀態轉移了,而狀態轉移的方法就是操做符重載。

樸素的狀態轉移

知道了經過操做符重載引入狀態轉移也許能解決咱們的問題,咱們能夠呼一口氣了。

可是……這個解決方案下咱們要建立多少種類型?

你也許不知道的是,VFL 能夠被表達爲一個 DFA。

是的。由於如[, ], () 這樣的遞歸文本在 VFL 中並非真正的遞歸文本(在正確的 VFL 中他們只能出現一層而且沒法嵌套),一個 DFA 就能夠表述出 VFL 的全部可能的輸入集合。

因而我繪製了一個 DFA 來模擬咱們設計中的狀態轉移。要當心。在這張圖中我沒有把 layout guide 放進去。加入 layout guide 只會讓這個 DFA 變得更復雜。

瞭解更多的關於遞歸和 DFA 的樸實的簡介你能夠看看這本書計算的本質:深刻剖析程序和計算機

自動機
自動機

上圖中, |pre 表示一個前綴 | 操做符,一樣的,|post 表示一個後綴 | 操做符。兩個圓圈表示接受,單個圓圈表示接收。

數咱們要建立的類型的數目是一個複雜的任務。因爲有雙目操做符 |-,還有單目操做符 |-, -|, |prefix|postfix,計數方法在這兩種操做符中是不一樣的。

一個雙目操做符消耗兩次狀態轉移,而一個單目操做符消耗一次。每個操做符都將建立一個新的類型。

由於這個計數方法自己實在太複雜了,我寧願想一想別的方法……

多狀態的狀態轉移

我是經過死命測試可能的輸入字符以測試一個狀態是否接受他們來畫出上面這個 DFA 圖的。這將全部的一切都映射到了一個一個維度上。也許咱們能夠經過在多個維度對問題進行抽象來創造一種更加清澈的表達。

在開始深刻探索前,咱們不得不獲取一些關於 Swift 操做符結合性的一些基礎知識。

結合性是一個操做符(嚴格來說,雙目操做符。就是像 - 那樣連結左手邊算子和右手邊算子的操做符)在編譯時期,肯定編譯器選擇在哪邊構建語法樹的一個性質。Swift 默認的操做符結合性是向左。這意味着編譯器更加傾向於在一個操做符的左手邊構建語法樹。因而咱們能夠知道,對於一個由向左結合的操做符生成的語法樹,其在視覺上是向左傾斜的。

首先讓咱們來看看幾個最簡單的表達式:

// 應該接受
withVFL(H: view1 - view2)

// 應該接受
withVFL(H: view1 | view2)

// 應該接受
withVFL(H: |view1|)

// 應該接受
withVFL(H: |-view1-|)
複製代碼

他們的語法樹以下:

簡單表達式
簡單表達式的語法樹

而後咱們能夠將狀況分爲兩類:

  • view1 - view2, view1 | view2 這樣的雙目表達式。

  • |view1, view1-| 這樣的單目表達式。

這使咱們直覺地建立了兩種類型:

struct Binary<Lhs, Rhs> { ... }

func - <Lhs, Rhs>(lhs: Lhs, rhs: Rhs) -> Binary { ... }

func | <Lhs, Rhs>(lhs: Lhs, rhs: Rhs) -> Binary { ... }

struct Unary<Operand> { ... }

prefix func | <Operand>(operand: Operand) -> Unary { ... }

postfix func | <Operand>(operand: Operand) -> Unary { ... }

prefix func |- <Operand>(operand: Operand) -> Unary { ... }

postfix func -| <Operand>(operand: Operand) -> Unary { ... }
複製代碼

可是這夠了嗎?

Syntax Attribute

你立刻會發現,咱們能夠將任何東西代入 BinaryLhs 或者 Rhs,或者 UnaryOperand 中。咱們須要作一些限制。

典型地說,像 |-, -|, |prefix, |postfix 這種輸入只應該出如今表達式首尾兩端。由於咱們也但願支持 layout guide(如 safeAreaLayoutGuide),而 layout guide 也只應該出如今表達式首尾兩端,咱們還須要對這些東西作一些限制來確保他們僅僅出如今表達式的兩端。

|-view-|
|view|
複製代碼

另外,像 4, >=40 這種輸入只應該和前驅和後繼視圖/父視圖或者 layout guide 配合出現。

view - 4 - safeAreaLayoutGuide

view1 - (>=40) - view2
複製代碼

以上對於表達式的研究提示咱們要將全部參與表達式的事情分紅三組:layout'ed object (視圖), confinement (layout guides 以及被 |-, -|, |prefix 還有 |postfix 包裹起來的東西), 和 constant.

如今咱們要將咱們的設計變動爲:

protocol Operand {
    associatedtype HeadAttribute: SyntaxAttribute
    
    associatedtype TailAttribute: SyntaxAttribute
}

protocol SyntaxAttribute {}

struct SyntaxAttributeLayoutedObject: SyntaxAttribute {}

struct SyntaxAttributeConfinment: SyntaxAttribute {}

struct SyntaxAttributeConstant: SyntaxAttribute {}
複製代碼

而後對於像 view1 - 4 - view2 之類的組合,咱們能夠建立下列表達式類型:

/// 連結 `view - 4`
struct LayoutableToConstantSpacedSyntax<Lhs: Operand, Rhs: Operand>: Operand where /// 確認左手邊算子的尾部是否是一個 layouted object Lhs.TailAttribute == SyntaxAttributeLayoutedObject, /// 確認右手邊算子的頭部是否是一個 constant Rhs.HeadAttribute == SyntaxAttributeConstant {
     typealias HeadAttribute = Lhs.HeadAttribute
     typealias TailAttribute = Lhs.TailAttribute
}

func - <Lhs, Rhs>(lhs: Lhs, rhs: Rhs) -> LayoutableToConstantSpacedSyntax<Lhs, Rhs> { ... }

/// 連結 `(view - 4) - view2`
struct ConstantToLayoutableSpacedSyntax<Lhs: Operand, Rhs: Operand>: Operand where /// 確認左手邊算子的尾部是否是一個 constant Lhs.TailAttribute == SyntaxAttributeConstant, /// 確認右手邊算子的頭部是否是一個 layouted object Rhs.HeadAttribute == SyntaxAttributeLayoutedObject {
     typealias HeadAttribute = Lhs.HeadAttribute
     typealias TailAttribute = Lhs.TailAttribute
}

func - <Lhs, Rhs>(lhs: Lhs, rhs: Rhs) -> ConstantToLayoutableSpacedSyntax<Lhs, Rhs> { ... }
複製代碼

經過聽從 Operand 協議,一個類型實際上就得到了兩個編譯時容器,它們的名字分別爲:HeadAttributeTailAttribute;其值則是屬於 SyntaxAttribute 的類型。經過調用函數 - (上述代碼塊的任意一個),編譯器將檢查左手邊算子和右手邊算子是否和函數返回值(ConstantToLayoutableSpacedSyntaxLayoutableToConstantSpacedSyntax)中的泛型約束一致。若是成功了,咱們就能夠說狀態成功地被轉移到另一個了。

咱們能夠看到,由於咱們在上述類型的體內已經設置了 HeadAttribute = Lhs.HeadAttributeTailAttribute = Lhs.TailAttribute,如今 LhsRhs 的頭部和尾部的 attribute 已經從 LhsRhs 上被轉移到了這個被新合成的類型上。而值就被儲存在其 HeadAttributeTailAttribute 上。

而後咱們成功讓編譯器接受了相似 view1 - 4 - view2, view1 - 10 - view2 - 19 這樣的輸入……等等!view1 - 10 - view2 - 19??? view1 - 10 - view2 - 19 應該是一個被編譯器拒絕的非法輸入!

Syntax Boundaries

實際上,咱們剛纔僅僅只是保證了一個視圖緊接着一個數字、一個數字緊接着一個視圖,而這和表達式是否以一個視圖(或者 layout guide)開始或結束無關。

爲了使表達式始終以一個視圖,layout guide 或者 |-, -|, |prefix|postfix 開頭,咱們必需要構建一個幫助咱們過濾掉無效輸入的邏輯——就像咱們以前作的 Lhs.TailAttribute == SyntaxAttributeConstantRhs.HeadAttribute == SyntaxAttributeLayoutedObject 那樣。咱們能夠發現實際上這些表達式能夠分爲兩組:confinementlayout'ed object。爲了使表達式始終以這兩組表達式中的表達式開頭或者結尾,咱們必須使用編譯時邏輯來實現它。咱們用運行時代碼寫出來就是:

if (lhs.tailAttribute == .isLayoutedObject || lhs.tailAttribute  == .isConfinment) &&
    (rhs.headAttribute == .isLayoutedObject || rhs.headAttribute == .isConfinment)
{ ... }
複製代碼

可是這個邏輯不能在 Swift 編譯時中被簡單實現,並且 Swift 編譯時計算的惟一邏輯就是邏輯。因爲在 Swift 中咱們只能在類型約束中使用邏輯(經過使用 Lhs.TailAttribute == SyntaxAttributeLayoutedObjectRhs.HeadAttribute == SyntaxAttributeConstant 中的 , 符號),咱們只能將上述代碼塊中的 (lhs.tailAttribute == .isLayoutedObject || lhs.tailAttribute == .isConfinment)(rhs.headAttribute == .isLayoutedObject || rhs.headAttribute == .isConfinment) 融合起來存入一個編譯時容器的值,而後使用邏輯來連結他們。

實際上,Lhs.TailAttribute == SyntaxAttributeLayoutedObject 或者 Rhs.HeadAttribute == SyntaxAttributeConstant 中的 == 和大多數編程語言中的 == 操做符等效。另外,Swift 編譯時計算中也有一個和 >= 等效的操做符: :

考慮下列代碼:

protocol One {}
protocol Two: One {}
protocol Three: Two {}

struct Foo<T> where T: Two {}
複製代碼

如今 Foo 中的 T 只能是「比 Two 大」的了.

而後咱們能夠將咱們的設計變動爲:

protocol Operand {
    associatedtype HeadAttribute: SyntaxAttribute

    associatedtype TailAttribute: SyntaxAttribute

    associatedtype HeadBoundary: SyntaxBoundary

    associatedtype TailBoundary: SyntaxBoundary
}

protocol SyntaxBoundary {}

struct SyntaxBoundaryIsLayoutedObjectOrConfinment: SyntaxBoundary {}

struct SyntaxBoundaryIsConstant: SyntaxBoundary {}
複製代碼

這一次咱們加入了兩個編譯時容器:HeadBoundaryTailBoundary,其值是屬於 SyntaxBoundary 的類型。對於視圖或者 layout guide 對象而言,他們提供了首尾兩個 SyntaxBoundaryIsLayoutedObjectOrConfinment 類型的 boundaries。當調用 - 函數時,視圖或者 layout guide 的 boundary 信息就會被傳入新合成的類型中。

/// 連結 `view - 4`
struct LayoutableToConstantSpacedSyntax<Lhs: Operand, Rhs: Operand>: Operand where /// 確認 LhsTailAttributeSyntaxAttributeLayoutedObject Lhs.TailAttribute == SyntaxAttributeLayoutedObject, /// 確認 RhsHeadAttributeSyntaxAttributeConstant Rhs.HeadAttribute == SyntaxAttributeConstant {
    typealias HeadBoundary = Lhs.HeadBoundary
    typealias TailBoundary = Rhs.TailBoundary
    typealias HeadAttribute = Lhs.HeadAttribute
    typealias TailAttribute = Lhs.TailAttribute
}

func - <Lhs, Rhs>(lhs: Lhs, rhs: Rhs) -> LayoutableToConstantSpacedSyntax<Lhs, Rhs> { ... }
複製代碼

如今咱們能夠修改咱們的 withVFL 系列函數的函數簽名爲:

func withVFL<O: Operand>(V: O) -> [NSLayoutConstraint] where
    O.HeadBoundary == SyntaxBoundaryIsLayoutedObjectOrConfinment,
    O.TailBoundary == SyntaxBoundaryIsLayoutedObjectOrConfinment
{ ... }
複製代碼

而後,只有 boundaries 是視圖或者 layout guide 的表達式才能被接受了。

Syntax Associativity

可是 syntax boundaries 的概念仍是不能幫助編譯器中止接受如 view1-| | view2 或者 view2-| - view2 之類的輸入。這是由於即便一個表達式的 boundaries 被確保了,你仍是不能保證這個表達式是不是 associable (可結合)的。

因而咱們要在咱們的設計中引入第三對 associatedtype

protocol Operand {
    associatedtype HeadAttribute: SyntaxAttribute

    associatedtype TailAttribute: SyntaxAttribute

    associatedtype HeadBoundary: SyntaxBoundary

    associatedtype TailBoundary: SyntaxBoundary

    associatedtype HeadAssociativity: SyntaxAssociativity

    associatedtype TailAssociativity: SyntaxAssociativity
}

protocol SyntaxAssociativity {}

struct SyntaxAssociativityIsOpen: SyntaxAssociativity {}

struct SyntaxAssociativityIsClosed: SyntaxAssociativity {}

複製代碼

對於像 |-, -| 之類的表達式或者一個表達式中的 layout guide,咱們就能夠在新類型的合成過程當中關掉他們的 associativity。

這足夠了嗎?

是的。實際上,我在這裏作了個弊。你也許會驚訝,爲何我能夠經過舉例快速地發現問題,一塊兒能夠對上面這個問題沒有猶豫地說「是」。緣由是,我已經在紙上枚舉完了全部語法樹的構型。在紙上計劃是成爲一個優秀軟件工程師的好習慣。

如今語法樹設計的核心概念已經很是接近個人生產代碼了。你能夠在這裏查看他們。

生成 NSLayoutConstraint 實例

好了,回來。咱們還有東西要來實現。這對咱們總體的工做很重要——生成佈局約束。

因爲咱們在 withVFL(V:) 系列函數的參數中所獲的的是一個語法樹,咱們能夠簡單地構建一個環境來對這個語法樹進行求值。

我正在剋制本身使用大詞,因此我說的是「構建一個環境」。可是禁不住告訴你,咱們如今要開始構建一個虛擬機了!

一些語法樹的例子
一些語法樹的例子

經過觀察一顆語法樹,咱們能夠發現每一層語法樹都是或不是一個單目操做符節點、雙目操做符節點或者算子節點。咱們能夠將 NSLayoutConstraint 的計算抽象成小碎片,而後讓這三種節點產生這些小碎片

聽起來很好。可是怎樣作這個抽象呢?如何設計那些小碎片呢?

對於有虛擬機設計經驗或者編譯器構造經驗的人來講,他們也許會知道這是一個有關「過程抽象」和「指令集設計」的問題。可是我並不想嚇唬到像你這樣可能對這方面沒有足夠知識的讀者,因而我以前稱呼他們爲「將 NSLayoutConstraint 的計算抽象成」「小碎片」。

另外一個讓我不以「過程抽象」和「指令集設計」來談論這個問題的理由是「指令集設計」是整個解決方案的最前端:你以後將會獲得一個被稱做 opcode (operation code 的縮寫,我也不知道爲何他們這樣縮略這個術語)的東西。可是「指令集設計」會嚴重影響「過程抽象」的最終形態,而若是在作「指令集設計」以前跳過思考「過程抽象」的問題的話,你也很難揣測出指令集背後的概念。

抽象 NSLayoutConstraint 的初始化過程

因爲咱們要支持 layout guide,那麼老式的 API:

convenience init(
    item view1: Any,
    attribute attr1: NSLayoutConstraint.Attribute,
    relatedBy relation: NSLayoutConstraint.Relation,
    toItem view2: Any?,
    attribute attr2: NSLayoutConstraint.Attribute,
    multiplier: CGFloat,
    constant c: CGFloat
)
複製代碼

就變得不可用了。你沒法用這個 API 讓 layout guide 工做。是的,我試過。

而後咱們也許會想起 layout anchors。

是的,這是可行的。個人生產代碼就是利用的 layout anchors。可是爲何 layout anchors 可行?

實際上,咱們能夠經過檢查文檔來知道 layout anchors 的基類 NSLayoutAnchor 有一組生成 NSLayoutConstraint 的 API。若是咱們能夠在肯定的步驟內得到這組 API 的全部參數,那麼咱們就能夠爲這個計算過程抽象出一個形式化的模型。

咱們能夠在肯定的步驟內得到這組 API 的全部參數嗎?

答案顯然是「是的」。

語法樹求值一瞥

在 Swift 中,語法樹的求值是深度優先遍歷的。下面這張圖就是下面這個代碼塊中 view1 - bunchOfViews 的遍歷順序。

let bunchOfViews = view2 - view3
view1 | bunchOfViews
複製代碼

Swift 語法樹遍歷
Swift 語法樹遍歷

可是雖然根節點是整個求值過程當中最早被訪問的,因爲它須要它左手邊子節點和右手邊子節點的求值過程來完成求值過程,它將在最後一個生成 NSLayoutConstraint 實例。

抽象 NSLayoutConstraint 的計算過程

經過觀察上面這個 Swift 語法樹求值過程的插圖,咱們能夠知道節點 view1 將於第二位被求值,可是求值結果最後才用得上。因此咱們須要一個數據結構能夠保存每個節點的求值結果。你也許想起來了要用棧。是的。我在個人生產代碼中就是用的棧。可是你應該知道爲何咱們要用棧:一個棧能夠將遞歸結構轉換爲線性的,這就是咱們想要的。你也許已經猜到了我要用棧,可是直覺並非每次都靈。

有了這個棧,咱們就能夠將全部初始化一個 NSLayoutConstraint 實例的計算資源放入之中了。

另外,咱們也要讓棧可以記憶已經被求完值的語法樹的首尾節點。

爲何?看看下面這個語法樹:

一個複雜的語法樹
一個複雜的語法樹

這個語法樹由如下表達式生成。

let view2_3 = view2 - view3
let view2_4 = view2_3 - view4
view1 | view2_4
複製代碼

當咱們對位於樹的第二層(從根節點開始數)的 - 節點進行求值時,咱們必需要選取 view3 這個「內側」來建立一個 NSLayoutConstraint 實例。實際上,生成 NSLayoutConstraint 實例老是須要選取從被求值節點看起來是「內側」的節點。可是對於跟節點 | 來講,「內側」節點就變成了 view1view2。因此咱們不得不讓棧來記憶被已經求完值的語法樹的首尾節點。

關於 "返回值"

是的,咱們不得不設計一個機制來讓語法樹的每個節點來返回求值結果。

我並不想談論真實電腦是如何在棧幀間是如何傳遞返回值的,由於這會根據返回數據的大小不一樣而不一樣。在 Swift 世界中,因爲全部東西都是安全的,這意味着可以綁定一片內存爲其餘類型的 API 是很是難用的,以碎片化的節奏來處理數據也不是一個好選擇(至少不是編碼效率的)。

咱們只須要使用一個在求值上下文中的本地變量來保存棧的最後一個彈棧結果,而後生成從這個變量取回數據的指令,而後咱們就完成了「返回值」系統的設計。

構建虛擬機

一旦咱們完成了過程抽象,指令集的設計就只差臨門一腳了。

實際上,咱們就是須要讓指令作以下事情:

  • 取回視圖、layout guide、約束關係、約束常數、約束優先級。

  • 生成要選取那個 layout anchor 的信息。

  • 建立佈局約束。

  • 壓棧、彈棧。

完成的生產代碼在這裏

評估

咱們已經完成了咱們這個編譯時確保安全的 VFL 的概念設計。

問題是咱們獲得了什麼?

對於咱們的編譯時確保安全的 VFL

咱們在此得到的優點是表達式的正確性是被保證了的。諸如 withVFL(H: 4 - view) 或者 withVFL(H: view - |- 4 - view) 之類的表達式將被在編譯時就被拒絕。

而後,咱們已經讓 layout guide 和咱們的 VFL Swift 實現一塊兒工做了起來。

第三,因爲咱們是在執行由編譯時組織的語法樹生成的指令,整體的計算複雜度就是 O(N),這個 N 是語法樹生成的指令的數目。可是由於語法樹並非編譯時完成構建的,咱們必需要在運行時完成語法樹的構建。好消息是,在個人生產代碼中,語法樹的類型都是 struct,這意味着語法樹的構建都是在棧內存上而不是堆內存。

事實上,在一成天的優化後,個人生產代碼超越了全部已有的替代方案(包括 Cartography 和 SnapKit)。這固然也包含了原版的 VFL。我將會在本文後部分放置一些優化技巧。

對於 VFL

理論上,相對於咱們的設計,原版 VFL 在性能上存在一些優點。VFL 字符串實際上在可執行文件(Mach-O 文件)的 data 段中被儲存爲了 C 字符串。操做系統直接將他們載入內存且在開始使用前不會有任何初始化動做。載入這些 VFL 字符串後,目標平臺的 UI 框架就預備對 VFL 字符串進行解析了。因爲 VFL 語法十分簡單,構建一個時間複雜度是 O(N) 的解析器也很簡單。可是我不知道爲何 VFL 是全部幫助開發者構建 Auto Layout 佈局約束方案中最慢的。

性能測試

如下結果經過在 iPhone X 上衡量 10k 次佈局約束構建測得。

Benchmark 1
Benchmark with 1 View
Benchmark 2
Benchmark with 2 Views
Benchmark 3
Benchmark with 3 Views


深刻閱讀

Swift 優化

Array 的代價

Swift 中的 Array 會花費不少時間在判斷它的內部容器是 Objective-C 仍是 Swift 實現的這點上。使用 ContiguousArray 可讓你的代碼單單以 Swift 的方式思考。

Collection.map 的代價

Swift 中的 Collection.map 被優化得很好——它每次在添加元素前都會進行預分配,這消除了頻繁的分配開銷。

Collection.map 的代價
Collection.map 的代價

可是若是你要將數組 map 成多維數組,而後將他們 flatten 成低維數組的話,在一開始就新建一個 Array 而後預分配好全部空間,再傳統地調用 Arrayappend(_:) 函數會是一個更好的選擇。

不具名類型的代價

不要在寫入場合使用不具名類型(tuples)。

Non-Nominal Types 的代價
Non-Nominal Types 的代價

當寫入不具名類型時,Swift 須要訪問運行時來確保代碼安全。這將花費不少時間,你應該使用一個具名的類型,或者說 struct 來代替它。

subscript.modify 函數的代價

在 Swift 中,一個 subscript(self[key] 中的 [key]) 有三種潛在的配對函數。

  • getter

  • setter

  • modify

什麼是 modify?

考慮如下代碼:

struct StackLevel {
    var value: Int = 0
}

let stack: Array<StackLevel> = [.init()]

// 使用 subscript.setter
stack[0] = StackLevel(value: 13)

// 使用 subscript.modify
stack[0].value = 13
複製代碼

subscript.modify 是一種用來修改容器內部元素的某一個成員值的函數。可是它看起來作的比單純修改值要多。

subscript.modify 的代價
subscript.modify 的代價

我甚至沒法理解個人求值樹中的 mallocfree 是怎麼來的。

我將求值棧從 Array 替換爲了本身的實現,而且實現了一個叫 modifyTopLevel(with:) 的函數來修改棧的頂部。

internal class _CTVFLEvaluationStack {
    internal var _buffer: UnsafeMutablePointer<_CTVFLEvaluationStackLevel>

    ...

    internal func modifyTopLevel(with closure: (inout _CTVFLEvaluationStackLevel) -> Void) {
        closure(&_buffer[_count - 1])
    }
}
複製代碼

OptionSet 的代價

Swift 中 OptionSet 帶來的方便不是免費的.

OptionSet 的代價
OptionSet 的代價

你能夠看到 OptionSet 使用了一個很是深的求值樹來得到一個能夠被手動 bit masking 求得的值。我不知道這個現象是否是存在於 release build 中,可是我如今在生產代碼中使用的是手動 bit masking。

Exclusivity Enforcement 的代價

Exclusivity enforcement 也對性能有衝擊。在你的求值棧中你能夠看見不少 swift_beginAccesswift_endAccess 的調用。若是你對本身的代碼有自信,我建議關掉運行時 exclusivity enforcement。在 Build Settings 中搜索 「exclusivity」 能夠看到相關選項。

在 Swift 5 下的 release build 中,exclusivity enforcement 是默認開啓的.

Exclusivity Enforcement 的代價
Exclusivity Enforcement 的代價

C 的編譯時計算

我還在個人一個框架中實現了一種有趣的語法: 經過 metamacros.h 來爲 @dynamic property 來添加自動合成器。範例以下:

@ObjCDynamicPropertyGetter(id, WEAK) {
    // 使用 _prop 訪問 property 名字
    // 其他和一個 atomic weak Objective-C getter 同樣.
}

@ObjCDynamicPropertyGetter(id, COPY) {
    // 使用 _prop 訪問 property 名字
    // 其他和一個 atomic copy Objective-C getter 同樣.
}

@ObjCDynamicPropertyGetter(id, RETAIN, NONATOMIC) {
    // 使用 _prop 訪問 property 名字
    // 其他和一個 nonatomic retain Objective-C getter 同樣.
};
複製代碼

實現文件在.

對於 C 程序員而言,metamacros.h 是一個很是有用的用來建立宏以減輕難負擔的腳手架。


謝謝你閱讀完了這麼長的一篇文章。我必需要道歉:我在標題撒了謊。這篇文章徹底不是「淺談」Swift 泛型元編程,而是談論了更多的關於計算的深度內容。可是我想這是做爲一個優秀程序員的基礎知識。

最後,祝願 Swift 泛型元編程不要成爲 iOS 工程師面試內容的一部分。


原文刊發於本人博客(英文)

本文使用 OpenCC 進行繁簡轉換

相關文章
相關標籤/搜索