如何编写高质量的 Swift 代码

Swift 刚刚正式发布了 5.3 版本,增加了很多新特性,比如上一篇的多尾随闭包。从 0.9 到 5.3 横跨数年,Swift 在语法、运行效率、易用性都在不断的提升和优化。对于开发者来说,如何写出高质量的 Swift 代码将提升程序的运行效率

本文基于官方文档 Writing High-Performance Swift Code
[本文难度:中,需要一定 Swift 基础]


预备知识

Swift 的编译流程

Swift 编译流程

相对于Objective-CSwift 语言在编译过程中增加了了 SIL 优化,是专门针对 Swift 的二次优化。从上图可以看出,在 AST 的基础上,进一步生成了 Swift 的高级中间语言 SIL,它与 LLVM IR 一起进行解析和优化,SIL 是其他语言没有的,在这里进行额外的优化。举个例子:

func test1() { print("hahaha") }
func test2() { test1() }
test2()

// 这样的代码,编译器会优化成直接调用 print("hahaha"),从而忽略对中间方法的调用。

优化虽然能提升运行效率,也会减缓编译时间(参考DebugRelease的时间),Debug 模式下默认关闭优化。

修改优化等级方式: BuildSetting - Compilation Mode。

Swift 的派发方式

派发方式

在 Swift 中的派发方式分为:

  1. 静态派发/直接派发:在编译时就确定了,与动态派发相比非常快,编译器知道要执行的函数,不需要像动态派发那样直到运行时才确定调用方法,意味着不会有多余的通信开销。
  2. 动态派发-方发表:引用类型,尤其是类和协议的派发方式,其中类为V-Table(Virtual Table 虚函数表), 协议为PWT(Protocol Witness Table 协议见证表?)
  3. 动态派发-消息:最为灵活,支持运行时更新新的函数实现。

其中静态派发方法最快,可以粗暴的理解为直接取址。协议的派发方式也是使用动态派发-方发表的方式,苹果对其进行了强化以实现 Swift 中各种强大的 Protocol 特性。消息派发可参考 Objective-C

编译过程中编译器会自动识别可优化为静态派发的部分。

Swift 中的写时复制(Copy On Write)

简单的说,在 Swift 中大量使用着值类型 (Value Type),一般情况下使用新的变量去获取值类型对象时就会触发复制的操作,在很多时候,只是持有对象并不会对对象进行修改,这时就会造成不必要的复制开销。而 Swfit 的写时复制意味着只有对象会发生更改时才会触发复制操作


正文

1. final,private\fileprivate,internal

Swift在编译过程中会对“确定”的代码进行优化,是否“确定”与代码的派发方式有关,动态派发的代码为“不确定”,进而不能优化。在开发中对代码使用 privatefinal 等来标识代码,编译器能更好的的进行优化。

动态派发很强,但直接派发很快。

final 修饰的属性、类、方法不会被覆盖,编译器就可以知道能优化为直接调用,而不用去进行动态派发。final 能让编译器在优化时能更好的识别并优化。

privatefileprivate 均表示在其范围外部不可见,进而帮助编译器自动推断出 final 并删掉对方法和属性的间接引用。

internal 是 Swift 的默认标识,不需要显式标记。表示内部的,编译器能自动推断 final

熟练使用 final、private 等标识并不仅仅在于给代码设定权限,更加规范。也帮助了编译器更好的优化我们的代码。

2. 容器中的类型

容器主要是指 ArrayDictionary,Swift 中两者均可以存入值类型引用类型,值类型的效率比引用类型高,也是 Swift 推荐的方式。某些情况下,容器在使用值类型可能会造成不必要的开销,对此主要有以下几点优化:

1)与引用类型不同的是,值类型在容器中时,只有在递归过程中才会进行引用计数,避免了额外的保留,进而优化了容器的使用效率。

2)开发中可以将 Array 看做是 OC 中 NSArray 与 NSMutableArray 当不需要与 NSArray 有桥接关系时 使用 ContiguousArray 来当做引用类型的容器。ContiguousArray 与 Array 的不同点在于其强制在内存上连续,效率比 Array 更高。

3)容器在 写时复制 的特性下,可能造成不必要的副本,在方法中需要修改的参数使用 inout 来避免这种情况。

3. 溢出检查

Swift 会对数值运算进行溢出检查,当确定计算不会造成溢出时,溢出检查就显得多余了,在需要进行大量运算的地方溢出检查就会对运算效率造成影响。此时,我们可以使用 Wrapping operations( &+、&-、&* )来避免溢出检查。

let value = 1 &+ 1

4. 泛型

编译器会查看泛型的每一次调用,并将其转换为专门的调用(参考类型推断)。仅当泛型的的声明在当前模块中可见时,优化器才能执行特化。仅当声明与泛型调用位于同一个文件中时,才会发生这种情况,除非使用了-whole-module-optimization标志。注意标准库是一种特殊情况,标准库中的定义在所有模块中均可见,并且可以进行专门化。

5. 大值类型

值类型复制时会创建一个副本,大值类型可能很耗时,降低效率。例如,值类型的树结构。

对这样的树类型采用“写时复制”时,比如使用 Array 将其包装。但这又引入了 Array 的所有方法,以及 Array 本身与 OC 的交互,索引的访问,都降低了效率。对此,自定义结构是个不错的建议:

final class Ref<T> {
  var val: T
  init(_ v: T) {val = v}
}

struct Box<T> {
    var ref: Ref<T>
    init(_ x: T) { ref = Ref(x) }

    var value: T {
        get { return ref.val }
        set {
          if !isKnownUniquelyReferenced(&ref) {
            ref = Ref(newValue)
            return
          }
          ref.val = newValue
        }
    }
}

自定义的 Box 简化了 Array 非必要部分,针对树结构重新设计容器。

6. 明确类协议

将仅由类满足的协议标记为类协议,编译器可以基于仅类满足该协议的特点来优化程序。

例如,如果 ARC 内存管理系统知道它正在处理类,则可以轻松保留(增加对象的引用计数)。在没有这种特点的情况下,编译器必须假定对象可以满足协议,并且需要保留或释放不确定的对象,这可能会很昂贵。

protocol Pingable: AnyObject { func ping() -> Int }

7. let/var 逃逸闭包

任何时候使用 let/var 来创建闭包绑定时,都会产生逃逸闭包,而当逃逸闭包被 var 捕获时,就会分配到堆区。当被 let 捕获时,是当做值捕获的,不必再存储副本。

如果闭包并没有逃逸,方法传递时就使用inout将其进行转义,这样就不会再被堆区那套保留/释放所影响。

总结

通过借助 编译过程优化值类型派发方式类型推断 以及 特定函数 等语言特性来对 Swift 进行优化。想要达到一个好的优化水准就需要对这些特性有深入了解。对优化过程中衍生出来的问题也要有一定认知。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342