Swift 作为现代、高效、安全的编程言语,其反面有许多高级特性为之支撑。

『 Swift 最佳实践 』系列对常用的言语特性逐一进行介绍,助力写出更简练、更典雅的 Swift 代码,快速完成从 OC 到 Swift 的改变。

该系列内容首要包括:

  • Optional
  • Enum
  • Closure
  • Protocol
  • Generics
  • Property Wrapper
  • Error Handling
  • Advanced Collections
  • Pattern Matching
  • High Performance

ps. 本系列不是入门级语法教程,需求有必定的 Swift 基础

本文是系列文章的第十篇,首要介绍 Swift Method Dispatch 以及一些常见的 Swift 功用优化方法。

Overview


Swift is a general-purpose programming language built using a modern approach to safety, performance, and software design patterns.

Swift.org – About Swift

Swift 作为一门现代言语,具有安全、高效等特色,可是在其延生之初就背上了兼容 OC 的沉重包袱,使得其在设计上不得不做出一些让步和让步。

其间,Method Dispatch 便是比方之一,今日的共享就从 Swift Method Dispatch 开端!

Swift Method Dispatch


一次方法调用总共分 ③ 步 ( inline 除外 ):

  • 找到方法进口地址
  • 跳转
  • 返回

其间,最重要的一步便是找到方法的进口地址,总的来说有 2 种方法:

  • 静态调用,编译-链接期间承认跳转地址

    • 编译时生成符号表
    • 链接时查表找到跳转地址

在终究生成的可执行文件中跳转地址是承认的

对上述进程感兴趣的同学可以看看 程序员的自我涵养 — 链接、装载与库

  • 动态调用,运行时查表,这也是 OOP 中 「 多态 (Polymorphism) 」 得以完成的基础,不同言语在完成上又有所区别:

    • 像 C++ 中有虚函数表 (VMT),全部虚函数都会记录到 VMT,调用时查表找到进口地址

      对 C++ Object Model、VMT 感兴趣的同学可以看看 深度探究 C++ 对象模型

    • Objective-C 中有 method_list,全部的方法都会记录到 method_list 中

下图简要总结了静态调用VMT 调用Message Dispatch 的进程与区别:

Swift 最佳实践之 High Performance

很明显,静态调用的功用比动态调用好,但灵活性缺乏。有得必有失

Swift 支撑上述 3 种调用方法!

那么 Swift 中上述 3 种调用方法别离发生在什么情况下❓

一句话,不需求 「动态性」 的情况下就会用静态调用

方法调用的「动态性」本质上是指,详细执行方法的哪个完成需求到运行时才干承认,而非编译期间就指定的,如:多态、swizzle。

  • Value type:structenum,因为它们不支撑继承,也就没有动态性可言。

    故,值类型的方法调用都是静态调用

  • Protocol extension: 通过 Existential Type 调用 Protocol extension 中完成的方法

    如:

    protocol SomeProtocol {}
    extension SomeProtocol {
      func bar() {
        print("Hello world!")
      }
    }
    func doSomething(_ sp: any SomeProtocol) {
      sp.bar()   //  静态调用
    }
    
  • Class extension: 因为 Swift extension 不允许重写主类中的方法,故 Class extension 中的方法都是静态调用

    来自 OC 基类的方法以及用 @obj dynamic 修饰的方法除外

    如:

    class SomeClass {}
    extension SomeClass {
      func bar() {    //  静态调用
        print("Hello world!")
      }
    }
    
  • final:final 修饰的类不能被继承、修饰的方法不能被重写 (override),故它们都是静态调用

  • private/privatefile 私有性使得编译器在编译时可以判别是否有继承、重写的操作,若没有,进而用静态调用替换直接调用,如:

    private class  SomePrivateClass {
      func doSomething() { ... }
    }
    class SomeClass {
      fileprivate func filePrivateMethod() { ... }
    }
    func doSomething(_ obj: SomePrivateClass) {
      // 因为在当前文件中没有 SomePrivateClass 的子类
      // 也便是 SomePrivateClass 不或许有子类,因其 private 的可见性
      // 故编译器可以「放心肠」将方法 doSomething 的调用改为静态调用
      //
      obj.doSomething()
    }
    func doSomething(_ obj: SomeClass) {
      // 同理
      //
      obj.filePrivateMethod()
    }
    
  • internal 在敞开 Whole-Module Optimization (WMO) 时,可见性为 internal 的方法也或许被编译器优化为静态调用,原理与上述 private/privatefile 相同

除以上情况外,都是动态调用

如前所述,Swift 中动态调用又分为查表 (vtable/witness_table) 和 OC Message Dispatch:

  • Swift 中继承并重写 OC 基类的方法,通过 Message Dispatch 调用

  • dynamic + @objc 关键字修饰的方法通过 Message Dispatch 调用

    • dynamic 关键字的意义是用动态派发 (Dynamic Dispatch)
    • @objc 标明对 OC 可见
    • 2 者没有任何相关、交集
    • 可是,Swift 并没有什么 Dynamic Dispatch 一说,有必要凭借 OC Message Disaptch。因此,dynamic 需求协作 @objc 一起运用才有用。
    • dynamic + @objc 首要用于 KVO、swizzling 等
  • 其他情况都是查表

下面通过一个比方、一个表格来总结一下 Swift Method Disaptch 的全部情况:

Swift 最佳实践之 High Performance

Swift 最佳实践之 High Performance

High Performance


减少动态调用

上一末节详细介绍了 Swift Method Dispatch,我们知道静态调用比动态调用功用更好。

因此,尽量用静态调用代替动态调用,我们可以做的有:

  • 充分利用 privatefileprivate 以及 internal

