浅谈 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 进行繁简转换

相关文章
相关标签/搜索