我个人觉得,privatefileprivate 以及 internal 对代码可维护性、对接口语义的完善比功用更重要!

因此,不要放过任何一次能用它们的时机!

充分利用泛型特化 (Generic Specialization)

在 Swift 最佳实践之 Generics 一文中,我们讲过编译器会对 Generic 做特化优化,其条件是:

  • 泛型方法与调用方在同一个源文件,一起进行编译
  • 敞开Whole-Module Optimization 时,同一模块内部的泛型调用也可以被特化

因此,在设计允许下尽量将泛型方法界说与调用放在同一文件或模块内

关于只要 class 能完成的协议加上 AnyObject 束缚

在开发中,或许有些 protocol 只希望 class 去完成

此时,可以在 protocol 界说时加上 AnyObject 束缚,如:

protocol SomeProtocol: AnyObject {}

关于有 AnyObject 修饰的 protocol,编译器在完成时可以做许多优化,因为可以打扫其反面的完成是 Value-Type 的或许。

比方,在内存管理上可以放心的用 ARC,而不用考虑 Value-Type copy 等问题。

在 Swift 最佳实践之 Protocol 中介绍过,non-class constraint protocol 与 class constraint protocol (: AnyObject) 的 Existential Container 不相同:

// for non-class constraint protocol
//
struct OpaqueExistentialContainer {
  void *fixedSizeBuffer[3];
  Metadata *type;
  WitnessTable *witnessTables[NUM_WITNESS_TABLES];
}
// for class constraint protocol
//
struct ClassExistentialContainer {
  HeapObject *value;
  WitnessTable *witnessTables[NUM_WITNESS_TABLES];
}

Collections

函数式编程 (Functional programming) 思想在 Swift 中随处可见,在 Collection 上也有很多函数式操作符。

优先考虑运用函数式操作符处理集结,如:

let values = [1, 2, 3]
values.forEach { ... }         // ✅
for value in values { ... }    // ❌

contains > first(where:)

在判别集结中是否存在某个元素时优先运用 contains,如:

let values = [1, 2, 3]
let result = values.contains(1)                     // ✅
let result1 = values.first { $0 == 1 } != nil       // ❌
let result2 = !values.filter { $0 == 1 }.isEmpty    // ❌

isEmpty > count

When you need to check whether your collection is empty, use theisEmptyproperty instead of checking that thecountproperty is equal to zero. For collections that don’t conform toRandomAccessCollection, accessing thecountproperty iterates through the elements of the collection.

isEmpty – Apple Developer Documentation

判别集结是否为空时,优先运用 isEmpty,其时间复杂度为 O(1),而关于 Array 等非 RandomAccessCollection 的集结 count 需求遍历,时间复杂度为 O(n)

如:

let values = [1, 2, 3]
let result = values.isEmpty           // ✅
let result1 = values.count != 0       // ❌

first(where:) > filter

查找第一个满足条件的元素时优先运用 first(where:),如:

let result = values.first { $0 > 1 }            // ✅
let result2 = values.filter { $0 > 1 }.first    // ❌

allSatisfy > reduce

判别集结中全部元素是否都满足某个条件时,优先运用allSatisfy,如:

let result = values.allSatisfy { $0 > 0 }                  // ✅
let result1 = reduce(true) { partialResult, value in       // ❌
  return partialResult && value > 0
}
let result2 = values.filter { $0 <= 0 }.isEmpty            // ❌

slice > sub collection

正如,Swift 最佳实践之 Advanced Collections 中所讲到,对集结做 slicing 不会有内存分配、copy 元素的操作,切片与原始集结共用同一块内存,切片的时间复杂度为 O(1)

Swift 最佳实践之 High Performance

因此,优先考虑运用 Slice,而非创建子集结,如:

let values = [1, 2, 3, 4, 5, 6]
let slice = values[2...4]    // ✅
var sub = [Int]()            // ❌
for i in 2...4 {
  sub.append(values[i])
}

数组元素优先考虑 Value-type

Swift Array 其完成需求考虑反面是 NSArray 的或许性

为了处理 NSArrayArray 有一些额定的桥接作业要做

可是,NSArray 的元素类型必定是 Reference-type

因此,如果 Array 的元素类型是 Value-type,就可以打扫是 NSArray 的或许性,此时编译器就可以优化掉相关的桥接作业,如:

struct Student {  //  struct ✅ class ❌
  let name: String
  let age: Int
}
var students: [Student]  // 

Swift 最佳实践之 High Performance

需求留意的是,值类型触及 copy、move 等问题,在数据量很大时不必定比引证类型有优势,因为通常情况下引证类型 copy 的是指针,需求 case-by-case 处理。

小结

为了统筹功用、灵活性以及兼容 OC,Swift 存在 3 种方法派发方法:Static、Table、Message。它们有着不同的功用表现,在开发中我们要尽量运用静态派发。

通过设置可继承性 (final)、可见性 (privatefileprivateinternal) 等,可以进步静态派发的或许性。

我个人觉得,它们的意义不仅仅是提升功用,更重要的是可以进步代码的可维护性、完善接口语义!

另外,函数式编程思想在 Swift 中有充分的表现,在日常开发中我们要改变思想,长于利用 Swift 供应的函数式基础设备。

至此,共 10 期的『 Swift 最佳实践 』系列共享告一段落,希望对正在学习、运用 Swift 的同学有所协助❗️

参考资料

swift/docs/OptimizationTips.rst at main apple/swift GitHub

What’s .self, .Type and .Protocol? Understanding Swift Metatypes

www.mikeash.com/pyblog/frid…

varun04tomar.medium.com/to-the-dept…

@objc and dynamic – Swift Unboxed