Swift-高性能指南-全-

Swift 高性能指南(全)

原文:zh.annas-archive.org/md5/8d73e1345bcbb06820c0bc44a2f3332a

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在 2014 年 6 月的 WWDC 上,苹果公司宣布了一种新的编程语言,名为 Swift。Swift 是一种非常现代且强大的语言。在过去的一年里,Swift 已经成为一种非常流行的编程语言。它已经发展和变化。由于 Swift 相对较新,因此与其性能特性和实现高性能的最佳实践相关的问题有很多。

Swift 高性能概述了 Swift 的重要特性、其性能特点以及一系列建议和技术,这些技术可以帮助你在 Swift 中构建性能卓越且可持续的应用程序。

本书还概述了不同的工具,这些工具可以帮助你调试、调查和改进你的代码。

本书涵盖的内容

第一章, 探索 Swift 的强大功能和性能,介绍了 Swift、其强大的功能、其性能以及与 Objective-C 的互操作性。

第二章, 在 Swift 中构建良好的应用程序架构,详细介绍了 Swift 的强大功能以及如何将这些功能应用于构建稳固的应用程序架构。

第三章, 使用 Swift 工具包测试和识别慢速代码,介绍了不同的 Swift 和 Xcode 工具,用于代码原型设计、性能测量以及识别和改进慢速代码。

第四章, 提高代码性能,展示了 Swift 与性能相关的细节和功能,并演示了 Swift 如何实现其高性能。本章还涵盖了提高应用程序性能的不同优化技术。

第五章, 选择正确的数据结构,涵盖了不同的数据结构、它们的特性、它们的性能特点以及何时应用它们的建议。

第六章, 为高性能构建应用程序架构,展示了不同的应用程序架构技术,这些技术可以帮助你实现高性能,例如并发、避免状态和单一职责。

第七章, 懒惰的重要性,涵盖了提高应用程序性能的重要技术,如延迟加载、延迟集合和评估。

第八章,发现 Swift 的所有潜在力量,为您提供了更多关于 Swift 结构、其工具和编译过程的信息,并更好地理解了 Swift 如何实现其性能。

您需要本书的内容

本书的内容和代码示例使用 Xcode 7 和 Swift 2.0 编写。为了跟随教程,您需要以下内容:

  • Mac OS 10.9 或更高版本:目前 Swift IDE、编译器和工具仅适用于 Mac OS。

  • Xcode 7.0 或更高版本:Xcode 是 Swift iOS 和 Mac 应用程序的主要开发工具。您可以通过 Mac AppStore 在 itunes.apple.com/en/app/xcode/id497799835?mt=12 安装。

  • Xcode 和模拟器的命令行工具:安装并启动 Xcode 后,它将提供安装附加命令行工具的选项。Xcode 默认安装模拟器,但您可以通过转到 Xcode | 首选项 | 下载 来下载更多模拟器。

本书面向的对象

本书是为已经了解 Swift 基础的开发者而写的,他们想学习更高级的功能以及如何在 Swift 中实现高性能和构建稳固的应用程序。我们假设您至少对 Mac OS 和 Xcode IDE 熟悉。本书适合所有希望将 Swift 知识提升到新水平的人。

了解 iOS 或 Mac OS 编程和 Objective-C 将是加分项,但不是必需的。

规范

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下所示:“首先,让我们给 Person 类添加一个昵称。”

代码块设置如下:

var sam = Person(firstName: "Sam", lastName: "Bosh", 
  nickName:"BigSam")
sam = sam.changeNickName("Rockky")

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

<type parameter : constraint >

func minElem<T : Comparable>(x: T, _ y: T) -> T {
  return x < y ? x : y
}

任何命令行输入或输出都应如下编写:

(lldb) repl
1> func isAllPositive(ar: [Int]) -> Bool { 
2\.   let negatives = ar.filter { $0 < 0 }
3\.   return negatives.count == 0
4\. }

新术语重要词汇将以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,将以如下方式显示:“我们将选择一个时间分析器模板并点击记录。”

注意

警告或重要注意事项将以如下框显示。

小贴士

小技巧和窍门如下所示。

读者反馈

我们欢迎读者的反馈。请告诉我们您对本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要向我们发送一般反馈,请简单地发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及本书的标题。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您购买的所有 Packt 出版物的账户中下载示例代码文件。www.packtpub.com。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您,或者您也可以访问 github.com/kostiakoval/SwiftHighPerformance,这是包含本书代码示例的 GitHub 仓库。

错误清单

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站或添加到该标题的错误部分下的现有错误清单中。

要查看之前提交的错误清单,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在错误清单部分下。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。

第一章. 探索 Swift 的功能和性能

在 2014 年,苹果发布了一种新的编程语言,名为 Swift。Swift 是从头开始设计的,具有许多强大的功能。它是静态类型的,非常安全。它拥有简洁且优美的语法,运行速度快,灵活,并且还有许多其他优点,你将在本书后面的章节中了解到。Swift 看起来非常强大,具有巨大的潜力。苹果对 Swift 设定了很高的期望,他们的主要目标是让 Swift 成为 Objective-C 的替代品,这将在不久的将来实现。

在本章中,你将熟悉 Swift 编程语言,了解它是为什么而设计的,以及它的优势和功能。我们还将制作我们的第一个 Swift 应用程序,并看看它如何容易地与现有的 Objective-C 代码集成。

在本章中,我们将涵盖以下主题:

  • 欢迎来到 Swift

  • 编写 Swift 代码

  • Swift 互操作性

  • 性能和性能关键指标的重要性

极速

我可以猜到你打开这本书是因为你对速度感兴趣,可能正在想,“Swift 能有多快?”在你开始学习 Swift 并发现它的所有优点之前,让我们在这里立即回答这个问题。

让我们用一个包含 100,000 个随机数的数组为例;使用 stdlib 中的标准 sort 函数(在 Swift 中是 sort,在 C 中是 qsort,在 Objective-C 中是 compare)在 Swift、Objective-C 和 C 中对其进行排序,并测量每个操作所需的时间。

对包含 100,000 个整数元素的数组进行排序,我们得到以下结果:

技术 耗时
Swift 0.00600 秒
C 0.01396 秒
Objective-C 0.08705 秒

而赢家是,Swift!Swift 比 Objective-C 快 14.5 倍,比 C 快 2.3 倍。

在其他示例和实验中,C 通常比 Swift 快,而 Swift 比 Objective-C 快得多。这些测量是在 Xcode 7.0 beta 6 和 Swift 2.0 下进行的。重要的是要强调,Swift 2.0 的改进主要集中在使其更简洁、更强大、更安全、更稳定,并为开源做准备。Swift 的性能尚未达到其全部潜力,未来非常令人兴奋!

欢迎来到 Swift

Swift 编程语言是由苹果从头开始设计的。它的口号是“没有 C 的 Objective-C”。这个短语的意思是 Swift 没有任何向后兼容性的限制。它是全新的,没有任何旧的重负。在你开始学习 Swift 的全部力量之前,我认为回答一些关于为什么你应该学习它的问题会有所帮助,如果你对此有任何疑问,我应该消除它们。

为什么我应该学习 Swift?

Swift 是一种非常新的编程语言,但它已经变得非常流行,并获得了巨大的关注。然而,许多 iOS 和 OS X 开发者提出了这些问题:

  • 我应该学习 Swift 吗?

  • 我应该学习 Swift 还是 Objective-C?

  • Objective-C 会继续存在还是消亡?

  • Swift 是否准备好用于生产应用程序?

  • Swift 比 Objective-C 或 C 快吗?

  • 我可以用 Swift 编写哪些应用程序?

我的回答是,“是的,绝对!”你应该学习 Swift。无论你是新的 iOS 和 OS X 开发者,还是有一些 Objective-C 的背景,你都应该学习 Swift。

如果你是一名新开发者,那么从 Swift 开始是非常有用的,因为你在 Swift 中将学习编程基础和技术,进一步学习 Swift 会更容易。尽管学习 Objective-C 也绝对有用,但我建议先学习 Swift,这样你就可以在 Swift 上建立你的编程思维。

如果你已经有一些 Objective-C 的经验,那么你应该尽快尝试 Swift。它不仅会给你带来一门新编程语言的知识,还会打开在 Objective-C 中解决问题的新思路和方法。我们可以看到,由于 Swift 的出现,Objective-C 已经开始进化。

由于与 C 的向后兼容性,Objective-C 有很多限制。它是在 23 年前,即 1983 年创建的,但它会比 Swift 更早消亡。

在 Swift 1.0 版本发布后,仅仅一年时间,我们就看到了许多 Swift 应用程序在 App Store 上成功开发和发布。在这个时间段内,许多提高开发生产力的 Swift 工具和开源库也被创建。

在 2015 年的 WWDC 上,苹果宣布 Swift 将开源。这意味着 Swift 可以用来编写任何软件,而不仅仅是 iOS 或 OS X 应用程序。你可以用 Swift 编写一段服务器端代码或网络应用程序。这是你应该学习 Swift 的另一个原因。

另一方面,我们看到 Swift 正在持续发展中。在 1.2 版本中有很多变化和改进,在 2.0 版本中变化更多。尽管使用 Xcode 迁移器升级到新的 Swift 版本非常容易,但这仍然是你需要考虑的事情。

Swift 有一些有前景的性能特性。我们在 Swift 1.2 的发布中看到了巨大的性能提升,Swift 2.0 也有一些改进。你从之前的例子中已经看到了 Swift 的速度有多快,总的来说,Swift 比 Objective-C 有更多的潜力实现高性能。

最后,我想提到一个我非常喜欢的,由 Bryan Irace 提出的短语:

当 iOS SDK 说“跳”,你问“多高”?

不要等待,学习 Swift!

Swift 的特性和优势

到目前为止,你知道你应该学习 Swift,而且不应该有任何疑虑。让我们看看是什么让 Swift 如此神奇和强大。以下是我们将要介绍的一些重要特性列表:

  • 清晰美观的语法

  • 类型安全

  • 可达的类型系统

  • 强大的值类型

  • 多范式语言——面向对象、协议导向和函数式

  • 通用目的

  • 快速

  • 安全

清晰美观

强大的功能和性能很重要,但我认为整洁和美观同样重要。你每天都会编写和阅读代码,它必须整洁美观,这样你才能享受它。Swift 非常整洁美观,以下是一些使其如此的主要特性。

无分号

分号是为了编译器而创建的。它们帮助编译器理解源代码并将其拆分为命令和指令。但源代码是为人类编写的,我们可能应该从其中去除编译器指令:

var number = 10
number + 5

// Not recommended
var count = 1;
var age = 18; age++

每条指令的末尾不需要分号 (😉。这看起来可能是一个非常小的特性,但它使得代码更加美观,更容易编写和阅读。然而,如果你想的话,可以添加分号。当同一行有两个指令时,需要分号。还有一些情况下必须使用分号,例如 for 循环(for var i = 0; i < 10; i++),但在这个上下文中,它们用于不同的目的。

小贴士

我强烈建议不要使用分号,并避免在同一行中使用多个指令。

类型推断

使用类型推断,你不需要指定变量和常量的类型。Swift 会自动从上下文中检测正确的类型。有时,你不得不显式指定类型并提供类型注解。当变量没有赋值时,Swift 无法预测该变量的类型:

var count = 10            //count: Int
var name = "Sara"         //name: String
var empty = name.isEmpty   //empty: Bool

// Not recommended
var count: Int = 10
var name: String = "Sara"
var empty: Bool = name.isEmpty

// When you must provide type annotation
var count: Int
var name: String

count = 10
name = "Sara"

在大多数情况下,Swift 可以从分配给变量的值中理解变量的类型。

小贴士

如果不需要,不要使用类型注解。给变量起有描述性的名字应该足够了。这使得你的代码整洁且易于阅读。

其他干净的 Swift 代码特性

Swift 所有干净代码特性的列表非常长;这里列举其中一些:闭包语法、函数的默认参数值、函数的外部参数名称、默认初始化器、下标和运算符:

  • 干净的闭包语法:闭包是一个独立的代码块,可以被视为一个轻量级的无名称函数。它具有与函数相同的功能,但语法更简洁。你可以将其分配给变量,调用它,或将它作为参数传递给函数。例如,{ $0 + 10 } 是一个闭包:

    let add10 = { $0 + 10 }
    add10(5)
    
    let numbers = [1, 2, 3, 4]
    numbers.map { $0 + 10 }
    numbers.map(add10)
    
  • 默认参数值和外部名称:在声明函数时,你可以为参数定义默认值,并给它们不同的外部名称,这些名称在调用该函数时使用。使用默认参数,你可以定义一个函数,但可以用不同的参数来调用它。这减少了创建不必要的函数的需求:

    func complexFunc (x: Int, _ y: Int = 0, extraNumber z: Int = 0, name: String = "default") -> String{
        return  "\(name): \(x) + \(y) + \(z) = \(x + y + z)"
    }
    
    complexFunc(10)
    complexFunc(10, 11)
    complexFunc(10, 11, extraNumber: 20, name: "name")
    
  • 默认和成员初始化器:Swift 在某些情况下可以为你创建结构体和基类的初始化器。代码更少,代码质量更高:

    struct Person {
        let name: String
        let lastName: String
        let age: Int
    }
    
    Person(name: "Jon", lastName: "Bosh", age: 23)
    
  • 下标:这是访问集合成员元素的一种好方法。你可以使用任何类型作为键:

    let numbers = [1, 2, 3, 4]
    let num2 = numbers[2]
    
    let population = [
      "China" : 1_370_940_000,
      "Australia" : 23_830_900
    ]
    population["Australia"]
    

    您也可以为您的自定义类型定义下标运算符,或者通过向它们添加自己的下标运算符来扩展现有类型:

    // Custom subscript
    struct Stack {
      private var items: [Int]
    
      subscript (index: Int) -> Int {
        return items[index]
      }
    
      // Stack standard functions
      mutating func push(item: Int) {
        items.append(item)
      }
    
      mutating func pop() -> Int {
        return items.removeLast()
      }
    }
    
    var stack = Stack(items: [10, 2])
    stack.push(6)
    stack[2]
    stack.pop()
    
  • 运算符:这些是表示功能的符号,例如,+ 运算符。您可以通过扩展您的类型来支持标准运算符或创建自己的自定义运算符:

    let numbers = [10, 20]
    let array = [1, 2, 3]
    let res = array + numbers
    
    struct Vector {
      let x: Int
      let y: Int
    }
    
    func + (lhs: Vector, rhs: Vector) -> Vector {
      return Vector(x: lhs.x + rhs.x, y: lhs.y + rhs.y);
    }
    
    let a = Vector(x: 10, y: 5)
    let b = Vector(x: 2, y: 3)
    
    let c = a + b
    

    提示

    仔细定义您的自定义运算符。它们可以使代码更简洁,但它们也可能给代码带来更多的复杂性,并使其难以理解。

  • guardguard 语句用于在继续执行代码之前检查条件是否满足。如果条件不满足,它必须退出作用域。guard 语句消除了嵌套条件语句和“死亡金字塔”问题:

    注意

    更多关于“死亡金字塔”(编程)的信息,请参阅en.wikipedia.org/wiki/Pyramid_of_doom_(programming)

    func doItGuard(x: Int?, y: Int) {
      guard let x = x else { return }
      //handle x 
      print(x)
    
      guard y > 10 else { return }
      //handle y
      print(y)
     }
    

清洁代码总结

如您所见,Swift 非常干净、漂亮。展示 Swift 如何干净和美丽最好的方式是尝试在 Swift 和 Objective-C 中实现相同的功能。

假设我们有一个人的列表,我们需要找到符合特定年龄标准的人,并将他们的名字转换为小写。

这就是这段代码的 Swift 版本将看起来像什么:

struct Person {
  let name: String
  let age: Int
}

let people = [
  Person(name: "Sam", age: 10),
  Person(name: "Sara", age: 24),
  Person(name: "Ola", age: 42),
  Person(name: "Jon", age: 19)
]

let kids = people.filter { person in person.age < 18 }
let names = people.map { $0.name.lowercaseString }

以下是将此代码转换为 Objective-C 版本的内容:

//Person.h File
@import Foundation;

@interface Person : NSObject

@property (nonatomic) NSString *name;
@property (nonatomic) NSInteger age;

- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;

@end

//Person.m File
#import "Person.h"

@implementation Person

- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
  self = [super init];
  if (!self) return nil;

  _name = name;
  _age = age;

  return self;
}

@end

NSArray *people = @[
    [[Person alloc] initWithName:@"Sam" age:10],
    [[Person alloc] initWithName:@"Sara" age:24],
    [[Person alloc] initWithName:@"Ola" age:42],
    [[Person alloc] initWithName:@"Jon" age:19]
];

NSArray *kids = [people filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"age < 18"]];

NSMutableArray *names = [NSMutableArray new];
for (Person *person in people) {
  [names addObject:person.name.lowercaseString];
}

结果相当惊人。Swift 代码有 14 行,而 Objective-C 代码有 40 行,包括 .h.m 文件。现在您可以看到差异了。

安全

Swift 是一种非常安全的编程语言,它在编译时进行大量的安全检查。目标是尽可能在编译时而不是在运行应用程序时捕获尽可能多的问题。

Swift 是一种类型安全的编程语言。如果您在类型上犯了错误,例如尝试将 IntString 相加或向函数传递错误的参数,您将得到一个错误:

let number = 10
let part = 1.5

number + part; // Error

let result = Double(number) + part

Swift 不会为您做任何类型转换;您必须显式地进行,这使得 Swift 更加安全。在这个例子中,我们在添加之前必须将一个 Int 数字转换为 Double 类型。

可选

Swift 中引入的一个非常重要的安全类型是 可选。可选是一种表示值不存在的方式——nil。您不能将 nil 分配给具有 String 类型的变量。相反,您必须通过将其声明为可选的 String? 类型来表明这个变量可以是 nil

var name: String = "Sara"
name = nil //Error. You can't assign nil to a non-optional type

var maybeName: String?
maybeName = "Sara"
maybeName = nil // This is allowed now

要将一个类型转换为可选类型,您必须在类型后放置一个问号(?),例如,Int?String?Person?

您也可以使用 Optional 关键字声明可选类型,例如 Optional<String>,但使用 ? 的简短方式更受欢迎:

var someName: Optional<String>

可选就像一个包含某些值或无物的盒子。在使用值之前,您需要先解包它。这种技术称为解包可选,或者如果您将解包的值分配给一个常量,则称为可选绑定:

if let name = maybeName {
  var res = "Name - " + name
} else {
  print("No name")
}

提示

在访问可选值之前,你必须始终检查它是否有值。

错误处理

Swift 2.0 具有强大且非常易于使用的错误处理。其语法与其他语言的异常处理语法非常相似,但工作方式不同。它有throwcatchtry关键字。Swift 错误处理由以下组件组成,如下所述:

  • 错误对象表示一个错误,并且它必须符合ErrorType协议:

    enum MyErrors: ErrorType {
      case NotFound 
      case BadInstruction
    }
    

    提示

    Swift 枚举最适合表示一组相关的错误对象。

  • 每个可能抛出错误的函数都必须在参数列表之后使用throws关键字进行声明:

    func dangerous(x: Int) throws
    func dangerousIncrease(x: Int) throws -> Int
    
  • 要抛出一个错误,请使用throw关键字:

    throw MyErrors.BadInstruction
    
  • 当你调用一个可能抛出错误的函数时,你必须使用try关键字。这表示函数可能会失败,并且后续代码将不会执行:

      try dangerous(10)
    
  • 如果发生错误,必须使用dotry关键字来捕获和处理,或者通过声明该函数为throws来进一步抛出:

    do {
      try dangerous(10)
    }
    catch {
      print("error")
    }
    

让我们看看一个代码示例,展示如何在 Swift 中处理异常:

enum Error: ErrorType {
  case NotNumber(String)
  case Empty
}

func increase(x: String) throws -> String {
  if x.isEmpty {
    throw Error.Empty
  }

  guard let num = Int(x) else {
    throw Error.NotNumber(x)
  }

  return String(num + 1)
}

do {
  try increase("10")
  try increase("Hi")
}
catch Error.Empty {
  print("Empty")
}
catch Error.NotNumber (let string) {
  print("\"\(string)\" is not a number")
}
catch {
  print(error)
}

Swift 还有许多其他安全特性:

  • 内存安全性确保在使用之前初始化值。

  • 具有安全检查的两阶段初始化过程

  • 必须的方法重写和许多其他功能

丰富的类型系统

Swift 有以下强大的类型:

  • 结构体是灵活的构建块,可以存储数据以及操作这些数据的方法。结构体与类非常相似,但它们是值类型:

    struct Person {
      let name: String
      let lastName: String
    
      func fullName() -> String {
        return name + " " + lastName
      }
    }
    
    let sara = Person(name: "Sara", lastName: "Johan")
    sara.fullName()
    
  • 元组是将多个值组合成一个类型的一种方式。元组内的值可以有不同的类型。元组对于从函数中返回多个值非常有用。如果元组有命名元素,你可以通过索引或名称访问元组内的值;或者你可以将元组中的每个项分配给一个常量或变量:

    let numbers = (1, 5.5)
    numbers.0
    numbers.1
    
    let result: (code: Int, message: String) = (404, "Not fount")
    result.code
    result.message
    
    let (code ,message) = (404, "Not fount")
    
  • Range表示从xy的数字范围。还有两个范围运算符可以帮助创建范围:闭合范围运算符和半开范围运算符:

    let range = Range(start: 0, end: 100)
    let ten = 1...10 //Closed range, include last value 10
    let nine = 0..<10 //half-open, not include 10
    
  • 枚举表示一组相关的常见值。枚举的成员可以是空的,有原始值,或者有任何类型的关联值。枚举是一等类型;它们可以有方法、计算属性、初始化器和其他功能。它们非常适合类型安全的编码:

    enum Action: String {
      case TakePhoto
      case SendEmail
      case Delete
    }
    
    let sendEmail = Action.SendEmail
    sendEmail.rawValue //"SendEmail"
    
    let delete = Action(rawValue: "Delete")
    

强大的值类型

Swift 中有两种非常强大的值类型:structenum。Swift 标准库中的几乎所有类型都是使用structenum实现的不可变值类型,例如RangeStringArrayIntDictionaryOptionals等。

值类型相对于引用类型有四个主要优势,它们是:

  • 不可变

  • 线程安全

  • 单一所有者

  • 在栈内存上分配

值类型是不可变的,并且只有一个所有者。值数据在赋值和将值作为函数参数传递时被复制:

var str = "Hello"
var str2 = str

str += " :)"

注意

Swift 足够智能,只有在值被修改时才会执行值复制。赋值操作不会发生值复制,即 str2 = str,而是在值修改时,即 str += ":)"。如果你删除那行代码,strstr2 将会共享相同的不可变数据。

多范式语言

Swift 是一种多范式编程语言。它支持许多不同的编程风格,例如面向对象、协议导向、函数式、泛型、块结构化、命令式和声明式编程。让我们在这里更详细地看看其中的一些。

面向对象

Swift 支持面向对象的编程风格。它具有单继承模型的类,能够遵守协议,访问控制,嵌套类型和初始化器,具有观察者的属性,以及其他面向对象的特性。

协议导向

协议和协议导向编程的概念并不新鲜,但 Swift 协议拥有一些强大的特性,使它们变得特别。协议导向编程的一般思想是使用协议而不是类型。这样,我们可以创建一个非常灵活的系统,具有对具体类型的弱绑定。

在 Swift 中,你可以扩展协议并提供方法的默认实现:

extension CollectionType {

  func findFirst (find: (Self.Generator.Element) -> Bool) -> Self.Generator.Element? {

    for x in self { 
      if find(x) {
        return x
      }
    }
    return nil
  }
}

现在,每个实现了 CollectionType 的类型都有一个 findFirst 方法:

let a = [1, 200, 400]
let r = a.findFirst { $0  > 100 }

使用协议导向编程的一个主要优势是,我们可以向相关类型添加方法,并使用点(.)语法进行方法链式调用,而不是使用自由函数和传递参数:

let ar = [1, 200, 400]

//Old way
map(filter(map(ar) { $0 * 2 }) { $0 > 50 }) { $0 + 10 } 

//New way
ar.map{ $0 * 2 } .filter{ $0 > 50 } .map{ $0 + 10 }

函数式

Swift 还支持函数式编程风格。在函数式语言中,函数是一种类型,它被以与其他类型相同的方式处理,例如 Int;它也被称作 一等函数。函数可以被分配给变量,并作为参数传递给其他函数。这实际上有助于解耦你的代码,并使其更具可重用性。

一个很好的例子是数组的 filter 函数。它接受一个执行实际过滤逻辑的函数,并给我们提供了如此多的灵活性:

// Array filter function from Swift standard library
func filter(includeElement: (T) -> Bool) -> [T]

let numbers = [1, 2, 4]

func isEven (x: Int) -> Bool {
    return x % 2 == 0
}
let res = numbers.filter(isEven)

通用目的

Swift 有一个非常强大的功能,称为 泛型。泛型允许你编写不提及特定类型的泛型代码。泛型对于构建算法、可重用代码和框架非常有用。解释泛型最好的方式是通过示例。让我们创建一个 minimum 函数,它将返回较小的值:

func minimum(x: Int, _ y: Int) -> Int {
  return (x < y) ? x : y
}

minimum(10, 11)
minimum(11,5, 14.3) // error

这个函数有一个限制;它只能与整数一起工作。然而,获取较小值的逻辑对所有类型都是相同的——比较它们并返回较小的值。这是一段非常通用的代码。

让我们的 minimum 函数变得通用,并支持不同的类型:

func minimum <T : Comparable>(x: T, _ y: T) -> T {
  return (x < y) ? x : y
}

minimum (10, 11)
minimum (10.5, 1.4)
minimum ("A", "ABC")

小贴士

Swift 的标准库已经实现了通用的 min 函数。请使用它而不是自己实现。

快速

Swift 被设计成快速且性能高,这是通过以下技术实现的:

  • 编译时方法绑定

  • 强类型和编译时优化

  • 内存布局优化

之后,我们将更详细地介绍 Swift 如何使用这些技术来提高性能。

Swift 互操作性

当苹果公司引入 Swift 时,他们考虑了两个主要点:

  • 使用 Cocoa 框架和建立的 Cocoa 模式

  • 易于采用和迁移

苹果公司理解这一点,并在开发 Swift 时非常重视。他们使 Swift 与 Objective-C 和 Cocoa 无缝协作。你可以在 Swift 中使用所有 Objective-C 代码,甚至可以在 Objective-C 中使用 Swift。

能够使用 Cocoa 框架非常重要。所有用 Objective-C 编写的代码都可以在 Swift 中使用,包括苹果框架和第三方库。

在 Swift 中使用 Objective-C

默认情况下,所有用 Objective-C 编写的 Cocoa 框架都在 Swift 中可用。你只需导入它们并使用它们。Swift 没有头文件;相反,你需要使用模块名称。你还可以以相同的方式包含你自己的 Swift 框架:

import Foundation
import UIKit
import Alamofire // Custom framework

设置

要包含你自己的 Objective-C 源文件,你首先需要进行一些小的设置。对于应用程序目标和框架目标,这个过程略有不同。主要思想是相同的——导入 Objective-C 头文件。

应用程序目标

对于应用程序目标,你需要创建一个桥接头。桥接头是一个普通的 Objective-C 头文件,在其中你指定 Objective-C 的 import 语句。

当你第一次将 Objective-C 文件添加到 Swift 项目中,或者相反,Xcode 将会弹出一个提示,为你创建并设置桥接头。这是添加桥接头的最佳和最便捷的方式。

应用程序目标

如果你拒绝了 Xcode 的帮助,你可以在任何时候自己创建桥接头。为此,你需要遵循以下步骤:

  1. 向项目中添加一个新的头文件。

  2. 前往 目标 | 构建设置

  3. 搜索 Objective-C Bridging Header 并指定步骤 1 中创建的桥接头文件路径。应用程序目标

一旦设置了桥接头,下一步就是向其中添加 import 语句:

Bridging.h

//
//  Use this file to import your target's public headers that you //  would like to expose to Swift.

#import "MyClass.h"

框架目标

对于框架目标,你只需将 .h Objective-C 头文件导入到框架的伞形头文件中。Objective-C 头文件必须标记为公开。伞形头文件是你指定公开 API 的头文件。通常,它看起来像这样——ExampleFramework.h 伞形头文件:

#import <UIKit/UIKit.h>

//! Project version number for MySwiftKit.
FOUNDATION_EXPORT double MySwiftKitVersionNumber;

//! Project version string for MySwiftKit.
FOUNDATION_EXPORT const unsigned char MySwiftKitVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <MySwiftKit/PublicHeader.h>

#import <SimpleFramework/MyClass.h>

调用 Objective-C 代码

完成设置后,你就可以在 Swift 中使用所有 Objective-C API。你可以创建实例、调用方法、从 Objective-C 类继承、遵守协议,以及执行你在 Objective-C 中能做的其他操作。在这个例子中,我们将使用 Foundation 类,但对于第三方代码,规则也是相同的:

import UIKit
import Foundation

let date = NSDate()
date.timeIntervalSinceNow

UIColor.blackColor()
UIColor(red: 0.5, green: 1, blue: 1, alpha: 1)

class MyView: UIView {
    //custom implementation
}

小贴士

只有在你需要时才从 Objective-C 类继承。这可能会对性能产生负面影响。

Swift 类型与 Objective-C Foundation 类型之间存在免费桥接。自动桥接发生在赋值时,以及当你将其作为参数传递给函数时:

let array = [1, 2, 3]

func takeArray(array: NSArray) { }

var objcArray: NSArray = array
takeArray(array)

从 Objective-C 转换到 Swift 类型需要显式的类型转换。有两种类型的转换:向下转换和向上转换。转换通常是一个不安全的操作,可能会失败,这就是为什么它返回一个可选类型:

//Upcasting or safe casting
let otherArray: [AnyObject] = objcArray as [AnyObject]

//Downcasting, unsafe casting
if let safeNums = objcArray as? [Int] {
  safeNums[0] + 10 //11
}

let string: NSString = "Hi"
let str: String = string as String

String 类型又向前迈进了一步。你可以在 Swift 的 String 类型上调用 Objective-C 基础方法,而无需任何类型转换:

var name: String = "Name"
name.stringByAppendingString(": Sara")

Swift 对 Objective-C 代码进行了一些小的改进,使其看起来更像 Swift 风格。最大的变化是在实例创建和初始化代码的风格上。initinitWith 和其他工厂方法被转换成了 Swift 初始化器:

//Objective-C

- (instancetype)initWithFrame:(CGRect)frame;
+ (UIColor *)colorWithWhite:(CGFloat)white alpha:(CGFloat)alpha;

// Swift 
init(frame: CGRect)
init(white: CGFloat, alpha: CGFloat)

另一项更改是针对 NS_ENUMNS_OPTIONS。它们变成了 Swift 的原生类型:enumRawOptionSetType

如你所见,API 看起来略有不同。因为 Swift 追求简洁,它从 API 命名法中删除了单词重复。其他方法调用、属性和名称与 Objective-C 中的相同,所以应该很容易找到并理解它们。

背后的操作是 Swift 生成特殊的接口文件来与 Objective-C 交互。你可以在我们的示例中通过按住 command 键并单击类型,例如 NSDateUIColor,来查看这些 Swift 接口文件。

在 Objective-C 中使用 Swift

还可以使用 Swift 在 Objective-C 中。这使得 Swift 很容易适应现有的项目。你可以从添加一个 Swift 文件开始,然后随着时间的推移将更多功能转移到 Swift。

设置过程比在 Swift 中包含 Objective-C 要简单得多。你所需要做的就是导入 Swift 的自动生成的头文件到 Objective-C。对于应用程序目标,文件命名约定是 ProductModuleName + -Swift.h,而对于框架,则是 <ProductName/ProductModuleName + -Swift.h>

看看下面的示例:

#import "SwiftApp-Swift.h"
#import <MySwiftKit/MySwiftKit-Swift.h>

你可以通过按住 command 键并单击来自检自动生成的文件的内容。默认情况下,Swift 类不公开用于 Objective-C。有两种方法可以使 Swift 类在 Objective-C 中可用:

  • 使用 @objc 属性标记 Swift 类、协议或枚举。

    你可以使用 @objc 属性来标记类、方法、协议和枚举。@objc 属性还接受 Objective-C 中使用的替代名称。当你通过标记 @objc 属性来暴露 Swift 类时,它必须继承自 Objective-C 类,并且枚举必须有一个原始的 Int 值:

    @objc(KOKPerson) class Person: NSObject {
      @objc(isMan) func man() -> Bool {
        ...
      }
    }
    @objc enum Options: Int {
      case One
      case Two
    }
    

    现在,带有 isMan 方法的 KOKPerson 类可以在 Objective-C 中使用。

  • 从 Objective-C 类,例如 NSObject 继承:

    当你从 Objective-C 类继承时,你的 Swift 类会自动在 Objective-C 中可用。在这种情况下,你不需要执行任何额外步骤。你还可以使用@objc属性标记它,并提供一个替代名称:

    class Person: NSObject {
    }
    

Swift 中不可用的功能

Swift 有一些功能在 Objective-C 中不可用,所以如果你计划从 Objective-C 中使用 Swift 代码,你应该避免使用它们。以下是这些功能的完整列表:

  • 结构体

  • 泛型

  • 元组

  • 枚举

  • 类型别名

  • 顶级函数

  • 偏函数

  • 全局变量

  • Swift 风格的变长参数

  • 嵌套类型

性能——含义和关键指标

代码有两个关键特性:

  • 代码质量:它必须坚固、灵活,并且具有良好的架构

  • 代码性能:它必须快速

使代码架构非常坚固和稳定是最重要的任务,但我们也不应该忘记让它变得快速。实现高性能可能是一个棘手且危险的任务。以下是一些你在进行性能改进时应该记住的几点:

  • 不要一开始就优化你的代码

    关于这个话题有很多文章,讨论了它的危险性和为什么你不应该这样做。只是不要这样做,正如唐纳德·克努特所说:

    "过早优化是万恶之源"

  • 先测量

    首先,不要一开始就优化,其次,先测量。测量代码的性能特征,只优化那些慢的部分。几乎 95%的代码不需要性能优化。

    我完全同意这些观点,但我们应该提前考虑另一种类型的性能优化。

每日代码性能

我们每天所做的微小决定包括以下内容:

  • 它应该是什么类型,Int还是String

  • 我应该为新的功能创建一个新的类,还是添加到现有的类中?

  • 使用数组?或者也许是一个集合?

看起来这些似乎对应用程序的性能没有影响,在大多数情况下,它们确实没有。然而,做出正确的决定不仅可以提高应用程序的速度,还可以使其更加稳定。这为应用程序开发提供了更高的性能。我们每天所做的微小改变,到年底时会产生重大影响。

性能的重要性

高性能非常重要。应用程序的性能直接关系到用户体验。用户希望立即得到结果;他们不希望等待视图加载,看到长时间的加载指示器,或者看到卡顿的动画。

每年,我们的计算机和设备变得越来越强大,拥有更快的 CPU 速度、更多的内存、更多的存储和更快的存储速度。由于这一点,性能问题可能看起来并不相关,但软件的复杂性也在增加。我们需要存储和处理更复杂的数据。我们需要显示动画和做很多其他事情。

解决性能问题的第一种方法是通过增加更多功率。我们可以添加更多服务器来处理数据,但我们无法更新客户的 PC 和移动设备。此外,增加更多功率并不能解决代码性能问题本身,而只是暂时推迟了它。

第二个,也是正确的解决方案,是移除导致性能问题的原因。为此,我们需要识别问题,找出代码中的慢速部分,并对其进行改进。

关键指标

有许多因素会影响应用程序的性能和用户体验。我们将涵盖以下关键指标:

  • 操作的性能速度

  • 内存使用

  • 磁盘空间使用

其中最重要的是操作的性能速度,它告诉我们特定任务可以执行得多快,例如,创建新用户、从文件中读取、下载图片、搜索具有特定名称的人等等。

摘要

Swift 是一种强大且快速的编程语言。在本章中,你了解了 Swift 的许多强大功能,以及如何轻松地在 Swift 中开始编码并将其集成到现有项目中。我们还讨论了为什么性能很重要,以及在使用它时应考虑哪些因素。

在下一章中,我们将使用 Swift 进行更多编码,你将学习如何使用 Swift 的所有功能来构建良好的应用程序架构。

第二章:在 Swift 中构建良好的应用架构

Swift 是一种高性能编程语言,正如你在上一章所学。你还了解到,编写良好的代码甚至比编写高性能代码更重要。在本章中,我们将结合 Swift 的所有强大功能来创建一个应用。我们将通过以下主题来实现这一点:

  • 编写干净的代码

  • 不可变性

  • 值类型和不可变性

  • 使用类表示状态

  • 使用可选表示值的缺失

  • 函数式编程

  • 泛型

创建 Swift 应用

创建良好应用架构的第一步是创建应用本身。我们将创建一个 iOS 日记应用,用于制作日常笔记。我们不会涵盖任何 iOS 特定主题,因此你可以使用相同的代码创建 OS X 应用。

开始吧!打开 Xcode 并创建一个新的 iOS 单视图项目应用。现在,我们准备好编码了。

首先,让我们创建一个Person类型,用于日记的所有者,以及一个日记条目类型。我们将使用Class类型来创建PersonJournalEntry。这两个类都非常简单——只是一系列属性和一个初始化器:

class Person {
  var firstName: String
  var lastName: String

  init (firstName: String, lastName: String) {
    self.firstName = firstName
    self.lastName = lastName
  }
 }

class JournalEntry {
  var title: String
  var text: String
  var date: NSDate

  init (title: String, text: String) {
    self.title = title
    self.text = text
    date = NSDate()
  }
}

这是我们需要为应用设置的最小环境。在我们继续前进之前,让我们让代码变得更好。

变量和常量的区别

可能是所有编程语言中最常用的功能就是创建和存储值。我们在函数中创建局部变量,并在类和其他数据结构中声明它们;这就是为什么正确地做这件事非常重要。

在 Swift 中,有两种创建和存储值的方式,如下所示:

  • 将其改为变量:

    var name = "Sara"
    
  • 将其改为常量:

    let name = "Sara"
    

变量和常量的区别在于,常量值只能分配一次,并且之后不能更改。另一方面,变量值可以随时更改。以下是一个例子:

var name = "Sam"
name = "Jon"

let lastName = "Peterson"
lastName = "Jakson" //Error, can't change constant after assigning

小贴士

金规则是始终首先将你的类型声明为常量(如前例中的let关键字)。只有在你之后需要它时,才将其改为变量(var关键字)。

有一些例外情况,你不能将其声明为常量,例如,在创建@IBOutlesweak时。另外,可选值必须声明为变量。

使用常量比使用变量有许多好处。常量是一种不可变类型,我们将在后面讨论所有不可变性的好处。最重要的两个好处如下:

  • 安全性(防止意外值变化)

  • 更好的性能

你应该在声明属性时以及作为函数中的局部常量时使用常量。我们应该应用这条规则,并按照以下方式更改我们的PersonJournalEntry类:

class Person {
  let name: String
  let lastName: String
...
}

class JournalEntry {
  let title: String
  let text: String
  let date: NSDate
...
}

通常,你会发现自己在变量上使用得比常量多。让我们看看一个你可能认为需要使用变量,但实际上常量会是更好的解决方案的例子。假设你在应用程序中创建了一个新的人,现在你想显示带有性别前缀的全名:

let person = Person(firstName: "Jon", lastName: "Bosh")
let man = true

var fullName: String
if man {
  fullName = "Mr "
} else {
  fullName = "Mrs "
}

fullName += person.firstName
fullName += " "
fullName += person.lastName

如果你再深入思考一下这个问题,你就会意识到人的fullName应该是不可变的;它不会改变,应该声明为常量:

let person = Person(firstName: "Jon", lastName: "Bosh")
let man = true

let gender: String = man ? "Mr": "Mrs"
let fullName = "\(gender) \(person.firstName) \(person.lastName)"

不可变性

在上一节中,你学习了使用不可变常量的重要性。Swift 中有更多不可变类型,你应该利用它们并使用它们。不可变性的优势如下:

  • 它消除了与意外值更改相关的大量问题

  • 它是安全的线程访问

  • 它使代码推理更容易

  • 性能有所提升

通过使类型不可变,你增加了额外的安全级别。你拒绝了对实例进行修改的访问。在我们的日记应用中,一旦创建了实例,就无法更改一个人的名字。如果有人意外地决定将新值分配给人的firstName,编译器将显示错误:

var person = Person(firstName: "Jon", lastName: "Bosh")
p.firstName = "Sam" // Error

然而,有些情况下我们需要更新变量。一个例子可以是数组;假设你需要向其中添加一个新项目。在我们的例子中,也许这个人想在应用中更改昵称。有两种方法可以实现这一点,如下所示:

  • 修改现有实例

  • 使用更新信息创建新实例

在原地修改实例可能导致危险且不可预测的效果,尤其是在你正在修改引用实例类型时。

注意

类是引用类型。“引用类型”意味着许多变量和常量可以引用相同的实例数据。对实例数据的更改会反映在所有变量中。

创建一个新实例是一个更安全的操作。它不会对系统中的现有实例产生影响。在我们创建了新实例之后,可能需要通知系统中的其他部分关于这一变化。这是一种更安全的更新实例数据的方式。让我们看看我们如何在Person类中实现昵称更改。首先,让我们向Person类添加一个昵称:

class Person {
  let nickName: String
…

func changeNickName(nickName: String) -> Person  {
    return Person(firstName: firstName, lastName: lastName,
                   nickName: nickName)
  }
}

let sam = Person(firstName: "Sam", lastName: "Bosh", 
  nickName:"sam")
let rockky = sam.changeNickName("Rockky")

因为我们将sam实例变成了常量,所以在更改nickName之后,我们无法为其分配新值。在这个例子中,最好将其改为变量,因为我们实际上需要更新它:

var sam = Person(firstName: "Sam", lastName: "Bosh", 
  nickName:"BigSam")
sam = sam.changeNickName("Rockky")

多线程

现在,我们越来越多地使用核心处理器,与多线程工作已成为我们生活的一部分。我们有 GCD 和 NSOperation 来在多个线程上执行工作。

多线程的主要问题是同步对数据的读写访问,而不会破坏数据。作为一个例子,让我们创建一个日记条目的数组,并尝试在后台和主线程中修改它。这将导致应用程序崩溃:

class DangerousWorker {
  var entries: [JournalEntry]

  init() {
    //Add test entries
    let entry = JournalEntry(title: "Walking", text: "I was 
      walking in the loop")
    entries = Array(count: 100, repeatedValue: entry)
  }

  func dangerousMultithreading() {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
      sleep(1) //emulate work
      self.entries.removeAll()
    }

    print("Start Main")
    for _ in 0..<entries.endIndex {
      entries.removeLast() //Crash
      sleep(1) //emulate work
    }
  }
}

let worker = DangerousWorker()
worker.dangerousMultithreading()

这些问题实际上很难找到和调试。如果你移除sleep(1)延迟,某些设备上可能不会发生崩溃,这取决于哪个线程先运行。

当你使你的数据不可变时,它变为只读,所有线程可以同时读取它而没有任何问题:

let entries: [JournalEntry]

let entry = JournalEntry(title: "Walking", text: "I was walking")
entries = Array(count: 100, repeatedValue: entry)
// entries is immutable now, read-only

dispatch_async(dispatch_get_global_queue(
              DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {

  for entry in self.entries {
    print("\(entry) in BG")
  }
}

for entry in self.entries {
  print("\(entry) in BG")
}

但我们经常需要更改数据。而不是直接更改源数据,更好的解决方案是创建新的、更新的数据并将结果传递给调用线程。这样,多个线程可以安全地继续执行读取操作。我们将在第六章中查看多线程数据同步,为高性能构建应用程序

值类型和不可变性

Swift 中有两种不同的数据类型:

  • 引用类型

  • 值类型

让我们来看看这些。

引用类型

类是一个引用类型。当你创建一个引用类型的实例并将其赋值给变量或常量时,你不仅赋值了一个值,还赋了一个指向值的引用,该值位于别处(实际上它位于堆内存中)。当你将这个引用传递给其他函数并将其赋值给其他变量时,你创建了多个指向相同数据的引用。如果这些变量中的任何一个改变了数据,这种变化也会反映在其他所有变量中。以下是一个展示这一点的示例:

let person = Person(firstName: "Sam", lastName: "Jakson")
let a = person, b = person, c = person

以下图表显示了这段代码的内存结构:

引用类型

所有四个常量都会引用同一个对象。这种架构的危险在于,如果其中一个常量更新了实例数据的一部分,其他所有常量也会被更新。以下是一个展示这一点的示例:

a.firstName = "Jaky"
b.firstName // Jaky

有时,这种行为可能是期望的,例如,当许多变量引用同一个窗口对象时。应该只有一个窗口对象,并且在一个地方做出的更改应该在其他地方也得到反映。

值类型

相反,结构体是一个值类型。当你创建一个值类型的实例并将其赋值给变量时,你是在分配实际数据。当你将这个实例传递给其他函数和变量时,你是在传递这个值的副本。复制是自动进行的。你可能认为复制值会对性能产生负面影响,但事实上,值类型比引用类型提供更高的性能。值类型足够智能,只在需要时(当数据被修改时)优化数据复制。

如果我们将Person类型改为Structure类型,相同的代码示例将看起来像这样:

let person = Person(firstName: "Sam", lastName: "Jakson")
let a = person, b = person, c = person

这个结构体常量的内存看起来会是这样:

值类型

这种架构的优势在于你的代码组件是隔离的,并且不相互依赖。

引用类型和值类型之间的一大区别如下所述:当你将一个常量作为引用类型创建时,你创建了一个常量引用(这意味着你不能将其更改为指向另一个实例)。但你可以更改实例本身中的数据,就像我们在示例中通过更新人的firstName所做的那样。

当你创建一个值类型的常量时,你得到一个不能更改的常量值。

结构体的力量

如果你更仔细地查看 Swift 标准库类型定义,你会发现大多数类型都是作为结构体实现的,例如struct Intstruct Stringstruct Array以及其他。

结构体不仅是一个简单快速的数据结构,而且是一个非常强大的结构体。结构体可以有方法、属性和初始化器,并且可以遵循协议。当你设计应用程序中的实体时,尽量使用结构体作为你的数据模型,通常结构体是首选。现在我们将应用这个建议,并将我们的类型更改为使用结构体而不是类:

struct Person {
  let firstName: String
  let lastName: String
  let nickName: String

  func changeNickName(nickName: String) -> Person  {
    return Person(firstName: firstName, lastName: lastName,nickName: nickName)
  }
}

extension Person {

  init(firstName: String, lastName: String) {
    self.init(firstName: firstName, lastName: lastName,       nickName:"")
  }
}

第一个区别是我们将class关键字更改为struct。第二个区别更有趣——我们移除了init方法。如果你没有定义初始化器,结构体会提供一个默认的成员变量初始化器。成员变量初始化器会取结构体的所有属性。如果你需要除了默认的成员变量初始化器之外的额外初始化器,你可以在扩展中创建它。这样,你将有两个初始化器:

  Person(firstName: "Sam", lastName:"Niklson", nickName: "Bigsam")
  Person(firstName: "Petter", lastName: "Hanson")

使用类表示状态

在设计应用程序中的数据模型时,使用值类型。值类型应该是:

  • 惰性

  • 隔离

  • 可交换

值类型不应该有行为,也不应该有副作用。数据上的操作应该进入值层。你可以在 Andy Matuschak 的演讲Controlling Complexity in Swift中了解更多关于使用值类型设计数据模型的信息,演讲链接为realm.io/news/andy-matuschak-controlling-complexity/

相反,类可以有行为和状态。创建一个新的JournalEntry的动作是一个行为,例如,它应该在类类型中实现。当前用户的JournalEntry列表是一个状态,这也应该存储在类类型中:

  1. 首先,我们创建一个Journal数据模型作为值类型。它包含数据和操作来处理这些数据(它有addEntry方法,该方法创建并添加新条目到日记中):

    struct Journal {
      let owner: Person
      var entries: [JournalEntry]
    
      mutating func addEntry(title: String, text: String) {
        let entry = JournalEntry(title: title, text: text)
        entries.append(entry)
      }
    }
    
    extension Journal {
      init(owner: Person) {
        self.owner = owner
        self.entries = []
      }
    }
    
  2. 下一步是创建一个作为引用类型的控制器实体,它将在应用程序中保存当前的日记状态并处理添加新条目的动作:

    class JournalController  {
      var journal: Journal
    
      init(owner: Person) {
        self.journal = Journal(owner: owner)
      }
    
      func addEntry(title: String, text: String) {
        journal.addEntry(title, text: text);
      }
    }
    

使用可选值表示值的缺失

让我们回到过去,看看 Objective-C 中如何表示值的缺失,作为一个例子。对于引用类型和简单值类型,没有标准的解决方案来表示值的缺失。有两种不同的方式:

  • nil

  • 0-1INT_MAXNSNotFound等等

对于引用类型,Objective-C 使用nil值来表示变量没有值。它指向任何地方。

对于值类型,不存在nil这样的值,也无法将nil赋值给整型变量。为了实现这一点,Objective-C(以及不仅仅是 Objective-C,还包括 C、Java 和许多其他语言)使用了一些不太可能由特定操作产生的特殊值。例如,NSArrayindexOfObject方法会返回NSNotFound

注意

NSNotFound只是一个常量,其值等于NSIntegerMax,其值又等于2147483647

Swift 使用可选值以统一的方式表示值类型和引用类型中的值缺失。可选值是一种注释方式,表明值可能不存在。你可以通过两种方式将类型声明为可选:

  • 使用可选关键字,即Optional<Type>

  • 通过在类型末尾添加一个问号,即Type?

    提示

    Type?是声明可选类型的首选方式。

要表示缺失的值,你可以简单地给可选值赋值nil,如下面的示例所示:

var view: Optional<UIView>
var index: Int?

view = nil
view = UIView()

index = 10
index = nil

可选类型和非可选类型

在 Objective-C 中,可选值和非可选值都由相同的类型表示,例如NSInteger, NSString *。通过查看源代码和方法定义,无法确定一个方法是否可以返回nilNSNotFound

- (NSUInteger)indexOfObject:(id)anObject;
+ (instancetype)stringWithString:(NSString *)string;

注意

在 Xcode 6.3 中,我们有了新的 Objective-C 注解nullablenonnull,允许我们指定是否可以传递nil。这些注解是在 Swift 发布后添加的,以提供更好的 Objective-C 与 Swift 的集成。

Swift 对此更为严格。它既有可选类型也有非可选类型。非可选类型的两个例子是IntString。这意味着你不能将nil赋值给Int变量或传递nil给具有Int参数类型的函数。另一方面,可选类型允许你使用nil

var index: Int?
var number: Int = 10

index = nil // Ok
number = nil // Error

func indexOfObject(object: Any) -> Int?
func stringWithString(string: String?) -> String?

这项严格的规定使代码的意图非常明确。从 API 中可以看到,要调用indexOfObject函数,需要传递一个非可选参数,并且它可能返回nil作为结果。

安全的nil处理

另一个问题在于尝试访问nil值。如果你使用过 C、Java 或 Objective-C 编程,你肯定遇到过NullPointerException异常或NSInvalidArgumentException异常。

通常,访问未初始化的内存是不安全的。例如,在 Objective-C 中将nil传递给initWithString方法会导致NSInvalidArgumentException异常,甚至可能引发应用程序崩溃:

[NSString initWithString: nil] – crash

令人遗憾的是,Objective-C 没有检查String * typenil之间的区别。

Swift 中的可选类型不仅清楚地说明了使用nil的能力,而且使工作非常安全,避免了崩溃。

使用可选类型

现在你已经了解了为什么可选类型被发明出来的背景,让我们继续在我们的应用程序中使用它们。我们有一个JournalEntry实体,假设用户可以添加一个创建此条目的位置(这是一个可选功能;一些条目会有,而一些则不会有)。我们需要创建一个新的类型来存储地理位置,并给我们的JournalEntry实体添加一个新的Optional属性:

struct Location {
  let latitude: Double
  let longitude: Double
}

struct JournalEntry {
  var location: Location?
...
}
var entry = JournalEntry(title: "Walking", text: "I was walking in the loop")
let location = Location(latitude: 37.331686, longitude: -122.030656)
entry.location = location

可选变量默认被分配一个nil值,因此我们不需要对我们的init方法做任何更多修改(所有属性都已提供了值)。

小贴士

var: Int? = nilvar: Int?相同。如果你在声明可选变量,不要分配一个nil值。

当你尝试访问一个可选值时,这会变得更有趣。可选类型就像一个封闭的盒子,里面装着东西。要从中取出值,你必须先打开它。要检查一个可选值内部是否有值,使用if and == nil!= nil比较运算符。要从盒子中获取实际数据,你需要使用! sign来解包它:

if entry.location != nil {
  showLocation(entry.location!)
} else {
  //locationNotAvailable
}

然而,这不是从可选类型中获取值的最佳方式。更好的方法是使用可选绑定运算符,它会检查可选值是否存在,并一次性解包其值。语法是if let unwrappedValue = optional

let location = entry.location
if let location = location {
  showLocation(location)
} else {
  //locationNotAvailable
}

首先,我们将位置提取到一个局部常量中,它具有Optional<Location>类型。接下来,我们应用可选绑定运算符,并将选项中的一个值获取到一个位置常量中。可选值的常量名称与可选名称相同(在这个例子中是location)。这种技术被称为名称遮蔽。当你将位置作为showLocation函数的参数时,你正在使用解包后的常量值。

小贴士

当使用可选绑定运算符时,使用名称遮蔽。这使得代码更加易读。以下是未使用名称遮蔽的代码示例:

if let location = maybeLocation {
  showLocation(location)
}

为可选及其解包后的值(在我们的例子中是maybeLocationlocation)使用不同的名称会使代码更加混乱。

在 Swift 中,还有一种可选类型可用——隐式解包可选类型。你用Type!来声明它们,例如Int!。隐式解包可选类型是一种不需要检查可选内部是否存在值的类型,但允许你像它不是可选类型一样访问数据。以下是一个例子:

var name: String! = "Jon"
print("My name is" + name)

name = nil
print("My name is " + name) // Crash

使用隐式解包的可选类型是不安全的,并且不建议使用。只有极少数情况下你应该使用它们。它们主要用于与 Objective-C API 交互。这是因为 Objective-C 中的许多类型在转换为 Swift 时被转换成了隐式解包的可选类型。

小贴士

避免使用隐式解包的可选类型。

对可选类型的总结

如果你对于可选类型的概念感到陌生,请不要害怕;你会习惯与它们一起工作,并且你会喜欢它们。以下是一些关于这个主题你应该记住的小提示:

  • 在可能的情况下,尽量不要使用隐式解包的可选类型——几乎永远不要!

  • 在访问可选类型之前检查它是否有值

  • 使用可选绑定和可选变量名的阴影来访问值

  • 设计你的 API 的意图,使其对可选和非可选类型清晰可见

函数式编程

在函数式编程范式中,一个函数有一个类型,并且它被以与其他类型相同的方式处理,例如 IntStringClass。函数可以被分配给变量,作为另一个函数的参数传递,也可以作为结果类型从函数中返回。主要目标是把代码拆分成小的、独立的函数。完美的函数没有副作用,并且只操作传递给它的参数。

在函数式编程风格中,你描述的是你想做什么,而不是你想怎么做

函数式编程非常适合数据转换和数据操作。这是因为你可以将代码拆分成更小的部分。你经常可以重用一些常规的样板代码。

函数类型

每个函数都有一个类型。函数的类型由其参数类型和返回类型组成。现在,我们将创建一些具有不同类型的函数并执行一些操作:

func hello() {
    print("Hello")
}

func add(x: Int, y: Int) -> Int {
    return x + y
}

func subtract(x: Int, y: Int) -> Int {
    return x - y
}

var hi: () -> () = hello
var mathOperation: (Int, Int) -> Int

mathOperation = add 
mathOperation(10, 11) // 21
mathOperation = subtract
mathOperation(10, 11) // -1

hi()
mathOperation = hello // Error, wrong types

hello 函数具有 () -> () 类型。它不接受任何参数也不返回任何内容。addsubtract 函数有不同的类型:(Int, Int) -> Int。在前面的示例代码中,我们将函数分配给了 himathOperation 本地变量。

无法将 hello 函数分配给 mathOperation 变量,因为它们的类型不同。

拆分代码

因为我们可以将一个函数传递给另一个函数,所以我们可以将代码拆分成实际的逻辑和常规工作。让我们实现一个非常常见的操作。任务是数组中每个元素的加倍。在命令式编程中,这个任务将转化为遍历数组中的每个元素,加倍每个元素,并将结果保存到一个新数组中。最后,返回一个所有元素都加倍的结果数组:

let numbers = [1, 2, 3]

func doubleNumbers(array: [Int]) -> [Int] {

  var result = [Int]()
  for element in numbers {
    result.append(element * 2)
  }
  return result
}

let result = doubleNumbers(numbers) // [2, 4, 6]

这段代码的问题在于,只有一行实际执行了工作,那就是 element * 2。它不能被重用,因为这种逻辑被硬编码在函数体中。如果我们想将数字乘以三或进行其他转换怎么办?以下是这个任务以函数式方式实现的示例:

func transform(array: [Int], f: Int -> Int) -> [Int] {

  var result = [Int]()
  for element in array {
    result.append(f(element))
  }
  return result
}

这里的唯一区别是 transform 函数接受一个转换函数作为参数。转换函数执行所有常规工作,遍历数组,但它将实际转换逻辑留给作为参数传递的函数执行。这样,你可以向 transform 函数传递不同的函数:

func double(x: Int) -> Int {
    return x * 2
}

func triple(x: Int) -> Int {
    return x * 3
}

let result = transform(numbers, f: double)
let result = transform(numbers, f: triple)

闭包表达式

闭包表达式是一个内联、无名称且自包含的代码块。你可以将闭包表达式视为一个没有名称的函数;它也接受参数,有主体和返回类型。如果你可以使用闭包,那么你可以用闭包代替函数。

闭包表达式的通用语法如下:

{ (parameter name: type) -> return type in  body }

让我们重构我们的 transform 函数以使用闭包。以下是结果:

transform(numbers, f: { (x: Int) -> Int in
  return x * 2
})

由于闭包表达式旨在内联使用,它们有许多语法优化,使它们简洁明了。以下是一些这些优化的例子:

  • 类型推断

  • 隐式返回类型

  • 简写参数名

  • 尾随闭包语法

类型推断

多亏了类型推断,你不需要指定参数类型和返回类型,如下所示:

transform(numbers, f: { x in
  return x * 2
})

小贴士

作为一般规则,你应该在可能的情况下避免指定类型。

隐式返回类型

闭包具有单表达式主体时,会隐式返回该表达式的结果。在这种情况下可以省略 return 关键字:

transform(numbers, f: { x in x * 2 })

在这个例子中,我们不能省略 return 关键字,因为有多个表达式:

transform(numbers, f: { x in
  let result = x * 2
  return result + 10 
})

简写参数名

你可以从闭包表达式中省略参数名称。在这种情况下,Swift 为每个参数提供了一个默认名称。这个名称由 $ 符号和参数索引组成,例如,$0$1$2 等等:

transform(numbers, f: { $0 * 2 })

小贴士

对于非常短的闭包表达式,其中参数只使用一次或两次,简写参数名是首选。在其他情况下,给你的参数一个描述性的名称。

尾随闭包语法

当一个函数的最后一个参数是闭包时,你可以将闭包表达式写在函数调用之外:

transform(numbers) { $0 * 2 }

你可以使用所有带有尾随闭包的闭包语法:

transform(numbers) { x in x * 2 }

如果一个函数只有一个参数且它是一个闭包,你不需要在函数调用时指定空括号:

func map(function: Int -> Int) -> [Int] {
  ...
}
map() { $0 * 2 }
map { $0 * 2 }

标准库

Swift 的标准库中有许多接受其他函数的函数和方法。以下是一些你应该了解并使用的 SequenceType 方法:

  • map

  • reduce

  • filter

  • sort

map 方法

map 方法将一个 transform 函数应用到每个元素上,并返回新的结果集合。这个过程称为映射,其中值 A 映射到 B:

func map(transform: (Int) -> String) -> [String]

注意

Swift 的标准库使用泛型函数,但在以下示例中,它们已被更改为实际类型以提供更简单的示例。以下是 map 函数的实际定义:

func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]

map 方法与我们的 transform 函数执行完全相同的任务。因此,你应该使用 map

let result = numbers.map(double)
let result = numbers.map { $0 * 2 }

可选值的 map 操作

Optional 类型也有 map 方法,但在这里它的工作方式不同。它接受一个函数,该函数将可选值映射到另一个值(如果存在):

func map<U>(@noescape f: (Wrapped) -> U) -> U?

这个 map 方法的主体看起来会是这样:

func map(f: (Wrapped) -> Double) -> Double? {
  switch self {
    case .None: return nil
    case .Some(let x): return f(x)
  }
} 

let number: Int? = 10
let res = number.map { Double($0) * 2.3 }

使用 map 与可选值可以使你的代码更简洁。考虑以下示例,它使用了 map 函数和手动解包可选值:

// Using the map function 
let doubled = number.map(double)

// Optional binding 
let doubled: Int?
if let number = number {
  doubled = double(number)
} else {
  doubled = nil
}

reduce 方法

reduce 方法接受初始值和 combine 函数。它通过为序列中的每个元素调用 combine 函数来聚合结果。combine 函数接受前一次调用 combine 函数返回的值或第一次调用的初始值以及集合中的一个元素:

func reduce(initial: Double, combine: (Double, Int) -> Double) -> Double

reduce 函数最简单的用例可能是计算几个元素的求和。它的实现看起来像这样:

{
  var result = initial
  for item in self {
    result = combine(result, item)
  }
  return result
}

let sum = numbers.reduce(0) { acc, number in acc + number }

你可以通过使用闭包、简写参数名或操作符函数来使这段代码更简洁:

numbers.reduce(0) { $0 + $1 }
numbers.reduce(0, combine: +)

注意

+ 操作符被定义为操作符函数,可以在需要函数的任何地方使用。

infix operator + {
    associativity left
    precedence 140
}
func +(lhs: Int, rhs: Int) -> Int

过滤方法

filter 方法通过询问 includeElement 函数哪些元素需要保留来从源集合中过滤元素。includeElement 函数会对源集合中的每个元素进行调用,并返回一个布尔值,表示该元素是否应该保留或删除:

func filter(includeElement: (Int) -> Bool) -> [Int]

实现看起来像这样:

{
  var filtered = [Int]()
  for item in self {
    if includeElement(item) {
      filtered.append(item)
    }
  }
  return filtered
}

let evenNumbers = numbers.filter { $0 % 2 == 0 }

函数式编程是一个非常广泛的话题。如果你感兴趣,你可以在 Chris Eidhof、Florian Kugler 和 Wouter Swierstra 的《Swift 函数式编程》中了解更多信息。你可以从 www.objc.io/books/ 获取它。

泛型

泛型是一种编写通用、可重用代码的方式,而不需要指定类型。你可以编写一个可能不限于一种类型的 generic 函数。你可以创建 generic 函数以及添加类型限制的 generic 类型。即使你没有注意到,你在这本书中已经使用了 generic 类型。

泛型背后的主要思想是,而不是指定一个类型,你使用一个泛型类型占位符。泛型是消除代码重复和使代码可重用的强大工具。

第一步是确定可以泛化的代码。最好的方法是问,“这个功能是否仅限于这种类型?”如果你意识到它不是,你应该考虑将其泛化。

提示

只有在你需要这样做并且你打算用不同的类型使用它们时,才使函数泛化。使它们泛化可能会对性能产生轻微的负面影响。

让我们创建我们的第一个简单的泛型函数。我们的 printMe 函数目前只能处理整数,但让它能够处理所有类型会更好:

func printMe(x: Int) {
  print("Me - \(x)")
}

要获取一个泛型函数或类型,你需要在尖括号 (<T>) 中指定一个泛型类型参数,并使用该类型而不是实际类型:

func printMe<T>(x: T) {
  print("Me - \(x)")
}

printMe(10.0)

提示

类型参数的命名约定是驼峰式。在简单情况下,当泛型类型没有特殊含义时,使用单字符名称 T。在复杂情况下,你可以给出描述性的名称,例如 KeyValue

泛型函数

我们编写的 transform 函数是泛型函数的绝佳候选者。它不执行任何需要特定类型的计算。我们唯一需要做的是为数组类型和转换函数使用占位符类型名称,而不是 Int 类型:

func transform<T>(array: [T], function: T -> T) -> [T] {

  var result = [T]()
  for element in array {
    result.append(function(element))
  }
  return result
}

现在我们可以使用我们的转换函数与任何类型:

let numbers = [1, 2, 3]
let increasedNumbers = transform(numbers) { $0 + 1}

let names = ["Jon", "Sara", "Sam"]
let formattedNames = transform(names) { "Name: " + $0 }

类型约束

你不能对泛型类型 T 的变量执行任何操作,因为 Swift 对该类型一无所知。如果你尝试比较两个类型为 T 的参数,Swift 将显示以下错误:找不到接受提供的参数的重载 '<'

func minElem<T>(x: T, _ y: T) -> T {
  return x < y ? x : y
}

比较运算符 < 在可比较协议中定义。我们需要指定我们的泛型类型 T 应该遵守可比较协议。使用类型约束,你可以指定一个类型必须遵守一个协议或继承自一个类。要做到这一点,你需要在泛型名称定义后的冒号(:)后面列出约束:

<type parameter : constraint >

func minElem<T : Comparable>(x: T, _ y: T) -> T {
  return x < y ? x : y
}

现在我们的 minElem 函数可以与任何遵守可比较协议的类型一起工作,例如 IntString

minElem(10, 20)
minElem("A", "B")

minElem 制作为一个具有 constraint 协议的泛型函数的伟大之处在于它不仅限于仅与现有类型一起工作。我们不需要对其进行任何更改即可使其与新类型一起工作。假设我们想找到最小的 JournalEntry 实体。我们唯一需要做的是确保它遵守可比较协议。

注意

可比较协议要求在你的类型中实现两个函数运算符:==<

func ==(lhs: Self, rhs: Self) -> Bool
func <(lhs: Self, rhs: Self) -> Bool

假设我们想找到最小的 JournalEntry 实体。我们唯一需要做的是确保它遵守可比较协议:

extension JournalEntry : Comparable {
}

func == (lhs: JournalEntry, rhs: JournalEntry) -> Bool {

  return lhs.title == rhs.title &&
    lhs.text == rhs.text &&
    lhs.date == rhs.date
}

func < (lhs: JournalEntry, rhs: JournalEntry) -> Bool {
  return lhs.text < rhs.text
}

小贴士

在类型扩展中遵守协议。这样,你可以将代码分成功能部分。

当你在类型声明中遵守协议时,类型声明变得难以阅读,并且包含太多信息:

struct JournalEntry : Comparable, Hashable, CustomStringConvertible { 
...
}

现在,我们可以创建两个 JournalEntry 实体并调用一个 minElem 函数。minElem 函数将使用 < 运算符函数来比较两个日志条目:

let walking = JournalEntry(title: "Walking", text: "It was a great weather")
let goal = JournalEntry(title: "Read", text: "Read a book")
let smaller = minElem(walking, goal)

泛型类型和集合

泛型另一个很好的用途是创建泛型类型。ArrayDictionarySet 都被实现为泛型类型:

struct Array<T> ...
struct Dictionary<Key : Hashable, Value> ...
struct Set<T : Hashable> ...

这使我们能够将任何类型存储在集合中,并使它们成为单类型集合。这意味着我们无法将错误类型存储在其中:

var numbers = [1, 2, 3] // [Int]
numbers.append(10)
numbers.append("Name") //Error, Can't add String to [Int] array

你可以创建自己的自定义泛型类型。规则与声明泛型函数相同;你指定一个泛型类型在尖括号中,并在任何地方将其用作类型名称。例如,我们可以创建自己的简单泛型栈,如下所示:

struct Stack<T> {
  private var items: [T]

  mutating func push(item: T) {
    items.append(item)
  }

  mutating func pop() -> T {
    return items.removeLast()
  }

  init() {
    items = []
  }
}

var s = Stack<Int>()
s.push(10) // 10
//s.push("Name") // Error
s.push(4)  // 10, 4
s.pop()    // 10

安全性

Swift 是为了安全而设计的。它消除了许多编译时的问题。以下是一些 Swift 为你处理的事情列表:

  • 类型安全:Swift 是一个非常强类型的语言。如果一个函数有Int参数,你必须在使用时传递Int作为参数。这个规则也适用于运算符。Swift 不允许使用错误类型:

    func increase(x: Int) -> Int {
      return x + 1
    }
    
    let x = 10
    let percent = 0.3
    let name = "Sara"
    
    x + name //Error, can't apply + operator for Int and String
    x * percent //Error, can't apply * to Int and Double
    Double(x) * percent // 3
    
    increase(x) // 11
    increase(percent) // Wrong type
    increase(name) // Wrong type
    
  • 变量必须在使用前初始化:访问未初始化的内存是一种危险操作。Swift 以非常优雅和安全的方式处理这个问题。当你尝试这样做时,它不会编译:

    var y: Int
    //y + 10 //Error, variable 'y' used before being initialized
    y = 1
    y + 10
    

    常量值在设置后不能更改,但你可以在声明常量时不设置初始值,稍后设置它:

    let z: Int
    
    if y == 2 {
      z = 10
    } else {
      z = 0
    }
    z + 10
    

    如果你移除了else分支,Swift 编译器将会显示错误,因为在这些情况下,当y != 2时,z将不会被初始化。

  • 安全的nil处理:正如你所看到的,Swift 有一个可选类型用于安全的nil处理和值不存在处理。

危险操作

仍然有一些情况我们需要小心,因为我们可能会犯错并导致应用程序崩溃。以下是一些情况列表:

  • 隐式解包的可选值:解包可选值(!运算符)是一个可能危险的操作。你应该只在确认可选值有值时进行解包。实际上,使用可选绑定会更好。

    使用隐式解包的可选值也是一个非常危险的操作。它们的行为与非可选类型(在访问值之前不需要解包)类似,但与nil值一起使用时会导致崩溃:

    var x: Int?
    x! + 10 // Crash! Unwrapping optional that does not have value.
    
    var y: Int!
    y + 10 // Crash! Implicitly unwrapped optional has nil value.
    
  • 类型转换:在某些情况下,你可能希望存储基类对象的任何对象,并在稍后检查该对象是否具有特定的类型。你可以使用is关键字安全地检查对象的类型:

    var view: UIView = UIImageView()
    if view is UIImageView {
      print("yes")
    } else {
      print("no")
    }
    

    通常,你不仅需要检查变量是否为某种类型,还需要将其转换为相应的类型。你可以通过两种方式来完成:安全和不可安全。你应该始终使用安全的方式。

    不安全的类型转换与可选值的解包非常相似。它试图在不检查是否可行的情况下进行类型转换,这可能会导致崩溃:

    let imageView = view as! UIImageView
    

    安全的类型转换类似于可选绑定。首先,它会检查一个视图是否实际上是UIImageView类型,然后进行类型转换。最后,它将转换结果保存在一个view常量中:

    if let view = view as? UIImageView {
      view.image = UIImage(named: "image")
    }
    

    小贴士

    总是使用安全的类型转换!

  • 不安全类型和操作:你会在 Swift 的标准库中找到许多以单词Unsafe*开头的类型和方法。这些操作尤其危险,你可以从它们的名称中理解这一点。通常,你会使用不安全类型来与 C 函数一起工作。让我们看看 C 中count函数的一个例子,它以Int的指针作为参数:

    int count(int *a); // C function
    

    C 中的count函数在 Swift 中将以这种类型可用:

    count(a: UnsafeMutablePointer<Int32>)
    

    你不需要创建一个 UnsafeMutablePointer<32> 变量。你可以通过引用将一个 Int32 Swift 变量作为 in-out 参数传递:

    var x: Int32 = 10
    count(&x)
    

    你也可以直接操作指针的内存,但这是一个非常危险的操作,应该避免:

    let pointer = UnsafeMutablePointer<Int>.alloc(1)
    pointer.memory = 10
    pointer.memory // store Int value - 10
    pointer.dealloc(1)
    

    小贴士

    避免使用不安全的数据类型。唯一的使用场景是与 C 函数和核心库进行交互。

  • 访问数组元素:尽管与数组一起工作通常是安全的,但它有一个你应该注意的不安全操作——访问超出其范围的元素。例如,让我们创建一个包含三个元素的数组。Swift 仍然允许我们尝试访问索引为 10 的元素,这将导致崩溃。Swift 会检查数组边界,并且不允许我们在数组外部使用或更新内存。这可以防止内存损坏问题,但它并不能防止应用程序崩溃:

    let numbers = [1, 2, 3]
    
    numbers.count
    numbers[1]
    numbers[10] //Crash
    

    为了安全起见,在访问元素之前检查边界数组:

    if numbers.count > 10 {
      numbers[10]
    }
    

摘要

在本章中,我们介绍了 Swift 的一些最重要和最强大的功能。现在你应该有信心使用它们。此外,本章还为你提供了一些关于如何使用这些功能并创建稳固应用程序的建议。

在下一章中,你将学习不同的调试技术,这些技术将帮助你识别缓慢的代码。正如你已经学到的,在执行任何优化之前,识别导致性能问题的原因非常重要。

第三章. 使用 Swift Toolkit 测试和识别慢速代码

应用程序开发的过程通常可以分为三个阶段:

  • 尝试新想法

  • 实现代码并检查其是否正确工作

  • 测量获得的结果的性能

第一阶段涉及尝试一个新想法。比如说,你想要实现一个排序算法,并且希望快速地原型化一个解决方案。

在第二阶段,你将实际实现解决方案并检查它是否正确工作。在本章中,我们将介绍如何测试和检查解决方案是否正确实现。

第三阶段和最终阶段涉及测量创建的软件的性能。当你已经编写了足够多的代码以进行测试,或者在你开发过程中看到不良的性能特征时,你会这样做。

REPL

REPL 代表 read-eval-print-loop。Swift REPL 是一个交互式的 Swift 代码解释器,它立即执行代码。要启动 Swift REPL,打开 Terminal 应用程序并执行以下命令:

$ xcrun swift

现在,你可以开始输入 Swift 代码并查看结果。在 REPL 中评估代码的一个好处是,如果你犯了一个错误,这个错误如果编译并运行程序最终会停止应用程序的执行,你仍然可以继续评估代码并保留所有进度。让我们在 Swift REPL 中尝试这段代码:

let a = 10
let b = a + "c"
let b = a + 10

REPL

在 REPL 控制台中编写代码不如在现代 Xcode IDE 中方便,但熟悉它是一项有用的技能。在 Swift REPL 的基础上,Apple 开发了更强大的工具,例如 Playgrounds,它拥有一个不错的源代码编辑器和 Swift REPL 的灵活性。

Playgrounds

Playgrounds 是一个强大的尝试代码并获取结果的工具。正如其名所示,它是一个玩耍的地方。在 Playgrounds 中,Swift 代码会立即被评估,这与 REPL 中的情况相同。你可以通过访问 文件 | 新建 | Playground 来创建一个新的 Playgrounds。输入文件名并创建它。

Playgrounds 由两部分组成,下一张截图将展示:

  • 编辑器

  • 结果

Playgrounds

本书展示的大多数代码示例都是在 Playgrounds 中创建的。例如,让我们创建一个数组并与之互动。我们可以应用过滤和映射函数,并打印数组中对象的数量:

Playgrounds

你会看到评估后的代码结果随着你的输入而出现。如果你将光标移到结果部分的一行,它将被突出显示,并出现两个按钮:

  • 快速查看

  • 显示/隐藏结果

快速查看将显示关于执行操作的更多详细信息。此功能对于函数尤其有趣。例如,如果您点击 filter 函数,您可以看到每次迭代的每个结果;结果是 truefalse 值。如果您显示具有数值结果的函数的详细信息,例如 map 函数,它可以显示一个漂亮的图表。

显示/隐藏结果允许您将快速查看结果直接添加到操场编辑器中。除非您将其隐藏,否则它将始终可见并刷新数据。

当您使用数学函数并希望以视觉表示形式查看结果时,显示具有数值返回类型的函数的结果视图非常有用。例如,让我们显示 sinpow 函数的结果,如以下截图所示:

操场

另一个有用的用例是用于显示算法的结果。我们将创建一个选择排序算法。在每一步看到算法的结果通常非常有用。您可以在操场中轻松检查它:

操场

交互式文档

操场的另一个用例是在制作交互式文档。您可以将标记格式化的文本添加到操场中。这样,您可以将操场中运行的交互式代码示例与丰富的文本描述相结合。

标记语法基于众所周知的 Markdown 语法:

  • 标题: # 标题

  • 粗体: **粗体文本**__ 粗体文本 __

  • 内联代码: `Int`

完整的 Markdown 文档可以在daringfireball.net/projects/markdown/syntax找到。

操场中有两种标记文本样式:

  • 单行: 单行标记文本样式如下:

    //: Markup text
    
    
  • 多行: 多行标记文本样式如下:

    /*:
     Markup text
    /*
    
    

现在让我们向操场添加一些标记文本:

//: # Array
//: Arrays is an ordered collection. [Read more here](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/CollectionTypes.html)
//: Arrays operations:
//: * Sort
//: * Map
//: * Etc.

标记文本可以在操场中以两种模式呈现,原始模式和渲染模式:

交互式文档

要在原始模式和渲染模式之间切换,请转到编辑器 | 显示渲染标记/显示原始标记。您也可以通过在实用工具面板中的操场设置部分启用渲染文档复选框来在这些模式之间切换,如图所示:

交互式文档

Playgrounds 标记格式的完整文档可以在developer.apple.com/library/ios/documentation/Xcode/Reference/xcode_markup_formatting_ref找到。

文件夹结构

操场文件实际上不是一个简单的文件,而是一个包含多个文件的包。您可以通过在Finder中打开它来探索其全部内容。右键点击显示包内容

在 Xcode 中,你可以在 项目导航器 中展开 playground 文件。它包含三个项目:

  • :用于 playground 的额外 Swift 源文件文件夹

  • 资源:用于 playground 的额外资源文件夹,例如图像、文本文件和其他内容

  • 页面:父 playground 的 playground 文件集合

源文件夹

通过将 playground 的源代码拆分为多个 Swift 源代码文件,你可以使 playground 更干净、更快。每次你在 playground 中进行更改时,它都会重新运行该 playground 中的所有代码。你放入 playground 的代码越多,它就越慢。source 文件夹中的 Swift 源文件不会在每次更改 playground 文件时重新运行;它们仅在更改该文件的内容时重新运行。例如,当我们与 Person 玩耍时,将 Person 类型添加到 source 文件夹中的单独 Swift 源文件是完美的用例。

小贴士

source 文件夹中的 Swift 文件被编译到框架中。在框架中,你必须使用 public 关键字标记你的类型和函数,以便它们在框架外部可见,在我们的例子中是在 playground 中。

资源

通过在 Resources 文件夹中包含资产,你可以在 playground 中引用它们。最简单的例子是添加一个 circle.png 图像文件:

let circle = UIImage(named: "circle.png")

Resources 文件夹中的文件可以通过 NSBundle.mainBundle 获取。让我们创建相同的圆,UIImage,但这次使用 NSBundle API:

if let path = NSBundle.mainBundle().pathForResource("circle", ofType: "png") {
  let cicrcle2 = UIImage(contentsOfFile: path)
}

页面

一个 playground 文件可以包含许多子 playground 文件,称为页面。一个页面是一个具有自己的源和资源文件夹的完整功能的 playground 文件。一个 playground 可以包含许多页面。要添加新页面,请转到 文件 | 新建 | Playground 页面,或者简单地右键单击 playground 文件并选择 新建 Playground 页面

Playground 页面非常适合将内容拆分为单独的部分,例如一本书的页面或章节。

为了在 playground 中的页面之间轻松导航,有一个页面导航标记。你可以跳转到第一个、最后一个、下一个、上一个或任何特定的页面。页面导航标记由两部分组成:[可见文本](page-link)。让我们看看这个标记的一些示例:

First and last pages links
//: First Page
//: Last Page 

Next and previous pages links
//: Next
//: Previous 

Page specific links. Use the same page name as a link. The space must be changed to "%20" 
//: Type Safe
//: Optionals

本书中的许多代码示例都是使用 playground 页面创建的。

XCPlayground

XCPlayground 是一个专门为与 playground 一起工作而创建的模块。它是一个非常小的模块,有四个主要功能:

  • XCPCaptureValue

  • XCPShowView

  • XCPSetExecutionShouldContinueIndefinitely

  • XCPSharedDataDirectoryPath

让我们快速查看这些函数。所有这些函数的结果都显示在 Playground 时间轴上。要查看它,请转到 视图 | 辅助编辑器 | 显示辅助编辑器,然后选择当前 playground 的时间轴。

XCPCaptureValue 允许你手动捕获一个值并在时间轴视图中显示它。这样,你可以创建自己的图形结果:

for i in 0...100 {
  let r = arc4random_uniform(100)
  XCPCaptureValue("random", value: r)
}

XCPShowView在 playground 时间轴中显示一个视图:

let frame = CGRect(x: 10, y: 10, width: 100, height: 100)
let view = UIView(frame: frame)
view.backgroundColor = .redColor()

XCPShowView("View", view: view)

XCPSetExecutionShouldContinueIndefinitely对于在 playground 中执行异步代码非常有用。它告诉 playground 在最后一条指令完成后无限期地继续执行其运行循环,这样我们就可以等待异步回调。

XCPSharedDataDirectoryPath返回一个指向所有 playgrounds 之间共享的目录的路径。这样,你可以在 playgrounds 之间以及每个 playground 运行之间保存和共享数据。

LLDB

LLDB 是一个高性能的命令行调试器。它在 Xcode 中可用。启动它的最简单方法是设置断点并运行应用程序。在 Xcode 的调试区域视图中,你会找到一个可以执行 LLDB 命令的控制台。因为我们制作了一个 iOS 应用程序,我们将在 AppDelegate 的didFinishLaunchingWithOptions方法中设置断点。

要打印变量的内容,我们可以使用p LLDB 命令。只需运行p并带上变量名,例如,p name,如下所示:

LLDB

LLDB 是一个非常强大的调试器。你可以在www.objc.io/issues/19-debugging/lldb-debugging/developer.apple.com/library/ios/documentation/IDEs/Conceptual/gdb_to_lldb_transition_guide了解更多关于 LLDB 调试器的信息。

Xcode 中的 REPL

Xcode LLDB 控制台提供的一些更有趣的功能之一是你可以在这里运行 Swift REPL。当你停止应用程序执行时,你可以输入并执行 Swift 代码。这对于调试非常有用。

注意

REPL 只能访问公共类型、函数和公共全局变量。局部变量在 REPL 中不可见。如果你需要与局部变量一起工作,请使用 LLDB 命令。

要进入 REPL 控制台,我们首先必须停止程序执行并进入 LLDB 调试器。有三个命令用于与 REPL 交互:

  • 进入 REPLrepl

  • 退出 REPL:

  • 在 REPL 中执行 LLDB 命令: command,例如,:p name

我们可以执行之前相同的功能,但现在使用调试控制台中的 REPL 命令,如下所示:

Xcode 中的 REPL

现在我们来看一些更有趣的 REPL 使用案例。当你进入 REPL 时,你可以输入并执行 Swift 代码。你还可以在 REPL 中访问应用程序中公开声明的 Swift 代码。总结一下,你可以运行现有代码,也可以添加新代码。

一个很好的用例是在调试应用程序的同时直接将测试代码添加到 REPL 中。作为一个例子,让我们实现一个跳过数组中负数的函数:

public func skipNegatives(a: [Int]) -> [Int] {
  return a.filter { $0 >= 0 }
}

func REPLTutorial() {
  let numbers = [2, -3, 1]
  let result = skipNegatives(numbers) 
}

skipNegatives函数的实现非常简单,在这个例子中很容易检查它是否正确工作,但你的其他函数可能更大且更难理解。此外,numbers数组只包含三个元素,而结果应该包含两个元素。我们可以通过在调试器视图中查找它来轻松检查这一点。

但如果我们的人数数组包含 1,000 个元素呢?遍历数组并验证它不包含负数元素会更困难。在这个例子中,数组中有 505 个非负元素:

func REPLTutorial() {

  let manyNumbers = makeNumbers()
  let bigResult = skipNegatives(manyNumbers)
}

public func makeNumbers() -> [Int] {
  var array = [Int]()
  for _ in 0..<1000 {
    let rand = Int(arc4random_uniform(10)) - 5
    array.append(rand)
  }
  return array
}

我们可以在 REPL 中编写一个测试函数来检查所有元素是否为正。让我们这样做,如下所示:

REPL in Xcode

首先,像往常一样,运行程序并在断点处停止。下一步是进入 REPL 并编写一个isAllPositive函数来检查所有数字是否为正。然后,只需调用skipNegativesisAllPositive,看看结果是否为true

(lldb) repl
1> func isAllPositive(ar: [Int]) -> Bool { 
2\.   let negatives = ar.filter { $0 < 0 }
3\.   return negatives.count == 0
4\. }
5> 
6> isAllPositive( skipNegatives([1, 2, -4, 7, 9, -1, 5, 12, -12, 24]))
$R0: Bool = true
7> isAllPositive([1, 2, -4])
$R1: Bool = false
8>

小贴士

如果你打算在 REPL 中多次使用测试函数,最好创建一个 Swift 源文件用于调试目的,并将其添加到那里。然后,你可以在 REPL 中调用它。

控制台日志

另一个强大的调试工具,你可能已经熟悉,是控制台日志。控制台日志可以用来记录所有类型的信息,包括:

  • 操作结果

  • 活动

  • 性能测量

要将语句记录到控制台,你可以使用以下这些函数之一:

  • print

  • debugPrint

这两个函数接受任何类型。

要为打印函数提供自定义文本格式,你必须遵守CustomStringConvertible协议,对于debugPrint,遵守CustomDebugStringConvertible协议。这两个协议都只需要实现一个属性。让我们创建一个简单的Person类型并实现自定义日志格式:

struct Person {
  let name: String
  let age: Int

extension Person: CustomStringConvertible, CustomDebugStringConvertible {

  // CustomStringConvertible
  var description: String {
    return "Name: \(name)"
  }

  // CustomDebugStringConvertible
  var debugDescription: String {
    return "Name: \(name) age: \(age)"
  }
}

现在,当使用printdebugPrint调用Person类型的实例时,它将显示自定义描述。

控制台日志的一个更有趣的使用案例是方法性能日志。我们想知道特定方法或代码片段运行了多长时间。

第一个想法可能是使用NSDate来测量时间。NSDate工作得很好,但在QuartzCore框架中有一个更好的解决方案——CACurrentMediaTime函数。它基于mach_absolute_time返回结果。我们案例的基础伪代码如下:

let startTime = CACurrentMediaTime()
// Perform code that we need to measure.
let endTime = CACurrentMediaTime()
print("Time - \(endTime - startTime)")

我们希望非常频繁地使用这个性能测量函数,将其提取到单独的可重用函数中将会非常有用。因为 Swift 支持函数式编程风格,我们可以轻松做到这一点。

我们将创建一个测量函数。它将接受另一个执行我们需要测量时间的任务的函数:

func measure(call: () -> Void) {
  let startTime = CACurrentMediaTime()
  call()
  let endTime = CACurrentMediaTime()

  print("Time - \(endTime - startTime)")
}

现在,假设我们想要找出创建 1,000 个Person类型实例所需的时间:

for i in 0...1000 {
  let person = Person(name: "Sam", age: i)
}

我们需要做的就是简单地将这段代码封装成一个闭包,并将其传递给measure函数:

measure {
  for i in 1...1000 {
    let person = Person(name: "Sam", age: i)
  }
}

在 playground 中运行此测量将给出以下结果:

Time 0.000434298478008714.

单元测试中的性能测量

当你创建一个新的项目时,Xcode 会为该项目创建一个名为ProjectName + Tests的单元测试目标。如果你不熟悉单元测试,你可以在 Xcode 中阅读有关测试的文档,链接为developer.apple.com/library/ios/documentation/DeveloperTools/Conceptual/testing_with_xcode

Xcode 还会为你创建一个简单的单元测试文件。在我们的项目中,它是Swift_ToolkitTests.swift。单元测试有三个主要方法,具有不同的目的:

  • setup

  • teardown

  • test

    注意

    单元测试函数必须以test前缀开始,例如:

    func testCreatingPerson
    func testChangingName
    
    

这些函数的名称反映了它们的作用。setup函数在运行单元测试之前执行额外的设置,teardown函数执行清理工作,但对我们来说最有趣的是test函数,它执行测试。

XCTestCase单元测试类有一个measureBlock函数,其工作方式与我们实现的measure函数类似。让我们实现一个单元测试来测量创建 1,000 个人的性能。

首先,我们需要使我们的应用程序中的Person类型和其他类型对单元测试目标可用。为此,我们需要导入一个带有@testable属性的 app 模块——@testable import {ModuleName}。现在,该模块中的所有publicinternal类型和方法都可在单元测试目标中使用:

注意

要启用@testable,必须将Enable Testability项目构建设置设置为。Xcode 默认将其设置为,用于调试模式。你永远不应该在发布模式下启用它。

@testable import Swift_Toolkit

func testCreatingPeoplePerformance() {
  measureBlock() {
    for i in 1...1000 {
      _ = Person(name: "Sam", age: i)
    }
  }
}

当你运行单元测试时,通过访问Product | Test或使用CMD + U快捷键,你将在函数名称的右侧看到性能特征。当你点击它时,你将看到更多详细信息以及设置基线性能值的按钮,这些值将用于比较未来的测量。

measureBlock运行几块代码并显示平均时间。你可以在详细弹出窗口中看到 10 次不同迭代的性能,如图所示:

单元测试中的性能测量

现在我们设置一个基线并再次运行单元测试。测试通过了!

性能单元测试的目的是测量性能并确保它不会大幅下降。默认情况下,允许的标准偏差为 10%。这意味着如果代码的性能下降超过 10%,则测试失败。让我们尝试模拟这种情况并看看会发生什么。为了模拟额外的工作,我们将在person初始化器中添加延迟:

init(name: String, age:Int) {
  self.name = name
  self.age = age
  usleep(100)
}

现在让我们再次运行测试。你会看到测试失败,并在测试函数名称旁边显示一个红色的标志。

这样,单元测试不仅可以帮助你测量性能,还可以确保你在开发应用时性能不会下降。

仪器

在本章中,我们将要探讨的最后一种工具是仪器。虽然我们是在章节的末尾提到它,但它却是测量应用各种特性的最强大工具:性能、内存使用和泄漏、网络、监控、动画、硬盘和文件活动。

启动 Instruments 进行应用的 simplest way 是通过转到产品 | 分析或使用CMD + I键盘快捷键。这将启动当前目标的仪器,并显示可用的仪器测量模板。我们将选择一个时间分析器模板,并点击记录。这将启动应用并记录每个被调用函数的性能。现在我们可以分析函数的性能:

仪器

Instruments 是一个非常强大的工具,要完全介绍其功能需要单独的一章。如果你不熟悉 Instruments,你应该阅读更多关于它的信息,请参阅developer.apple.com/library/ios/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide

提醒

无论何时进行任何性能测量,都应该在发布模式下进行。Swift 编译器在发布模式下执行许多优化步骤,从而显著提高性能。要设置发布模式,请转到产品 | 方案 | 编辑方案 | 运行,并将构建配置设置调整为发布。始终使用发布模式进行性能测试。

摘要

在本章中,我们介绍了许多可以提升你生产力的工具。REPL 和 Playgrounds 非常适合尝试新代码和快速代码原型设计。Playgrounds 还可以用来创建交互式文档和教程。然后我们介绍了 Xcode 中的调试工具(如 LLDB 和 REPL),这些工具对于检查运行时操作的结果非常有用。应用性能可以在仪器或使用控制台日志中进行测量。为了确保性能不会下降,你应该使用单元测试。

在本章中,你学习了众多用于发现缓慢和问题代码的工具,而在下一章中,你将学习如何改进和优化它们。

第四章. 提高代码性能

实现良好的代码性能是一项重要且令人向往的任务。每个人都希望拥有性能良好的应用程序。在本章中,我们将讨论以下性能主题:

  • 理解性能优化

  • 优化清单

  • 常量和变量

  • 方法调用

  • 智能代码

  • 值对象和引用对象

  • Swift 数组和不受保护的 C 数组

  • 避免使用 Objective-C

理解性能优化

优化的第一规则是——不要优化。您应该始终记住唐纳德·诺伊曼(Donald Knut)的这句话:

过早优化是万恶之源

这是一个非常真实且正确的陈述。您应该在看到性能问题并找到导致问题的原因后才开始进行性能优化。

有两种类型的性能优化:

  • 显式

  • 隐式

显式

显式性能优化是一种针对特定慢速代码片段的技术。此类优化需要显著的代码更改,可能会降低代码的可读性。您可以通过将算法更改为更有效的一种来执行显式性能优化。使用更多内存作为缓存也可能提高性能。

隐式

隐式性能优化是应用语言特定(在我们的例子中是 Swift 特定)功能以实现更好性能的技术。隐式代码性能不需要显著的代码更改。它不会对代码的可读性产生任何负面影响,有时甚至会使代码更好。我称之为隐式,因为您可以在代码的任何地方应用它,经过一段时间后,它对您来说就变得不可见了。

显式性能优化是一个非常流行且广泛的话题,在许多关于算法和数据结构的书籍中都有涉及。另一方面,隐式与 Swift 编程语言直接相关,是一个非常有趣的话题,我们将对其进行探讨。

优化清单

在进行任何优化和性能测量之前,您应遵循以下步骤:

  1. 启用发布模式:Swift 编译器在发布模式下进行大量的代码优化,并提高性能。要启用发布模式,请转到产品 | 方案 | 编辑方案 | 运行,选择信息选项卡,并在构建配置设置中选择发布

  2. 禁用安全检查:禁用安全检查可能会提高应用程序性能;但正如其名所示,它会影响安全性,禁用并不完全安全,应谨慎应用。Swift 执行的一个安全检查示例是在访问内存之前检查数组边界。如果您禁用安全检查,Swift 不会执行此操作。

    禁用安全检查是Swift 编译器 - 代码生成设置,可在目标构建设置中找到。要禁用安全检查,请选择项目 | 构建设置,然后搜索禁用安全检查设置,并将其设置为发布模式的

  3. 启用快速、整个模块优化级别:默认情况下,Swift 编译器一次只对单个文件进行优化。它就像为每个文件在沙盒环境中执行一样。一个文件的优化对其他文件没有影响。

整个模块优化一次对模块中的所有源文件进行优化。所有源文件一起评估和优化。这非常有用,因为我们经常在一个文件中声明一个类型并在另一个文件中使用它。整个模块优化所做的优化之一是搜索没有在任何地方覆盖的internal类型的声明,并为它们添加final声明优化。

要启用整个模块优化,请在构建设置中为 Swift 编译器选择优化级别,并为发布模式选择快速、整个模块优化 [-O -whole-module-optimization]选项。

启用此设置会增加构建时间。您应该用于发布构建和性能测试。在开发和调试时,最好禁用此设置以加快编译时间。

Xcode 中提供了两种不同类型的优化级别设置,具有不同的目的和选项:

  • Apple LLVM – 代码生成

  • Swift 编译器 – 代码生成

如果您选择优化级别设置,您可以在实用工具面板的快速帮助部分看到所有可用选项的详细描述。

优化清单

默认情况下,在发布模式下,Xcode 使用以下设置:

  • Apple LLVM, GCC_OPTIMIZATION_LEVEL: 最快、最小 [-Os]

  • Swift 编译器,SWIFT_OPTIMIZATION_LEVEL: 最快 [-O]

您可以尝试启用其他优化设置。例如,使用最快、最激进的优化[-Ofast]可以提高应用程序性能。

Swift 代码编译

LLVM 首先将您的源代码转换为伪代码。在下一步中,它被优化并编译成汇编代码。

您可以通过使用 Swift 编译器中的swiftc在命令行手动执行这些代码处理步骤。要查看 Swift 编译器所有可用选项,请打开Terminal.app并执行--help命令:

xcrun swiftc --help

您将看到可用的编译模式和选项。我们正在寻找的是:

  • -emit-assembly

  • -emit-ir

  • -emit-silgen

  • -emit-sil

这些编译模式允许您将不同的编译步骤应用于 Swift 源文件。例如,我们可以使用此命令将sourceFile.swift输出到规范 SIL 表示形式,并将结果写入outputFile

swiftc -emit-sil sourceFile.swift -o outputFile

我们将在第八章发现所有底层的 Swift 力量中更详细地介绍编译过程。

常量和变量

使用常量会影响代码的可读性。它使代码更清晰、更安全。使用常量而不是变量也可能带来性能上的好处。当你使用常量时,你给编译器一个明确的提示,表明这个值不会改变。Swift 编译器可以对常量的值进行内联优化,而不为其分配内存。

在简单的例子中,Swift 编译器也可以对变量执行相同的优化。让我们分析这个简单示例的结果,迭代并计算总和。在这个例子中,变量和常量的性能相同。

var result = 0
for _ in 0...10000000 {
  let a = Int(arc4random())
  result += a
}
// Average Time - 0.162666518447804

var result = 0
for _ in 0...10000000 {
  var a = Int(arc4random())
  result += a
}
// Average Time - 0.160957522349781

如果我们查看一个更复杂的例子,我们会看到常量表现得和变量一样,甚至更好。可能看起来使用变量的版本应该运行得更快,因为不需要在每次操作时为新的常量分配内存,但 Swift 编译器足够智能,能够执行智能优化,使它们表现得一样。

var result = 0
for _ in 0...100000000 {
  let a = Int(arc4random_uniform(10))
  let b = a + Int(arc4random_uniform(10))
  let c = b * Int(arc4random_uniform(10))

  result += c
}
// Average Time - 12.6813167635002

var result = 0
for _ in 0...100000000 {
  var a = Int(arc4random_uniform(10))
  a += Int(arc4random_uniform(10))
  a *= Int(arc4random_uniform(10))
  result += a
}
// Average Time - 12.6813167635102

因此,一般的建议是:优先使用常量。它们使代码更安全、更清晰,并且对性能也有积极的影响。在某些情况下,变量也可能提高代码的可读性,就像前面的例子中,我们不得不做一些数学计算,而改变变量的值实际上使代码更清晰。

常量比变量好得多,以至于当 Xcode 检测到一个从未被修改的变量时,它会显示警告,并建议你将其更改为常量。

常量和变量

方法调用

在讨论 Swift 方法调用优化之前,查看不同类型的方法调用实现将非常有用。

方法调用主要有两种类型:

  • 静态:静态方法绑定意味着,当你对一个对象调用方法时,编译器知道你正在调用这个类上的确切方法。C 是一个具有静态方法绑定的语言的例子。

  • 动态:另一方面,动态方法与对象之间的绑定较弱。当你对一个对象调用方法时,没有保证对象能够处理这个方法调用。Objective-C 有动态方法绑定。这就是为什么你会在 Objective-C 中看到“对象没有响应选择器”的错误。

Objective-C 是一种动态类型语言,它有一个动态运行时。调用方法被称为消息发送。你向目标发送一个消息。

[dog bark] // dog is a target and bark is a message

这看起来像是一个正常的方法调用,但在编译后,它实际上会看起来像:

objc_msgSend(dog, @selector(bark))

Objective-C 使用动态方法绑定。这意味着消息和接收者被分别存储。当你向 dog 对象发送一个 bark 消息时,狗类必须查找它是否有 bark 方法以及它是否能够处理这个方法。这个过程被称为动态方法绑定。实现方式可能如下所示:

id objc_msgSend ( id obj, SEL _cmd, ... )
{
    Class c = object_getClass(obj);
    IMP imp = CacheLookup(c, _cmd);
    if (!imp) {
        imp = class_getMethodImplementation(c, _cmd);
    }
    jump imp(obj, op, ...);
}

Swift 使用静态方法绑定。它使用 vtable(虚拟方法表)来存储方法。vtable 是一个函数指针数组。这意味着一个类有一个其方法列表及其方法实现的内存地址。当你调用 Swift 中的方法时,你是在调用特定类型的方法。方法与调用该方法的对象之间的绑定非常强,并且是在编译时完成的。

让我们看看相同的代码在 Swift 中的表现:

dog.bark()

因为 Swift 知道你想要在 Dog 类上调用 bark 方法,所以它不需要为方法信息进行任何额外的查找。它将获取函数地址并调用它:

methodImplementation = dog->class.vtable[indexOfBark] methodImplementation()

Swift 可以对方法调用进行更复杂的优化。如果方法没有被覆盖,这意味着对 bark 方法的调用将始终解析为相同的函数调用。Swift 编译器可以跳过 vtable 中的函数查找并内联直接函数调用:

_TFC12methodsCalls3Dog4barkfS0_FT_T_()
//this method is equal to- methodsCalls.Dog.bark()

注意

这是 bark 方法的混淆名称。我们将在第八章发现所有 Swift 的底层力量中了解更多,发现所有 Swift 的底层力量

汇编代码中的 _TFC12methodsCalls3Dog4barkfS0_FT_T_() 直接函数调用被翻译成简单的命令。以下是汇编伪代码的示例:

rbx = __TFC11Performance3DogCfMS0_FT_S0_(); // Create dog instance
r15 = *(*rbx + 0x48); //get the location of bark method
(r15)(rbx); // call the method

让我们比较 Swift 静态方法调用与 Objective-C 动态方法调用的性能差异。让我们创建一个简单的 Number 类,它有一个 add 方法,用于将两个数字相加(Swift 解决方案):

class Number {

  func add(x: Int, y: Int) -> Int {
    return x + y
  }
}

对于时间测量,我们使用上一章中的 measure 函数:

let number = Number()
measure("Sum", times: 20) {
  var result: Int = 0
  for i in 0...1000000000 {
    result += number.add(i, y: i + 1)
  }
  print(result)
}

结果:平均时间 - 1.45391867654989

让我们看看 Objective-C 的解决方案:

//  KKNumber.h
@import Foundation;

@interface KKNumber : NSObject

- (NSInteger)add:(NSInteger)num number:(NSInteger)num2;

@end

//  KKNumber.m
#import "KKNumber.h"

@implementation KKNumber

- (NSInteger)add:(NSInteger)num number:(NSInteger)num2 {
  return num + num2;
}

@end

KKNumber *number = [[KKNumber alloc] init];

[Measure measure:20 call:^{
  NSInteger result = 0;
  for (int i  = 0; i < 1000000000; ++i) {
    result += [number add:i number:i + 1];
  }
  NSLog(@"Result %ld", (long)result);
}];

结果:平均时间 - 2.974986

如你所见,即使在 Swift 中一个非常简单的函数调用也要快两倍。现在你已经了解了 Swift 方法和函数调用实现的细节,是时候转向更实际的例子了。

函数和方法

你可以通过以下四种方式之一创建函数或方法来使代码可重用:

  • 全局函数

  • 类型方法

  • 静态和最终方法

  • 实例方法

全局函数

全局函数是最简单的一种。它们不能被覆盖和更改。全局函数存储为内存中的命名指针。当你调用全局函数时,它会被翻译为直接内存调用,而不需要在 vtable 中进行查找。这应该是最快的方式。调用全局函数的汇编代码如下:

call       __TZFC4test3Dog5speakfMS0_FT_T_

类型方法

类型方法操作的是类型本身,而不是该类型的实例。类方法存储在该类的 vtable 中。类方法可以被子类覆盖。因为类方法可以被覆盖,Swift 编译器有时无法像全局函数那样优化类方法调用为直接函数调用。为了更好地理解原因,让我们看看这个简单的覆盖类方法的例子:

class Dog {
  class func bark() {
    print("Bark")
  }
}

class BigDog: Dog {
  override class func bark() {
    print("big loud BARK")
  }
}

func getDog() -> Dog.Type {
  return arc4random() % 2 == 0 ? Dog.self : BigDog.self
}

let dog = getDog()
dog.bark()

我们创建了两个简单的类:DogBigDoggetDog 函数返回一个 Dog.Type 类类型,但它也可以返回 BigDog.Type。因此,dog 变量可以是 Dog.TypeBigDog.Type。正因为如此,Swift 编译器不能直接在行内进行函数调用。它必须查找虚表中的函数指针,这是一个非常便宜的操作。这个伪汇编代码将看起来像这样:

rax = __TF4test6getDogFT_MCS_3Dog(); // call getDog()
*__Tv4test3dogMCS_3Dog = rax;    // convert result to Dog.Type.
(*(rax + 0x48))(rax);         // call bark method, vtable lookup

当你明确指定类型时,Swift 编译器可以对重写的方法进行行内直接函数调用。在这个例子中,我们调用 Dog 类上的 bark 方法,Swift 编译器会跳过虚表查找:

Dog.bark()

// Pseudo assembly code
__TTSf4d___TZFC4test3Dog4barkfMS0_FT_T_

静态方法

你可以在类、结构和枚举中声明静态类型方法。在类中声明类型方法时,使用 static 关键字与使用 final class 关键字相同。这两个方法声明是等价的:

static func speak() {}
final class func speak() {}

静态方法不能在子类中被重写。因为它们不能被重写,所以不需要存储在虚表中。静态方法的实现细节与全局函数非常相似。在汇编代码中,它将被翻译为直接函数调用,与全局函数相同。让我们给我们的狗类添加一个静态函数,并探索它如何翻译成汇编代码:

class Dog {

  static func speak() {
    print("I don't speak")
  }
}

Dog.speak()
BigDog.speak()

DogBigDog 类中 speak 方法的两次调用都被翻译成一条汇编指令。

call       __TZFC4test3Dog5speakfMS0_FT_T_

实例方法

类型方法和实例方法之间的主要区别在于实例方法可以操作实例常量和变量。实例方法可以被重写,并且需要存储在虚表中。让我们给我们的 Dog 类添加一个 name 变量和一个 changeName 实例方法:

class Dog {
  var name = ""

func changeName(name: String) {
    self.name = name
  }
}

let someDog = Dog()
someDog.changeName("Cocoa")

changeName 方法将被翻译成以下汇编代码。从虚表中获取方法地址,并传递参数来调用它:

rbx = __TFC4test3DogCfMS0_FT_S0_(); // Create Dog()
*__Tv4test7someDogCS_3Dog = rbx;  //Assign Dog instance to a someDog variable
r15 = *(*rbx + 0x68);    // Get changeName method, vtable lookup
(r15)("Coca", 0x4, 0x0, rbx); // call method and pass arguments

比较函数速度

现在你已经知道了函数和方法是如何实现以及它们是如何工作的。让我们比较那些全局函数和不同方法类型的性能速度。对于测试,我们将使用一个简单的 add 函数。我们将将其实现为一个全局函数、静态、类类型和实例,并在子类中重写它们:

func add(x: Int, y: Int) -> Int {
  return x + y
}

class NumOperation {

  func addI(x: Int, y: Int) -> Int
  class func addC(x: Int, y: Int) -> Int
  static func addS(x: Int, y: Int) -> Int
}

class BigNumOperation: NumOperation {

  override func addI(x: Int, y: Int) -> Int
  override class func addC(x: Int, y: Int) -> Int
}

对于测量和代码分析,我们使用一个简单的循环来调用这些不同的方法:

measure("addC") {
  var result = 0
  for i in 0...2000000000 {
    result += NumOperation.addC(i, y: i + 1)
    // result += test different method
  }
  print(result)
}

结果:

所有这些方法的表现方式完全相同。此外,它们的汇编代码看起来也完全一样,除了函数调用的名称:

  • 全局函数add(10, y: 11)

  • 静态NumOperation.addS(10, y: 11)

  • NumOperation.addC(10, y: 11)

  • 子类静态BigNumOperation.addS(10, y: 11)

  • 子类重写类BigNumOperation.addC(10, y: 11)

这些函数的汇编伪代码看起来是这样的:

r14 = 0x0;
do {
  rbx = "Function name Here"(r14 + 0x1, r14) + rbx;
  r14 = r14 + 0x1;
} while (r14 != 0x3ea);

即使直接调用 BigNumOperationaddC 类函数会重写 NumOperationaddC 函数,也不需要进行虚表查找。

实例方法调用看起来略有不同。

  • 实例

      let num = NumOperation()
      num.addI(10, y: 11)
    
  • 子类重写实例

      let bigNum = BigNumOperation()
      bigNum.addI()
    

唯一的不同之处在于它们需要初始化一个类并创建对象的实例。在我们的例子中,这并不是一个昂贵的操作,因为我们是在循环外部进行的,并且只发生一次:

if (rax == 0x0) {
  rax = _swift_getInitializedObjCClass (
    objc_class__TtC4test12NumOperation);
  *__TMLC4test12NumOperation = rax;
}
var_78 = _swift_allocObject(rax, 0x10, 0x7);

调用实例方法的循环看起来完全一样,所以我们不再列出它。

正如你所见,全局函数和静态及类方法之间几乎没有什么区别。实例方法看起来略有不同,但这不会对性能产生太大影响。尽管这在简单用例中是正确的,但在更复杂的例子中,它们之间还是有区别的。让我们看看这个例子:

let baseNumType = arc4random_uniform(2) == 1 ? 
  BigNumOperation.self : NumOperation.self

  for i in 0...loopCount {
    result += baseNumType.addC(i, y: i + 1)
  }
  print(result)

这里的唯一区别是,我们不是在编译时指定 NumOperation 类类型,而是在运行时随机返回它。正因为如此,Swift 编译器在编译时不知道应该调用哪个方法——BigNumOperation.addC 还是 NumOperation.addC。这个小小的改动对生成的汇编代码和性能有影响。

函数和方法使用总结

全局函数是最简单的,提供最佳性能。全局函数太多会使代码难以阅读和跟踪。

不能被重写的静态类型方法与全局函数具有相同的性能,但它们还提供了一个命名空间(类型 name),因此我们的代码看起来更清晰,且不会损失任何性能。

可以被重写的类方法可能会导致性能损失,应该在你需要类继承时使用。在其他情况下,静态方法更受欢迎。

实例方法操作对象的实例。当你需要操作该实例的数据时,使用实例方法。

当你不需要重写方法时,将方法设置为 final。这告诉编译器,由于这个原因,优化和性能可能会提高。

智能代码

由于 Swift 是一种静态和强类型语言,它可以很好地读取、理解和优化代码。Swift 尝试移除所有不必要的代码执行。为了更好地解释,让我们看看一个简单的例子:

class Object {

  func nothing() {  
  }
}

let object = Object()
object.nothing()
object.nothing()

我们创建 Object 类的实例并调用一个 nothing 方法。nothing 方法是空的,调用它没有任何作用。Swift 编译器理解这一点,并移除了这些方法调用。之后,我们只剩下一行代码:

let object = Object()

Swift 编译器还可以避免创建从未使用过的对象。它减少了内存使用和不必要的函数调用,这也有助于减少 CPU 使用。在我们的例子中,在移除 nothing 方法调用和创建 Object 之后,object 实例不再被使用,因此可以省略 Object 的创建。这样,Swift 就移除了所有三行代码,最终我们没有代码要执行。

Objective-C 无法进行此优化。因为它有动态运行时,nothing 方法的实现可以在运行时更改以执行一些工作。这就是为什么 Objective-C 无法删除空方法调用。

这种优化可能看起来并不起眼,但让我们看看另一个稍微复杂一些的例子,它使用了更多的内存:

class Object {
  let x: Int
  let y: Int
  let z: Int

  init(x: Int) {
    self.x = x
    self.y = x * 2
    self.z = y * 2
  }

  func nothing() {
  }
}

我们向我们的 Object 类添加了一些 Int 数据来增加内存使用。现在 Object 实例至少使用 24 字节(3 倍的 Int 大小;在 64 位架构中 Int 使用 4 字节)。让我们也通过添加更多指令来尝试增加 CPU 使用率,通过循环实现:

for i in 0...1_000_000 {
  let object = Object(x: i)
  object.nothing()
  object.nothing()
}
print("Done")

注意

整数字面量可以使用下划线 (_) 来提高可读性。1_000_000_000 与 1000000000 相同

现在我们有三百万条指令,我们使用了 2400 万字节,大约 24 MB。这对于实际上什么也不做的操作类型来说相当多。如您所见,我们没有使用循环体的结果。对于循环体,Swift 执行与上一个例子相同的优化,我们最终得到一个空的循环:

for i in 0...1_000_000 {
}

空循环也可以跳过。结果,我们节省了 24 MB 的内存使用和三百万次方法调用。

危险的函数

有些函数和指令有时对应用程序没有任何价值,但 Swift 编译器无法跳过它们,它们可能会对性能产生非常负面的影响。

控制台打印

将语句打印到控制台通常用于调试目的。在发布模式下,printdebugPrint 指令不会被从应用程序中移除。让我们看看这段代码:

for i in 0...1_000_000 {
  print(i)
}

Swift 编译器将 printdebugPrint 视为有效且重要的指令,不能跳过。尽管这段代码实际上什么也不做,但它无法被优化,因为 Swift 不移除 print 语句。因此,我们有一百万条不必要的指令。这个汇编代码如下:

mov        qword [ss:rbp+var_20], rbx                 
inc        rbx                //increase i
mov        rdi, r14          // save stack state for function call                  
mov        rsi, r15                                   
call       __TFSs5printurFq_T_   //call print
cmp        rbx, 0xf4241      // check loop condition i > 1_000_000
jne        0x100155fb0      // continue loop if condition is true

如您所见,即使是非常简单的使用 print 语句的代码也可能非常大幅度地降低应用程序的性能。包含 1_000_000 个 print 语句的循环需要五秒钟,这已经很多了。如果在 Xcode 中运行,它将需要长达 50 秒。

如果你在前一个例子中的 Object 类的 nothing 方法中添加一个 print 指令,情况会更糟:

func nothing() {
  print(x + y + z)
}

在这种情况下,由于 print 指令,我们创建 Object 实例并调用 nothing 的循环无法消除。尽管 Swift 无法完全消除该代码的执行,但它通过移除创建实例和调用 nothing 方法来进行优化,将其转换为简单的循环操作。优化后的编译代码将如下所示:

// Initial Source Code
for i in 0...1_000 {
  let object = Object(x: i)
  object.nothing()
  object.nothing()
}

// Optimized Code
var x = 0, y = 0, z = 0
for i in 0...1_000_000 {

  x = i
  y = x * 2
  z = y * 2

  print(x + y + z)
  print(x + y + z)
}

如您所见,这段代码远非完美,提供了很多实际上对我们没有任何价值的指令。有一种方法可以改进这段代码,以便 Swift 编译器能够执行最优化的代码优化。

移除打印日志

为了解决这个性能问题,我们必须在编译之前从代码中删除 print 语句。有几种方法可以做到这一点。

注释掉

第一个想法是在发布模式下的代码中注释掉所有的 print 语句。

//print("A")

这将有效,但下次你想启用日志时,你需要取消注释那段代码。这是一个非常糟糕且痛苦的做法。有一个更好的解决方案。

小贴士

注释掉的代码通常是不良实践。你应该使用源代码版本控制系统,如 Git,而不是这样做。这样,你可以安全地删除不必要的代码,并在需要时在历史记录中找到它。

使用构建配置

我们可以仅在 Debug 模式下启用 print。为此,我们将使用构建配置来有条件地排除一些代码。首先,我们需要添加一个 Swift 编译器自定义标志。为此:

选择项目目标— 构建设置其他 Swift 标志 设置在 Swift 编译器 – 自定义标志 部分,并为 Debug 模式添加 –D DEBUG 标志:

使用构建配置

在此之后,你可以使用 DEBUG 配置标志来仅启用调试模式下的代码。我们将定义自己的 print 函数,该函数仅在调试模式下生成打印语句。在发布模式下,该函数将为空,Swift 编译器将成功将其删除:

func D_print(items: Any..., separator: String = " ", terminator: String = "\n") {
  #if DEBUG
    print(items, separator: separator, terminator: terminator)
  #endif
}

在任何地方,我们都会使用 D_print 替代 print

func nothing() {
  D_print(x + y + z)
}

小贴士

你也可以创建一个类似的 D_debugPrint 函数。

Swift 非常智能,做了很多优化,但我们还必须使代码对人们阅读和编译器优化来说清晰易懂。

小贴士

使用预处理器会增加代码的复杂性。明智地使用它,并且仅在正常 if 条件无法工作的情况下使用,就像我们的 D_print 示例中那样。

使用不可优化的常量

一些类型不能像其他类型那样优化,使用该类型的常量不能防止 Swift 编译器删除代码。

让我们看看这个简单的例子:

class Optimizable {
  let x = 10
}

// Use-case
let o = Optimizable()

Swift 编译器可以删除此代码。让我们看看一个更复杂的例子:

class Optimizable {
  let x = 10
  let a = ""
}

// Use-case
let o = Optimizable()

此代码不能完全被 Swift 编译器删除。通过添加一个非常简单的 String 常量,我们已经向源代码中添加了更多的复杂性。要理解为什么会发生这种情况,我们需要探索汇编代码:

if (*__TMLC4test11Optimizable == 0x0) {
  *__TMLC4test11Optimizable = _swift_getInitializedObjCClass();
  // Initialize objc_class__TtC4test11Optimizable
}
rax = _swift_allocObject();
*(rax + 0x10) = 0xa;
*(rax + 0x18) = "";

getInitializedObjCClass 的名字中,我们可以假设这个方法执行了一些 Objective-C 类类型的初始化。这可能会显得非常奇怪,因为我们没有在我们的代码中使用任何 Objective-C 类型。我们添加了一个简单的空字符串常量:let a = ""

问题是 Swift 的 String 提供了与 Objective-C 的 NSString 类型无缝互操作性。正因为如此,当我们使用 Swift 的 String 时,它会分配一些额外的数据来执行到 NSString 的桥接。以下是这个元数据 objc_class__TtC4test11Optimizable 的样子:

objc_class__TtC4test11Optimizable:
dq         __TMmC4test11Optimizable; metaclass,
dq         _OBJC_CLASS_$_SwiftObject; superclass
dq         __objc_empty_cache; cache
dq         __objc_empty_vtable; vtable
dq         0x1001bf7e1; data (Swift class)

有更多类型,当与简单的常量一起使用时,Swift 编译器无法简单地消除:

  • 字符串

  • 数组

  • 自定义类对象

  • 闭包

  • 集合

  • 字典

    class NotOptimizableTypes {
    
      let a: String = ""
      let b: String? = nil
      let c: Array<Int> = [1]
      let obj = Object()
      let d: Int -> Int = { $0 + 1 }
    
      let e: Set<Int> = [1]
      let f: Dictionary<Int, Int> = [1 : 1]
    }
    

如果我们尝试在结构体而不是类中使用这些类型,会发生有趣的行为。我们观察到不同的行为,因为 Swift 结构体没有暴露给 Objective-C 使用。这就是为什么 Swift 编译器可以消除其中许多类型:String、Array、Class 和 Closures。即使它们在结构体中使用,Set 和 Dictionary 也不会被消除。

struct NotOptimizableInStruct {

  let a: String = ""
  let b: Array<Int> = [1]
  let obj = Object()
  let c: Int -> Int = { $0 + 1 }
}

将这些常量移动到初始化器并不能解决问题。

解决这个问题的方法是您不应该使用任何未使用且不提供任何价值的常量。

提高速度

有几种技术可以简单地提高代码性能。让我们直接跳到第一种。

最终

您可以使用final属性来创建函数和属性声明。添加final属性使其不可覆盖。子类不能覆盖那个方法或属性。当您使方法不可覆盖时,没有必要将其存储在虚表中,可以直接调用该函数而无需在虚表中查找任何函数地址:

class Animal {

  final var name: String  = ""
  final func feed() {
  }
}

正如您所看到的,final方法比非final方法运行得更快。即使这样的优化也能提高应用程序的性能。它不仅提高了性能,而且使代码更安全。这样,您禁用了方法被覆盖,并防止了意外和不正确的行为。

启用整个模块优化设置可以达到非常相似的优化结果,但最好将函数和属性声明明确标记为final:这可以减少编译器的工作量并加快编译时间。在 Xcode 7 Beta 6 中,具有整个模块优化的大项目的编译时间可能长达五分钟。

内联函数

正如您所看到的,Swift 可以进行优化并使一些函数调用内联。这样调用函数就不会有任何性能损失。您可以使用@inline属性手动启用或禁用内联函数:

@inline(__always) func someFunc () {
}

@inline(never) func someFunc () {
}

尽管您可以手动控制内联函数,但通常最好将其留给 Swift 编译器来处理。根据不同的优化设置,Swift 编译器会应用不同的内联技术。

@inline(__always)的使用场景非常简单,即您希望始终内联的一行函数。

值对象和引用对象

在上一章中,您学习了使用不可变值对象的好处。值对象不仅使代码更安全、更清晰,而且使代码运行更快。值对象的速度性能优于引用对象,原因如下。在本章中,我们将使用结构体作为值对象的示例。

内存分配

值对象可以在栈内存上分配,而不是在堆内存上。引用对象需要分配在堆内存上,因为它们可以被许多所有者共享。因为值对象只有一个所有者,所以它们可以安全地分配在栈上。栈内存比堆内存快得多。

第二个优点是值对象不需要引用计数内存管理。因为它们只有一个所有者,所以值对象没有引用计数。使用ARC(自动引用计数),我们不需要过多考虑内存管理,对我们来说它看起来大多是透明的。尽管使用引用对象和值对象的代码看起来相同,但 ARC 为引用对象添加了额外的保留和释放方法调用。让我们看看一个表示数字的结构和类的非常简单的例子:

struct NumberValue {
  let x: Int
}

class NumberReference {
  let x: Int
  init(x: Int) {
    self.x = x
  }
}

作为例子,我们将使用NumberValueNumberReference编写完全相同的代码,并比较生成的汇编代码:

var x = NumberValue(x: 1)
var xres = x.x
x = NumberValue(x: 2)
xres += x.x

var y = NumberReference(x: 10)
var yres = y.x
y = NumberReference(x: 20)
yres += y.x

创建和使用NumberValue结构的三行代码看起来非常简单。在汇编中,它有三行代码执行以下操作:

  • 创建NumberValue对象

  • 将其分配给x变量

  • x数字保存到xres变量中

    rax = __TFV4test11NumberValueCfMS0_FT1xSi_S0_(0x1);
    *__Tv4test1xVS_11NumberValue = rax;
    *__Tv4test4xresSi = rax;
    
    // NumberValue(x: 2)
    rax = __TFV4test11NumberValueCfMS0_FT1xSi_S0_(0x2);
    *__Tv4test1xVS_11NumberValue = rax;
    *__Tv4test4xresSi = *__Tv4test4xresSi + rax;
    

如您所见,创建第一个数字对象和第二个对象的代码看起来完全相同。现在让我们看看NumberReference对象的汇编代码:

rax = __TFC4test15NumberReferenceCfMS0_FT1xSi_S0_(0xa);
*__Tv4test1yCS_15NumberReference = rax;
*__Tv4test4yresSi = *(rax + 0x10);

如您所见,前三条线看起来几乎相同。它创建了一个NumberReference实例,将其分配给变量,获取 x 数字,并将其保存到yres变量中。创建第二个实例的代码更有趣:

// NumberReference(x: 10)
rax = __TFC4test15NumberReferenceCfMS0_FT1xSi_S0_(0x14);
rdi = *__Tv4test1yCS_15NumberReference;
*__Tv4test1yCS_15NumberReference = rax;
_swift_release(rdi, r14);
rax = *__Tv4test1yCS_15NumberReference;
*__Tv4test4yresSi = *__Tv4test4yresSi + *(rax + 0x10);

如您所见,它比NumberValue多了三条线。我们将新的实例分配给了y变量;旧的NumberReference超出了作用域,需要释放。这三条线与_swift_release函数有关。如果您进一步分析处理引用对象的汇编代码,您还会发现另一个 ARC 函数:_swift_retain;

现在您已经知道了值类型和引用类型之间的主要性能差异,让我们看看它们的性能表现。为此,让我们使用数字类型并在循环中进行一些计算。

var result = NumberValue(x: 0)
for i in 0...1_000 {
  var x = NumberValue(x: result.x + i)
  result = x
}

print(result)

var refResult = NumberReference(x: 0)
for i in 0...1_000 {
  var x = NumberReference(x: refResult.x + i)
  refResult = x
}
print(refResult)

输出:

NumberValue: 500500
NumberReference: 500500

第一个使用NumberValue结构的循环在编译时被 Swift 编译器完全消除。计算循环被转换为一个简单的整数结果;十六进制的 500500 等于 0x7a314。以下是第一个循环的汇编伪代码:

var_30 = 0x7a314;                          // save 500500
__TFSs5printU__FQ_T_(var_30, 0x1001ba538); // call print

如您所见,没有循环执行,结果是在编译时评估的。

使用NumberReference引用对象的第二个循环不能在编译时消除。汇编伪代码结构与源代码完全相同:

if (r14 == 0x0) {
  r14 = _swift_getInitializedObjCClass();
  *__TMLC4test15NumberReference = r14;
}
r15 = _swift_allocObject();
*(r15 + 0x10) = 0x0;
rbx = 0x0;
do {
  r13 = rbx + 0x1;
  rbx = rbx + *(r15 + 0x10);
  r12 = _swift_allocObject();
  *(r12 + 0x10) = rbx;
  _swift_release(r15, 0x18);
  r15 = r12;
  rbx = r13;
} while (r13 != 0x3e9);
var_38 = r12;
__TFSs5printU__FQ_T_(var_38, r14);

如您所见,使用值对象带来了更大的性能提升。作为一个例子,让我们测量该操作的性能,但将循环迭代次数增加到 1_00_000_000:

NumberValue Time - 0.000438838000263786
NumberReference Time - 8.49874957299835

这并不是一个公平的性能测量,因为具有值对象的变体实际上并没有执行任何操作。为了比较实际的执行速度,让我们以调试模式运行此代码:

小贴士

你不应该在调试模式下测量性能。

结果如下:

NumberValue Time - 4.31753185200068
NumberReference Time - 15.4483174900015

差异仍然令人印象深刻;值对象的速度快至四倍

Swift 数组和非安全 C 数组

每个人都知道 C 是一种超级快的编程语言,当遇到性能问题时,人们会求助于 C。在 Objective-C 中,使用 C 函数和类型非常容易;其名称就说明了这一点——C with Objects。

Swift 还支持与 C 类型和指针交互。尽管它可用,但被认为是一种危险的操作,因为你需要手动进行内存管理。你需要分配和销毁内存。这些类型在 Swift 中被称为 Unsafe,并以 Unsafe 前缀开头——例如:

  • UnsafePointer

  • UnsafeMutablePointer

  • UnsafeBufferPointer

    小贴士

    避免在 Swift 中使用 C 指针。它会给代码增加很多复杂性。

UnsafePointers 有三个主要的使用场景:

  • 函数参数

  • 创建指向现有变量的指针

  • 为指针分配内存

函数参数

首先,让我们学习如何使用指针。当你在 Swift 中设计 API 时,你不应该使用 UnsafePointers,但你可能会遇到需要与 C API 交互的情况:例如 Core Foundation。C 指针将暴露给 Swift,如下所示:

  • const Int * 作为 UnsafePointer<Int>

  • Int * 作为 UnsafeMutablePointer<Int>

当你在 Swift 中调用带有 UnsafePointer 参数的函数时,你可以通过使用 & 符号或数组将相同类型的变量作为输入输出参数传递:

var num = 10
var ar = [1, 2]

func printNumber(x: UnsafePointer<Int>) {
  x.memory
}

printNumber(&num)
printNumber(ar)

也可能传递 nil,但那样的话,我们的函数将会有一个空指针作为参数,访问其内存会导致应用程序崩溃:

printNumber(nil)

你将在屏幕上看到以下内容:

Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x0).

使用 UnsafeMutablePointer 非常相似。UnsafeMutablePointerUnsafePointer 之间的主要区别在于,可变指针可以修改其所指向变量的值。当使用数组作为具有 UnsafeMutablePointer 参数的函数的参数时,它们也需要作为输入输出参数传递。

func changeNumber(x: UnsafeMutablePointer<Int>) {
  x.memory = 9901
}

changeNumber(&num)
changeNumber(&ar)
num // 9901
ar // [9901, 2]

创建指向现有变量的指针

当你创建指针时,你可以使用 initialize 方法将其连接到现有变量。initialize 方法将返回一个函数,可以用来设置该变量的新值。

var num = 10
var ar = [1, 2]

var numPtr = UnsafeMutablePointer<Int>.initialize(&num)
numPtr(10)
num //10

var numArPtr = UnsafeMutablePointer<[Int]>.initialize(&ar)
numArPtr([1])
ar  //[1]

为指针分配内存

与指针一起工作的另一种方式是为它们分配内存。alloc 方法有一个参数:它将为多少个对象分配内存。在为指针分配内存后,你可以使用它。最后,你需要释放指针使用的内存。

var numberPtr = UnsafeMutablePointer<Int>.alloc(1)
numberPtr.memory = 20
numberPtr.memory // 20
numberPtr.dealloc(1)

UnsafeMutablePointer有许多有用的方法可供使用,例如用于移动指针前后移动的successorpredecessor方法,用于访问随机指针索引的subscript,以及其他许多方法。

你可以在developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithCAPIs.html了解更多关于与 C 指针交互的信息。

现在,你已经学会了如何在 Swift 中处理 C 指针,让我们跳到我们的主要目标:测量使用指针数组工作的速度。

比较 Swift 数组和未安全 C 数组

为了比较,我们将创建一个随机数字数组并对它们进行排序。主要目标不是找到在数组中排序数字的最有效方法,而是比较使用 C UnsafePointers和 Swift 数组类型时的性能。

首先,让我们使用UnsafeMutablePointer创建一个 C 样式的变体:

let count = 3_000_0
measure("C Arrays") {
  let array = UnsafeMutablePointer<Int>.alloc(count)
  for a in 0..<count {
    array[a] = Int(arc4random())
  }

  // Sort
  for _ in 0..<count {
    for j in 0..<count - 1 {
      if array[j] > array[j + 1] {
        swap(&array[j], &array[j + 1])
      }
    }
  }
  array.dealloc(count)
}

结果是:平均时间 - 1.31680929350041

现在,让我们使用 Swift 数组来做出相同的解决方案:

let count = 3_000_0
measure("Swift Arrays") {
  var array = Array(count: count, repeatedValue: 0)

  for i in 0..<count {
    array[i] = Int(arc4random())
  }

  // Sort
  for _ in 0..<count {
    for j in 0..<count - 1 {
      if array[j] > array[j + 1] {
        swap(&array[j], &array[j + 1])
      }
    }
  }
}

结果是:平均时间 - 1.30709397329978

Swift 数组在处理UnsafePointers时的性能与之前相同。

如你所见,代码看起来非常相似。数组的初始化和排序算法在两种变体中看起来完全相同。这是因为ArrayUnsafeMutablePointer都有子脚本方法。唯一的区别在于我们创建数组的方式:

对于UnsafeMutablePointer

let array = UnsafeMutablePointer<Int>.alloc(count)
...
array.dealloc(count)

对于 Swift 数组:

  var array = Array(count: count, repeatedValue: 0)

通常,Swift 数组提供了更多的功能,并且与它们一起工作要容易得多。例如,数组有排序、过滤和其他许多方法,但UnsafeMutablePointer没有。

指针操作总结

简而言之——Swift 数组更受欢迎,以下是原因。

与指针一起工作是一个不安全且危险的操作。你需要手动分配和释放内存。使用指针访问内存也非常危险,因为你可能会访问不属于你的其他内存。

UnsafePointers和 Swift 数组具有相同的性能特征。

避免使用 Objective-C

你已经了解到,在大多数情况下,Objective-C(及其动态运行时)比 Swift 运行得慢。Swift 与 Objective-C 之间的互操作性做得非常无缝,有时我们可以在 Swift 代码中使用 Objective-C 类型及其运行时,而不知道这一点。

当你在 Swift 代码中使用 Objective-C 类型时,Swift 实际上使用 Objective-C 运行时进行方法调度。正因为如此,Swift 不能对纯 Swift 类型进行相同的优化。让我们看看一个简单的例子:

  for _ in 0...100 {
    _ = NSObject()
  }

让我们阅读这段代码,并对 Swift 编译器如何优化这段代码做出一些假设。NSObject实例在循环体中从未使用过,因此我们可以消除创建对象。之后,我们就会有一个空循环,也可以将其消除。因此,我们会从执行中移除所有代码。

让我们通过查看生成的汇编伪代码来看看现实中发生了什么:

rbx = 0x65;
  do {
    rax = [_OBJC_CLASS_$_NSObject allocWithZone:0x0];
    rax = [rax init];
    [rax release];
    rbx = rbx - 0x1;
    COND = rbx != 0x0;
  } while (COND);

如你所见,没有代码被消除。这是由于 Objective-C 类型使用动态运行时调度方法,称为消息发送。

所有标准框架,如 Foundation 和 UIKit,都是用 Objective-C 编写的,所有类型,如 NSDate、NSURL、UIView 和 UITableView,都使用 Objective-C 运行时。它们的性能不如 Swift 类型,但我们可以在 Swift 中使用所有这些框架,这是非常好的。

在 Swift 中无法从 Objective-C 类型中移除 Objective-C 的动态运行时调度,所以我们唯一能做的就是学习如何明智地使用它们。

避免暴露 Swift 到 Objective-C

我们无法从 Objective-C 类型中移除运行时行为,但我们可以阻止 Swift 类型使用 Objective-C 运行时。

当 Swift 类继承自 Objective-C 类时,它也会继承其动态运行时行为。这也使其在 Objective-C 代码中使用变得可能。因为它继承了动态行为,所以 Swift 编译器无法执行最优化的优化(如前面示例中 NSObject 在循环中的情况所示)。让我们创建一个继承自 Objective-C 的简单类,并探索其行为:

class MyNSObject: NSObject {
}

for _ in 0...100 {
  _ = MyNSObject()
}

这段代码无法被消除,其汇编代码看起来与前面示例中的非常相似。我们可以通过简单地移除 NSObject 继承来改进这种行为。我们可以在本例中这样做,因为我们没有使用 NSObject 的任何功能。

class MyObject {
}

for _ in 0...100 {
  _ = MyObject()
}

在这种情况下,Swift 编译器能够执行最优化的优化,并从执行中消除所有代码。它移除了循环中创建的 MyObject,并在之后消除了空循环。

正如你所见,在 Swift 中使用 Objective-C 类会使 Swift 编译器变得不那么强大。只有在以下情况下才继承和使用 Objective-C 类:

  • 将 Swift 类型暴露给 Objective-C

  • 需要继承自 Objective-C 类,例如 UIView、UIViewController 等。只有在真正需要时才进行子类化

动态

有一个更危险的属性会给你的类型添加 Objective-C 的动态运行时行为:dynamic 关键字。当你使用 dynamic 修饰符声明成员时,它会将 Objective-C 运行时添加到类中。对该成员的访问永远不会被静态内联,而总是通过使用 Objective-C 的 Target-Action 机制动态调度。让我们来看一个简单的例子:

class MyObject {
  dynamic func getName() -> String {
    return "Name"
  }

  dynamic var fullName: String {
    return "Full Name"
  }
}

let object = MyObject()
object.fullName
object.getName()

即使这样一个小的例子也做了很多工作。应用 dynamic 关键字会导致许多问题:

  • 使用 _objc_msgSend 进行动态消息发送

  • 类型转换

  • 因为我们使用了 Objective-C 的动态调度方法,我们需要将我们的 "Name" Swift 字符串类型转换为 NSString;当我们从该函数调用中获取结果并返回到 Swift 代码时,我们需要进行一次从 NSString 到 Swift String 的额外转换

  • 没有优化和函数调用内联

  • 由于方法总是动态调度的,Swift 编译器无法进行内联优化或消除空方法。

避免使用 Objective-C 的总结

你应该避免使用 Objective-C 及其运行时行为来实现高性能。

你应该不惜一切代价避免使用 dynamic 关键字。你几乎永远不应该使用它。

只有在你需要那个类行为时才从 Objective-C 类继承,例如 UIView。只有在你需要将你的类型暴露给 Objective-C 时才使用 @objc 属性。

总结

在本章中,我们讨论了许多与 Swift 性能相关的话题。首先,我们需要了解我们需要改进什么,并启用优化设置以获得最佳性能。

内存使用对于实现高性能非常重要。首先,我们讨论了使用常量如何对性能产生积极影响。第二个且更为重要的例子反映了使用值类型和结构体如何减少内存使用,并通过使用快速栈内存来提高性能。

我们讨论的第三个重要主题是方法调度。我们分析了比较了 Objective-C 的动态调度和 Swift 的静态调度。通过查看汇编代码,我们看到了 Swift 实际上是如何进行方法调度的,以及它是如何通过提高性能来受益的。

我们还讨论了一些可能降低性能的危险操作,这些操作应该避免。

在下一章中,我们将学习更多关于不同数据结构的知识:它们之间的差异和性能特征。

第五章。选择正确的数据结构

在上一章中,我们讨论了使 Swift 运行快速的 Swift 特定功能。为特定用例选择正确的数据结构同样重要。在本章中,我们将讨论不同的数据结构、它们的区别以及何时选择一个而不是另一个。

在本章中,我们将涵盖以下主题:

  • 数据结构概述

  • Swift 标准库集合

  • 数组、集合和字典

  • 使用 Accelerate 和 Surge 加速

数据结构概述

每种编程语言都有内置的原始数据类型,例如整数、双精度浮点数、字符、字符串和布尔值。Swift 编程语言还有一些更复杂的数据类型,例如枚举、可选和元组。通过组合原始类型,我们可以构建更复杂的数据类型。要组合它们,我们使用结构和类。

数据结构是以特定方式组织数据的一种方式,以便它可以高效地用于特定任务,例如搜索、检查是否存在以及快速更新值。

集合类型

创建一个新的类型并为其选择正确的类型,无论是值类型还是引用类型,是一项重要的任务,我们之前在第二章中已经讨论过,即在 Swift 中构建良好的应用程序架构。当我们将相同类型的多个实例放入集合中时,对性能的影响更大。为特定任务选择正确的集合非常重要。

Swift 有一些强大的构建集合;我们首先应该看看它们。

Swift 标准库集合

在你的应用程序中,你经常会使用不同的集合来存储和处理数据。Swift 有三种不同的内置集合类型:数组、字典和集合。

Swift 标准库还提供了许多用于处理这些集合的函数,例如排序、查找、过滤、映射等。这些函数具有非常高效的实现,你应该使用它们而不是自己实现。首先,让我们看看不同的集合。

数组

数组是有序值集合,通过索引提供对其元素的访问。这是一个非常简单且广为人知的集合。你会在这些情况下使用数组:

  • 简单元素存储(通常从末尾添加/删除)

  • 元素需要有序

  • 随机访问元素

数组通常实现为一个连续的内存块,在其中存储值。因为内存块通常位于彼此旁边,所以对元素的访问通常可以转换为简单的指针算术:第三个元素 = 数组起始位置 + (2 * 元素大小)

数组

使用数组

数组非常适合存储用于 UITableView 的数据。项目需要排序。我们需要知道项目数量,通过索引获取项目,并且能够编辑集合。当你需要存储两个或更多相同类型的对象时,你会使用数组。

数组是一个非常简单且灵活的集合。但由于这一点,它们在应该使用集合、字典或其他自定义集合的情况下经常被过度使用。

快速操作

数组对于某些操作具有非常高的性能,这些操作的复杂度为常数 O(1),不会随着数组大小的增加而增加。你可以自由地使用它们:

  • 访问元素:要访问元素,请使用这些操作,array[i]array.firstarray.last。大约需要 81 纳秒,或 0.000000081 秒。

  • 添加元素:要添加元素,请使用此操作 array.append(i)。大约需要 100 纳秒或 0.0000001 秒。

在数组的开始位置和随机位置插入和删除元素也是一个非常快的操作,但它具有 O(n) 的复杂度。它随着数组大小的增加而增加,如下所示:

数组大小 时间(秒)
0 to 50_000 0.00001
500_000 0.00019
5_000_000 0.0043
50_000_000 0.040
500_000_000 0.32

较慢的操作

数组上的一些其他操作随着数组大小的增加而急剧增加。使用此类方法时请小心。

搜索

查找元素具有 O(n) 的复杂度。数组中的元素越多,查找元素是否存在所需的时间就越长。为了确定元素是否存在于数组中,它必须遍历每个元素并比较它们:

let array: [Int]
let index = array.indexOf(3445)

如果搜索不是你在集合上执行的主要操作,并且数组的大小,例如,为 500_000_000 个元素,搜索将需要 0.5 秒。如果你必须非常频繁地执行搜索,并且这样做非常关键,请使用集合进行搜索操作,或者对数组进行排序并使用更有效的搜索算法,例如二分搜索。

排序

排序的复杂度甚至比搜索元素还要大;它具有 O(n * n) 的复杂度。排序需要遍历数组以找到某个元素的正确位置,并对其中的每个元素重复此操作。sort 标准库函数有一个非常高效的实现,你应该使用它。它根据数组大小使用不同的排序算法。因为排序很昂贵,你应该缓存排序结果并在需要时重用它。对包含 500_000_000 个 int 元素的数组进行排序大约需要 67 秒。

集合

集合是一个无序的集合,用于存储唯一的对象。集合用于检查成员资格。

通常,集合被实现为哈希表。集合中的元素必须符合可哈希可比较协议。

当您向集合添加元素或搜索元素时,它使用元素的哈希函数来找到该元素在存储中的索引。正因为如此,集合上的许多操作都非常快,并且具有 O(1) 的复杂度。

集合

使用集合

集合非常适合检查对象是否存在于集合中。此外,它们非常适合获取两个集合之间的差异,例如,找出添加或删除的对象。

集合有两个重要的限制。它们是无序的,并且不能包含重复项:

var numbers: Set = [1, 1, 2, 3, 3, 4]
// {2, 3, 1, 4}

但是,集合将这些限制转化为特性。正因为如此,并且因为它使用哈希表来存储其元素,所以在某些操作中实现了令人难以置信的性能,这些操作的复杂度为 O(1),并且不会随着集合大小的增加而增加。以下是一些操作:

  • 插入:

    numbers.insert(10)
    
  • 查找:containsIndexOfsubscript

    let number = numbers.contains(10)
    let foundIndex =  numbers.indexOf(101)
    let start: SetIndex = numbers.startIndex
    let first = numbers[start]
    
  • 删除:removeremoveAtIndex

    let number = numbers.remove(27)
    let number = numbers.removeAtIndex(numbers.startIndex)
    

所有这些操作都不到 6 微秒,即 0.000006 秒,即使是包含 50_000_000 个元素的集合。

提示

如果你打算进行元素的密集搜索,请使用集合。你可以同时使用数组来存储数据,以及集合来进行搜索操作。这将使用两倍多的内存,但搜索将瞬间完成。

集合操作

您可以对两个不同的集合执行基本集合操作,例如合并、提取和获取公共值。

集合操作

这些操作也非常快,但它们的复杂度为 O(n) 或 O(log n),并且随着大小的增加,处理数据所需的时间也会增加:

let set = makeRandomSet(size)
let otherSet = makeRandomSet(set.count)

set.union(otherSet)
set.subtract(otherSet)
set.intersect(otherSet)
set.exclusiveOr(otherSet)

这些操作的性能相当令人印象深刻,如下表所示:

集合大小 并集 差集 交集 异或
100 x 100 0.000015 0.000012 0.000013 0.00001
500_000 x 500_000 0.11 0.072 0.055 0.13

如果我们查看这些方法的声明,我们会看到它们接受的不是集合,而是 SequenceType

func union<S : SequenceType where Element == Element>(sequence: S) -> Set<Element>

我们可以使用这些集合方法与任何 SequenceType。让我们尝试使用数组而不是集合,看看是否会有任何区别:

let set = makeRandomSet(size)
let otherSequence = makeRandomArray(set.count)

set.union(otherSequence)
...
集合大小 x 数组大小 并集 差集 交集 异或
100 x 100 0.000013 0.0000045 0.000026 0.000032
500_000 x 500_000 0.10 0.058 0.13 0.18

如您所见,intersectexclusiveOr 在集合上表现更好。尽管差异很小,不会对整体应用性能产生重大影响,但这仍然是一个您应该记住的重要观察。

让我们来看看集合中的一种更多方法——isSubsetOf

func isSubsetOf<S : SequenceType where Element == Element>(sequence: S) -> Bool

它也具有 SequenceType 参数,因此可以使用集合和数组:

let set = makeRandomSet()

var otherSequence = Array(set)
otherSequence.append(random())
set.isSubsetOf(otherSequence)

var otherSet = set
otherSet.insert(random())
set.isSubsetOf(otherSequence)

结果非常有趣。当集合大小等于 5_000_000 时,使用数组参数的 isSubsetOf 需要 4 分钟,而使用集合则不到 1 秒。

提示

对于 isSubsetOf 方法,始终首选使用集合作为参数。

大小 isSubsetOf (数组) isSubsetOf (集合)
50_000 0.11 秒 0.0045 秒
5_000_000 237.2 秒 0.46 秒

字典

字典是一个无序集合,用于存储唯一的键值对。当需要通过键快速查找对象时,字典非常有用

字典也使用哈希表来存储它们的键和值。因此,字典具有与集合相似的性能特征。当需要连接两个对象并对其进行即时搜索和查找时,字典非常有用。

字典

字典集合是一个非常简单的类型。它没有很多自己的方法。主要功能是查询键的值、更新它和删除它:

var capital = ["Germany" : "Berlin", "France"  : "Paris"]

capital["Norway"] = "Oslo"
capital.removeValueForKey("France")
capital["France"] = nil

if let index = capital.indexForKey("Spain") {
  print("found Spain at: \(index)")
}
capital.values

在你有两个数组并且想要找到它们之间联系的情况下,使用字典将导致性能的大幅提升。

集合内存分配

每个集合在实例化其实例时都有非常相似的性能优化。创建集合实例有三种不同的方式。

空的

你可以创建一个空集合。所有数组、集合和字典都有一个空的 init 方法:

let array = [Int]()
let set = Set<Int>()
let dic = [String : Int]()

预留容量

另一种方法是实例化一个集合并预留所需的内存容量。所有集合都具有动态大小,并在需要时分配更多内存。当你知道你将在集合中存储多少元素时,预先分配所需的确切数量的内存是有用的:

var array = [Int]()
array.reserveCapacity(500_000)

var set = Set<Int>(minimumCapacity: 500_000)
var dic = String : Int

默认值

数组还有一个实例化的方式。你可以创建一个数组,其中所有元素的默认值都已设置:

var array = Int
array[i] = 10

这里是秒数的结果:

大小 方法 数组 集合大小 字典
500
空的 5.2e-06 2.4e-05 2.4e-05
容量 8.8e-07 1.6e-06 4.7e-06
默认值 4.8e-07
50_000_000
空的 1.29 11.7 12.9
容量 1.13 9.4 10.9
默认值 1.043

如从结果中可以看出,当你知道集合的大小,即使是很小的尺寸,为集合预留一些容量总是更好的。

对于数组,使用默认值是最快的方式,但你要记住数组是用默认值填充的,你必须处理它们或者用真实值替换它们。

此外,你可以看到创建一个数组是一个非常快速的操作,即使是大尺寸的数组也是如此。这是数组相对于其他集合的优势之一。

CollectionType 协议方法

所提到的所有集合——数组、集合和字典——都实现了 CollectionType 协议。正因为如此,它们可以互换使用。你可以在需要 CollectionType 方法的地方使用任何一个。一个例子是具有 CollectionType 参数的函数:

func useCollection<T: CollectionType>(x: T) {
  print("collection has \(x.count) elements")
}

let array = [1, 2, 3]
let set: Set = [2, 2, 3, 4, 5]
let dic = ["A" : 1, "B" : 2]

useCollection(array)
useCollection(set)
useCollection(dic)

协议扩展

另一个非常有用的功能是协议扩展。使用协议扩展,我们可以在协议中直接添加方法和属性的实现。所有符合该协议的类型都可以免费使用这些方法。让我们向一个CollectionType方法添加我们自己的属性:

extension CollectionType {
  var middle: Self.Index.Distance {
    return count / 2
  }
}

array.middle
set.middle
dic.middle

符合该协议的类型可以为该方法提供自己的实现。在这种情况下,将使用该类型的实现而不是协议扩展中定义的实现:

extension Dictionary {
  var middle: Dictionary.Index.Distance {
    print("Dictionary middle")
    return count / 2 + 100 // :( wrong middle index 
  }
}
dic.middle // 101 :)

CollectionType 协议非常依赖这个功能,并且有许多方法和属性可用于 CollectionType,例如 isEmptyfirstdropFirst(n: Int)mapindexOf 以及更多。让我们看看 isEmptydropFirst(n: Int)

array.isEmpty
set.isEmpty
dic.isEmpty

array.dropFirst(1)
set.dropFirst(1)
dic.dropFirst(1)

让我们检查这些方法。按住 command 键并点击 isEmpty 属性以跳转到其声明。数组使用 CollectionType 版本的 isEmpty,但集合和字典为 isEmpty 属性提供了自己的实现。集合和字典之所以这样做,是因为它们存储元素的方式不同,并且它们可以提供更好、更高效的 isEmpty 属性实现。如果类型能够做得更好,它们通常会使用自己的实现而不是使用协议的默认实现。

你可以在 The Swift Programming Language 书籍的 Protocols 部分了解更多关于协议扩展的信息,链接为 developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html

加速和激增

iOS 和 OS X SDK 都有一个非常强大的框架,它提供了用于处理矩阵、向量、信号、图像处理和数学运算的高性能函数。这个框架被称为 Accelerate 框架。Accelerate 框架相当庞大,所以我们将只查看与处理集合相关的一部分;它是 vDSP 部分。你可以在 developer.apple.com/library/prerelease/ios/documentation/Accelerate/Reference/vDSPRef/index.html 上了解更多信息。

首先,让我们使用 Swift 标准库实现非常基本的映射、计算和求和操作:

let array = [1.0, 2.0]
let result = array.map { $0 + 3.0 }
result // [4.0, 5.0]

let sum = array.reduce(0, combine: +)
sum // 3

这段代码非常清晰易读,不需要任何注释。让我们尝试使用 Accelerate 来实现同样的功能:

let array = [1.0, 2.0]
var result = Double

var add = 3.0
vDSP_vsaddD(array, 1, &add, &result, 1, vDSP_Length(array.count))
result // [4.0, 5.0]

var sum = 0.0
vDSP_sveD(array, 1, &sum, vDSP_Length(array.count))
sum // 3

如您所见,代码比上一个版本更复杂。vDSP 库与向量和矩阵一起工作。对于 vDSP_vsaddD 函数调用,我们传递一个输入数组。第二个参数给出数组中元素之间的距离。因为数组中的元素是相邻的,所以我们使用 1。第三个是我们的第二个参数,第四和第五个是类似于第一个和第二个参数的结果数组。

因此,Accelerate 代码更复杂,但性能更好。让我们来比较一下:

let array = makeRandomDoubleArray(size)
var result = [Double]()

measure("map") {
  result = array.map { $0 + 3.0 }
}

// vDSP Version
let array = makeRandomDoubleArray(size)
var result = Double
var add = 3.0

measure("vDSP_vsaddD") {
  vDSP_vsaddD(array, 1, &add, &result, 1, vDSP_Length(array.count))
}

对于具有 500_000_000 个元素的较大数组,结果非常有趣。map 函数需要 5.1 秒,而 vDSP_vsaddD 需要 0.6 秒。它几乎快了 10 倍。

是的!Accelerate 的性能要好得多,但源代码变得非常复杂。然而,有一个解决方案。我们可以创建一个漂亮的 API 包装器来与 Accelerate 框架一起工作。幸运的是,这已经完成了。有一个非常棒的名为 Surge 的开源 Swift 框架。您可以从 GitHub github.com/mattt/Surge 下载它。

下载后,将 Surge 框架项目添加到您的项目中。将其作为链接框架添加,然后您就可以使用它了。现在,通过使用 Surge,代码看起来非常漂亮,并且性能出色。以下是一个使用 Surge 计算所有元素总和的示例。它甚至比使用 reduce 方法更优雅:

import Surge
let numbers = makeRandomDoubleArray(size)
let sum = Surge.sum(numbers)

其他集合

我们已经介绍了三个主要的 Swift 标准库集合。还有一些不太为人所知但经常使用的辅助集合,例如 ArraySliceMapCollectionCollectionOfOneContiguousArrayEmptyCollectionFilterCollection。如果您想了解更多关于它们的信息,只需按 command 键并点击任何类型。您将看到 Swift 标准库的内容。然后,只需探索它!

如果需要,您也可以实现自己的集合。Swift 泛型允许您创建非常抽象的集合,可以与任何类型一起使用。例如,它可以是栈、链表、二叉树或任何满足您需求的集合。

摘要

在本章中,我们讨论了选择正确数据类型的重要性。我们介绍了具有其功能和限制的 Swift 标准库集合。您学习了哪个集合更适合哪种用例。此外,我们还展示了使用这些集合执行不同操作时的性能特征。此外,我们还提供了一些建议,以改善集合的性能和内存分配。

在下一章中,我们将看到如何创建一个有助于实现高性能的应用程序架构。

第六章:为高性能构建应用程序架构

在前面的章节中,我们讨论了不同的方法来改进代码以实现高性能。我们主要集中在一个小的代码部分以及如何改进一个函数、一个算法和一个数据结构。在本章中,我们将集中讨论更高层次的内容。我们将讨论如何设计一个可扩展、可维护且高性能的应用程序架构。

在本章中,我们将讨论以下主题:

  • 高性能和并发概述

  • 避免状态

  • 分而治之

  • 设计异步架构

实现高性能

提高应用程序性能的一种方法是通过并行运行代码。这不仅使我们能够更快地运行代码并更快地得到结果,而且还使主线程从做大量工作并被阻塞中解放出来。你应该知道主线程负责事件和用户输入处理。所有 UI 工作都在主线程上执行,为了实现真正流畅的用户交互,我们应该在主线程上尽可能少地做工作。

并行运行代码可能是一个棘手的问题,有时它可能导致操作运行时间的增加。构建稳固的并发应用程序架构也不是一个简单任务,你应该仔细规划。

为了充分利用并发,了解我们拥有的硬件,这对于我们来说非常有用,这一点非常重要。

设备架构

为了能够实现真正的高性能,首先我们需要学习和理解我们有哪些工具可以利用。如果你正在开发 iOS 应用程序,你的应用程序将在 iPhone 和 iPad 上运行;对于 OS X,它将在 Mac 上运行。尽管 iPhone 和 Mac 可能看起来有很大的不同,但它们共享相同的基本概念,我们可以将 Mac 视为一个更强大的 iPad 设备。

现在,所有计算机甚至手机都使用多核处理器,这使我们能够同时并行执行许多指令。从 iPhone 4s 开始,所有 iPhone 都配备了双核处理器,而 iPad Air 2 甚至配备了 3 核处理器。我们应该充分利用这种力量并利用它。

让我们看看我们如何设计可以在多核处理器上并行运行的代码。

并发概述

默认情况下,当你创建一个应用程序时,它将在单线程环境中运行代码,即主线程。例如,iOS 应用程序会在主线程上调用application: didFinishLaunchingWithOptions方法。

一个更简单的例子是一个 OS X 命令行工具应用程序。它只有一个文件:main.swift。当你启动它时,系统会创建一个主线程,并在该线程上运行main.swift文件中的所有代码。

对于测试代码,playgrounds 是最好的。默认情况下,playgrounds 在执行完最后一行代码后会停止,不会等待并发代码执行完成。我们可以通过告诉 playgrounds 无限期地运行来改变这种行为。为此,请在 playground 文件中包含这两行代码:

import XCPlayground
XCPSetExecutionShouldContinueIndefinitely()

现在,我们可以开始玩并发了。我们需要做的第一件事是为了并行运行代码,需要在一个不同的线程上安排一个任务。我们可以通过以下方式安排一个任务以进行并发执行:

  • 线程

  • GCDGrand Central Dispatch

  • 操作队列

线程

作为第一个选项,我们可以使用线程。线程是最底层的 API。所有并发都是建立在线程之上,并运行多个线程。我们可以使用来自 Foundation 框架的NSThread。这样做最简单的方式是创建一个新的类,其中包含一个将成为我们新线程起点的方法。

让我们看看我们如何安排新的线程:

class Handler: NSObject {
  @objc class func run() {
    print("run")
  }
}

NSThread.detachNewThreadSelector("run", toTarget: Handler.self, withObject: nil)

let thread = NSThread(target: Handler.self, selector: "run", object: nil)
thread.start()

你可以通过两种方式创建一个新线程:使用detachNewThreadSelector函数或创建NSThread的实例并使用start函数。我们必须将run函数标记为@objc属性,因为我们将其用作创建线程时的选择器,而NSThread是一个使用动态分派进行方法调用的 Objective-C 类。

NSObject有一个简单的 API 可以在不同的线程上执行一个方法。因为我们的处理程序继承自NSObject,所以我们可以使用它。

Handler.performSelectorInBackground("run", withObject: nil)

另一种方法是创建NSThread的子类并覆盖线程的起点,即main函数。这样我们就不需要处理程序类了。

class MyWorker: NSThread {

  override func main() {
    print("Do work here")
  }
}

let worker = MyWorker()
worker.start()

线程复杂度

尽管这里的代码相当简单,但处理线程却是一个相当复杂的操作。我们需要注意管理线程的状态,正确地终止它,并释放线程使用的资源。

创建一个新线程是一个非常昂贵且耗时的操作,我们应该尽可能避免它。解决这个问题的方法是重用创建的线程。创建和管理线程池会给应用程序增加不必要的复杂性。

当你需要在线程之间进行通信并同步数据时,这个过程变得更加困难。

线程解决方案

现在我们不是在解决我们想要并行运行的初始任务,而是在花费时间管理这个并发执行系统的复杂性。幸运的是,我们不需要这样做,因为有一个解决方案:不要使用线程

iOS 和 Mac 并发编程指南建议不要使用线程,而是选择高级 API,如 GCD 或操作队列。

小贴士

本章仅展示线程 API 以供一般了解。你几乎永远不应该使用线程;请使用 GCD。

GCD

GCDGrand Central Dispatch)是一个建立在线程之上的高级 API,为你处理线程管理的所有方面。与线程工作不同,GCD 提供了一个队列和任务抽象。你将任务调度到队列中执行,队列负责其他所有事情。让我们看看我们如何使用 GCD 重写我们的代码:

let bgQueue = dispatch_get_global_queue(QOS_CLASS_BACKGROUND,0)
dispatch_async(bgQueue) {
  print("run")
}

如你所见,代码从一开始就看起来更简单。在我们深入细节之前,让我们看看 GCD 及其概念:

  • 队列

  • 任务

  • 向队列添加任务

队列

队列 是一个负责管理和执行其任务的结构。队列是一个先进先出数据结构。这意味着队列中的任务以它们被添加到队列中的顺序启动。

注意

先入先出意味着任务以相同的顺序启动,但这并不意味着它们不能同时执行。并发队列可以同时启动许多任务。

队列本身没有太多功能。你需要执行的主要操作是创建一个队列或获取全局队列中的一个。

有三种队列类型:

  • 主队列

  • 并发:全局和自定义队列

  • 串行

主队列

主队列 代表与主线程关联的队列。它按顺序串行运行任务,一个接一个。你通常会使用这个队列将其他后台队列的执行结果传递到主队列以更新 UI 状态。你可以通过调用 dispatch_get_main_queue 函数来获取主队列。

let mainQueue = dispatch_get_main_queue()

并发队列

并发队列 并行运行其任务。获取并发队列的最简单方法是使用全局并发队列。

func dispatch_get_global_queue(identifier: Int, flags: UInt) -> dispatch_queue_t!

要获取全局队列,我们需要指定所需的优先级类型。有五种类型的队列,任务优先级递减。USER_INTERACTIVE 是优先级最高的队列,而 BACKGROUND 是最低的。

  • QOS_CLASS_USER_INTERACTIVE

  • QOS_CLASS_USER_INITIATED

  • QOS_CLASS_DEFAULT

  • QOS_CLASS_UTILITY

  • QOS_CLASS_BACKGROUND

    小贴士

    同样可用的还有旧的 DISPATCH_QUEUE_PRIORITY 常量,当指定队列优先级类型而不是 QOS_CLASS 时可以使用,但 QOS_CLASS 更受欢迎。

第二个标志参数是保留的,从不使用,所以我们只使用 0。全局队列可供整个系统使用,任何人都可以向它们添加任务。当你只需要运行一些任务时,这是一个完美的选择。

自定义队列

当你需要进行更复杂的处理并完全控制添加到队列中的任务时,你可以创建自己的队列。自定义队列非常适合当你需要通知所有任务完成时,或者需要在任务之间进行更复杂的同步时。

你可以创建并发和串行队列。串行队列一次执行一个任务,一个接一个,而不是并发执行。

let concurentQ = dispatch_queue_create("my-c", DISPATCH_QUEUE_CONCURRENT)
let serialQ = dispatch_queue_create("my-s", DISPATCH_QUEUE_SERIAL)

任务

任务 是需要运行的一段代码。任务被定义为 dispatch_block_t 类型,并定义为 () -> Void。我们可以使用闭包或函数作为任务。

typealias dispatch_block_t = () -> Void

let tasks: dispatch_block_t = {
  print("do Work")
}

func doWork() {
  print("do Work Function")
}

将任务添加到队列中

我们有一个队列,我们有一个想要运行的任务。为了在特定队列上运行任务,我们需要将其调度到该队列。我们可以用两种方式来做这件事:

  • 同步: dispatch_sync

  • 异步: dispatch_async

这两个函数都非常简单,并且具有相同的类型。它们之间的唯一区别在于它们的名称和工作方式。

dispatch_sync(queue: dispatch_queue_t, _ block: dispatch_block_t)
dispatch_async(queue: dispatch_queue_t, _ block: dispatch_block_t)

同步调度

同步调度提交一个任务以执行,并等待任务完成。

dispatch_sync(queue) { ... }

当您使用并发队列并以同步方式向其调度任务时,队列可以同时运行多个任务,但 dispatch_sync 方法会等待您提交的任务完成。

let queue = dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)

dispatch_sync(queue) { print("Task 1") }
print("1 Done")

dispatch_sync(queue) { print("Task 2") }
print("2 Done")

小贴士

从同一队列中执行的任务中永远不要调用 dispatch_sync 函数。这会导致串行队列发生死锁,并且对于并发队列也应避免。

dispatch_sync(queue) {
  dispatch_sync(queue) {
    print("Never called") // Don't do this
  }
}

在这个示例中,print("1 完成") 指令和其余代码将等待 任务 1 完成。

异步调度

另一方面,异步调度提交一个任务以执行,并立即返回。

dispatch_async(queue) { ... }

如果我们使用之前的示例并将其更改为使用 dispatch_async,则 1 完成 将不会等待任务完成。我们还可以通过使用 sleep 函数冻结当前线程来模拟额外的工作。

let queue = dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)

dispatch_async(queue) {
  sleep(2) // sleep for 2 seconds
  print("Task 1")
}
print("1 Submitted")

dispatch_async(queue) { print("Task 2") }
print("2 Submitted")

因此,任务 2任务 1 提交执行后立即提交执行,并在 任务 1 完成执行之前完成。以下是控制台输出:

1 Submitted
2 Submitted
Task 2
Task 1

GCD 还有一些用于同步提交任务的强大工具,但在这里我们不会介绍它们。如果您想了解更多,请阅读 Apple 库文档中的 并发编程指南 文章,网址为 developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide

操作队列

NSOperationQueue 是建立在 GCD 之上的,它提供了更高层次的抽象和 API,使我们能够控制应用程序的控制流。

这个概念与 GCD 非常相似;它有一个队列,您可以将任务添加到特定的队列中。

let queue = NSOperationQueue()
queue.addOperationWithBlock {
  print("Some task")
}

NSOperationQueue.mainQueue().addOperationWithBlock {
  print("Some task")
}

NSOperationQueue 提供了一个更高层次的 API,但它也比 GCD 略慢。当需要链式任务、相互依赖的任务或需要取消的任务来控制应用程序流程时,NSOperationQueue 非常适合。您可以通过使用 GCD 实现相同的功能,但这需要实现一些已经由 NSOperationQueue 实现的额外逻辑。

当您需要执行一个任务并获取结果,而不需要控制应用程序流程时,GCD 工作得非常好。

在本章的后续内容中,我们将使用 GCD 进行并发编程。现在,让我们继续学习一些技巧,这些技巧将帮助我们使我们的代码架构在并发编程中更加稳固。

设计异步代码

异步代码的第一个特点是它立即返回,并在操作完成时通知调用者。最好的解决方案是返回计算结果。这样我们就能得到更多函数风格的输入 -> 输出函数风格。

让我们看看这个简单的例子。这段代码有很多问题,我们将全部重构它们。

class SalesData {

  var revenue: [Int]
  var average: Int?

  init (revenue: [Int]) {
    self.revenue = revenue
  }

  func calculateAverage() {

    let queue = GCD.backgroundQueue()
    dispatch_async(queue) {

      var sum = 0
      for index in self.revenue.indices {
      sum += self.revenue[index]
      }

      self.average = sum / self.revenue.count
    }
  }
}

注意

我们创建了一个 GCD 结构,它提供了一个很好的 API 来处理 GCD 代码。在先前的例子中,我们使用了 GCD 的backgroundQueue函数;下面是它的实现:

struct GCD {
  static func backgroundQueue() -> dispatch_queue_t {
    return dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)
  }
}

总体来说,那个例子中的计算代码真的很糟糕,我们可以通过使用reduce方法来改进它,这实际上会解决许多问题,并使代码更安全、更易读。

let sum = self.revenue.reduce(0, combine: +)

但那个例子中的主要目的是展示它可能有多危险,以及你可能会遇到哪些问题。

让我们使用这段代码来看看问题:

let data = SalesData(revenue: makeRandomArray(100))
data.calculateAverage()
data.average // nil

问题在于calculateAverage立即返回,正如它应该做的那样,此时并没有计算平均值。为了解决这个问题,所有异步代码都应该有一种方式来通知调用者在任务完成时。最简单的方法是添加一个回调完成函数作为参数。

func calculateAverage(result: () -> Void ) {
  ...

  self.average = sum / self.revenue.count
  result()
}

现在,当使用这段代码时,我们可以为结果回调参数使用一个简洁明了的尾随闭包语法。

let data = SalesData(revenue: makeRandomArray(100))
data.calculateAverage {
  print(data.average)
}

这段代码有一个非常重要的问题。它是调用result回调函数在后台线程上。这意味着我们传递给data.calculateAverage的闭包将在后台调用,但对于我们来说,这并没有文档说明,这种行为也不明确。我们假设我们将得到在主线程上调用该闭包,因为我们是从主线程调用calculateAverage函数的。让我们这样做。我们需要做的是切换到主队列,并在主线程上调用result

dispatch_async(GCD.mainQueue()) {
  result()
}

最佳实践是默认在主队列上调用回调方法,除非需要其他行为。如果你需要在特殊队列上调用回调,那么它应该作为参数传递给函数。

这段代码可以工作,但仍然有一个可以改进的地方。当结果回调被调用时,我们首先获取average实例。如果结果回调返回其计算结果会更好。

提示

一般而言,对于函数来说,以输入和返回结果的方式X -> Y进行函数式编程是一种很好的风格。这些函数更容易使用和测试。

让我们重构这段代码,将平均值结果传递给回调函数:

func calculateAverage(result: (Int?) -> Void ) {
  ...

  self.average = sum / self.revenue.count

  dispatch_async(GCD.mainQueue()) {
    result(self.average)
  }
}

// Use case
let data = SalesData(revenue: makeRandomArray(1000))
data.calculateAverage { average in
  print(average)
}

变化不大,但好处相当广泛。当我们使用calculateAverage函数时,我们直接在闭包中作为参数得到结果。现在我们不需要访问SalesData的实例变量。SalesData更像是一个封闭的盒子类型,具有隐藏的实现细节,因此我们将在未来能够进行更多的重构。

避免状态

第一条规则是避免状态。当你同时做两件事时,这两个过程应该尽可能独立和隔离。它们之间不应该有任何了解,也不应该共享任何可变资源。如果它们确实这样做了,那么我们就需要处理对这些共享资源的访问同步,这会给我们的系统带来我们不想要的复杂性。目前在我们的代码中,我们有两个状态:一个revenue数字数组和average结果。这两个过程都可以访问这个状态。

那段代码的第一个问题是它引用了自己。当你尝试访问闭包作用域之外的实例变量或方法时,你会看到一个错误信息:在闭包中引用属性'revenue'需要显式的'self.'来使捕获语义明确

Xcode 也会提出对这个问题的修复,添加显式的自我捕获。这将解决 Xcode 的错误,但不会解决根本问题。当你看到这个错误时,停下来重新思考你的代码设计;在某些情况下,改变代码会更好,就像在我们的例子中一样。

避免状态

第二个问题是拥有可变状态并修改实例变量。让我们再次使用我们的上一个例子,看看为什么在并发代码中拥有状态和访问实例变量是一个坏主意:

let data = SalesData(revenue: makeRandomArray(1000))
data.calculateAverage { average in
  print(average)
}
data.revenue.removeAll(keepCapacity: false)

如果你运行这段代码,它将因为通过索引操作从数组中获取数字而崩溃,出现致命错误:数组索引超出范围

sum += self.revenue[index]

发生在这里的情况是,当我们调用calculateAverage时,收入数组有数据,但后来我们移除了所有的收入数字,数组变空了;然而,我们迭代的索引指向旧的数组大小,我们正在尝试访问超出范围的数组索引。

为了解决这个问题,我们应该始终尝试移除状态。一种方法是将所需的数据作为参数传递给函数,或者如果你想要像我们这样的某些状态,捕获闭包的不可变值。

捕获列表

使这段代码变得更好的第一步是移除访问可变数组变量。解决这个问题的最简单方法是创建一个局部常量并在闭包中使用它。

let revenue = self.revenue
dispatch_async(queue) {
  var sum = 0
  for index in revenue.indices {
    sum += revenue [index]
  }

  self.average = sum / revenue.count
  ...
}

这个解决方案是可行的,因为修改 revenue 实例变量不会影响我们创建的局部常量。这段代码有一个小问题。常量在闭包外部可见,但它的意图是仅在闭包内部使用。如果能将它移到闭包中会更好。我们可以通过使用闭包的捕获列表来实现这一点。我们唯一需要做的改动是移除局部常量声明,并将其添加到闭包捕获列表中。其余代码保持不变。

dispatch_async(queue) { [revenue] in
  ...
}

在这个例子中,我们使用了非常短的捕获列表表示法,但我们也可以为我们要捕获的常量提供一个替代名称,并添加额外的 ARC weakunowned 属性。

dispatch_async(queue) { [numbers = revenue] in  ...

不可变状态

在并发代码中拥有状态是一个糟糕的设计,但有两种状态:可变和不可变。无论如何,你都需要在应用程序中拥有某种状态。如果你要拥有状态,请使其不可变;这样你就可以确保它不会被更改,并且可以安全地与之交互。

如果我们查看之前的代码示例,我们可以使 revenue 数字不可变,这将解决问题:

class SalesData {

  let revenue: [Int]
  ...
}

我们首先会做的微小改动是将收入数字数组改为不可变。因为 revenue 数组是不可变的,所以我们创建实例后无法修改它,因此我们需要移除这段代码。

  data.revenue.removeAll(keepCapacity: false)

因为 revenue 现在是不可变的,所以可以在并发代码中使用它,因此我们可以移除捕获列表,并直接使用不可变的收入,就像我们之前使用 self 一样。

func calculateAverage(result: (Int?) -> Void ) {

  let queue = GCD.backgroundQueue()
  dispatch_async(queue) {

    var sum = 0
    for index in self.revenue.indices {
      sum += self.revenue[index]
    }

    self.average = sum / self.revenue.count
    dispatch_async(GCD.mainQueue()) {
      result(self.average)
    }
  }
}

SalesData 包含不可变的销售数字,这些数字不能被更改。这意味着,一旦我们计算过平均值,它将始终对该实例保持不变。下次我们调用 calculateAverage 时,如果我们能重用它,就无需再次计算。

func calculateAverage(result: (Int?) -> Void ) {

  if let average = self.average {
    result(average)
  } else {

    let queue = GCD.backgroundQueue()
    dispatch_async(queue) {
      ...
    }
  }
}

我们甚至可以再走一步,使其不可变,并使用 struct 而不是 class 作为 SalesData 类型。当我们这样做时,我们会得到一个错误信息:

无法分配属性:'self' 是不可变的

self.average = sum / self.revenue.count

当你为 self.average 赋予新值时,你正在修改一个 self 实例,并且由于结构体默认是不可变的,我们需要将该方法标记为可变的:

struct SalesData {
  ...
  mutating func calculateAverage(result: (Int) -> Void ) {
    ...
  }
}

这些只是我们需要做的两个改动。此外,当我们使用它时,我们需要将 SalesData 实例作为一个变量来创建,因为 calculateAverage 会修改它。

var data = SalesData(revenue: makeRandomArray(1000))
data.calculateAverage { average in
  print(average)
}

因此,现在我们不能有一个常量 let 不可变的 SalesData 实例。这不是良好架构的标志,我们应该重构它。使用结构体作为数据实体是一个非常好的解决方案,因此我们应该继续使用这种方法重构代码。

分而治之

要实现良好的、稳固的应用程序架构,最好的方法是编写良好的代码结构,创建适当的抽象,并将其分解为具有单一职责的组件。

在函数式编程中,这更进一步。数据——以及处理这些数据的函数——也是分离的。面向对象的数据和与之相关的操作的概念被分成两部分。这使得代码更加灵活和可重用。

对于并发代码执行,特别重要的是将你的代码拆分成独立的单独部分,因为它们可以并发执行而不会相互阻塞。

在我们开始重构代码之前,让我们先分析它。目标是识别一个具有单一职责的组件。我已经做到了,以下是我识别出的这些组件:

  • 在数据中:第一部分是我们的输入数据。在我们的例子中,它是一个SalesData结构,它在我们应用程序中持有不可变数据。

  • 计算函数:下一部分是我们知道如何计算SalesData平均值的函数。它是一个简单的第一类函数,它接受SalesData并返回平均值。它的数学表示法是f(x) = y,代码表示法是func calculateAverage(data: SalesData) -> Int

  • 结果数据:这是计算函数返回的结果。在我们的例子中,它是一个简单的Int数字,表示平均值。

  • 异步执行操作:下一部分是一个切换到后台线程再回到主线程的操作,这实际上允许我们执行异步代码。在我们的例子中,它是一个带有适当队列的dispatch_async函数调用。

  • 缓存:一旦我们计算出一个平均值,我们就存储它,不再进行计算。这正是缓存的作用。

现在我们已经在我们应用程序中识别出独立的组件,让我们建立它们之间的联系和通信。

分割与征服

为了保持交互简单,我们的应用程序将向缓存请求SalesData的平均值。如果缓存包含平均值,它将返回它。否则,它将启动一个异步操作并将SalesData传递给它。异步操作将调用一个calculateAverage函数,获取平均结果,并将其返回给缓存。缓存将保存它并将其转发给应用程序。

当用言语解释时,这可能会听起来有点复杂,但在代码中它相当简单、直接且清晰。在我们开始重构之前,让我们看看我们为这个结构编写的代码:

struct SalesData {
  let revenue: [Int]
  var average: Int?

  init (revenue: [Int]) {
    self.revenue = revenue
  }

  mutating func calculateAverage(result: (Int?) -> Void ) {

    if let average = self.average {
      result(average)
    } else {

      let queue = GCD.backgroundQueue()
      dispatch_async(queue) {
        var sum = 0
        for index in self.revenue.indices {
          sum += self.revenue[index]
        }

        self.average = sum / self.revenue.count
        dispatch_async(GCD.mainQueue()) {
          result(self.average)
        }
      }
    }
  }
}

第一个出现在我脑海中的想法是遵循 FP 原则保持数据和函数分离,并将calculateAverage函数移出SalesData类型之外。

struct SalesData {
  let revenue: [Int]
  var average: Int?
}

func calculateAverage(data: SalesData, result: (Int) -> Void ) {
  ...
}

这将有效,但这个代码有一个问题。calculateAverage函数只能与SalesData类型一起工作,因此它应该隐藏在SalesData类型内部,而不对其他类型可见。此外,在 Swift 方法表示法中更受欢迎。

小贴士

Swift 2.0 将方法转移到自由函数之上,因此它更倾向于使用不可变方法。

示例:

  • Swift 2.0 方法[1,2,3].map { $0 + 1 }.filter { $0 > 1 }

  • Swift 1.2 函数filter(map([1,2,3]) { $0 + 1 }) { $0 > 2 }

我们不是将 calculateAverage 函数从 SalesData 类型中移除,而是将其改为不可变,并使其只执行平均计算,就像我们在我们的方案中所展示的那样。

SalesData 应该:

  • 存储收入数字

  • 成为计算平均值的不可变函数

让我们重构 SalesData 结构,并移除所有其他方法,以遵循我们的新结构

struct SalesData {
  let revenue: [Int]

  var average: Int {
    return revenue.reduce(0, combine: +) / revenue.count
  }
}

这里是解决方案,它非常干净简单。我们不是使用函数,而是使用了计算属性。Swift 倾向于使用更多只读属性来处理不可变数据,在我们的例子中,它将使未来的可读性更好。此外,我们最终使用了 reduce 方法来计算平均值。我们可以这样使用它:

  let data = SalesData(revenue: [145, 24, 3012])
  data.average

下一步是使其异步工作。让我们为它创建一个新的类型。它应该接受一个 SalesData 和一个回调闭包,该闭包将返回 Int,一个计算出的平均值结果。

struct AsyncOperation {

  static func calculate(data: SalesData, result: (Int) -> Void ) {
    GCD.asyncOnBackground {
      let average = data.average
      GCD.asyncOnMain {
        result(average)
      }
    }
  }
}

注意

我们已经为我们的 GCD 类型添加了两个额外的辅助方法:

struct GCD {
  static func asyncOnBackground(block: () -> Void ) {
    dispatch_async(self.backgroundQueue(), block)
  }
  static func asyncOnMain(block: () -> Void ) {
    dispatch_async(self.mainQueue(), block)
  }
}

这段代码看起来不错,但还有一个问题。调用平均值时,背景和主线程的切换是嵌入在一起的。如果我们保持这些函数分开,将更好,这样我们就可以在需要添加增长数字并为他们执行类似计算时重用这些函数。

static func calculateAverage(data: SalesData, result: (Int) -> Void ) {
  runAsync(data.average, result: result)
}

//MARK: - Private
private static func runAsync<T>(@autoclosure(escaping) work: () -> T, result: (T) -> Void ) {
  GCD.asyncOnBackground {
    let x = work()
    GCD.asyncOnMain {
      result(x)
    }
  }
}

在这里,我们创建了一个 runAsync 泛型函数,它在后台执行一些工作,并在主线程上返回其结果。我们在这里使用了 @autoclosure(escaping) 属性,以便能够传递一个表达式 data.average, ...) 而不是手动将其包装在闭包中。这使得代码语法更简洁。

现在,我们可以以异步的方式计算平均值。

let data = SalesData(revenue: [145, 24, 3012])
AsyncOperation.calculateAverage(data) { average in
  print(average)
}

现在是时候构建我们的最后一个组件,一个缓存。对于缓存功能,字典将是最佳选择。让我们添加一个字典来存储 SalesData 的平均结果。

struct SalesDataCache {
  var revenueCache = [SalesData : Int]()

  subscript (data: SalesData) -> Int? {
    return revenueCache[data]
  }

  mutating func getAverage(data: SalesData, result: (Int) -> Void) {
    if let average = self[data] {
      result(average)
    } else {
      AsyncOperation.calculateAverage(data) { average in
        self.revenueCache[data] = average
        result(average)
      }
    }
  }
}

我们创建了一个具有一个属性(缓存)和获取平均值函数的 SalesDataCache 结构体,该函数要么返回缓存的值,要么计算它并将其保存到缓存中,然后返回。这是一个非常简单的解决方案,但它不会工作。它显示了一个错误:类型 'SalesData' 不符合协议 'Hashable'

字典中的键必须是 Hashable,因此我们需要在我们的 SalesData 类型中实现这一点。Hashable 协议要求我们实现 hashValue 属性和等式函数。

var hashValue: Int { get }
func ==(lhs: Self, rhs: Self) -> Bool

实现一个数字数组的良好哈希函数相当复杂。最简单的方法是为 SalesData 添加一个 id 属性,使其具有唯一标识。

struct SalesData {
  let id: Int
  revenue: [Int]
}

//MARK:- Hashable
extension SalesData: Hashable {
  var hashValue: Int {
    return id.hashValue
  }
}

func == (lhs: SalesData, rhs: SalesData) -> Bool {
  return lhs.id == rhs.id
}

现在我们的缓存将开始工作,我们最终可以在我们的应用程序中使用它。让我们这样做:

let range = 0...10
var cache = SalesDataCache()
let salesData = range.map {
  SalesData(id: $0, revenue: makeRandomArray(1000))
}

for data in salesData {
  cache.getAverage(data) { average in
    print(average)
  }
}

如您所见,我们创建的 API 真的非常容易使用。尽管幕后有很多逻辑在运行,但对于您来说,它就像调用一个方法:getAverage

此外,我们以这种方式构建了底层组件,以便它们可以单独使用——例如,如果我们不需要缓存或异步执行。

为了总结这个示例的重构工作,让我们看看我们最终得到的完整代码:

struct SalesData {
  let id: Int
  let revenue: [Int]

  var average: Int {
    return revenue.reduce(0, combine: +) / revenue.count
  }
}

//MARK:- Hashable
extension SalesData: Hashable {
  var hashValue: Int {
    return id.hashValue
  }
}

func == (lhs: SalesData, rhs: SalesData) -> Bool {
  return lhs.id == rhs.id
}

struct AsyncOperation {

  static func calculateAverage(data: SalesData, result: (Int) -> Void) {
    runAsync(data.average, result: result)
  }

  //MARK: - Private
  private static func runAsync<T>(@autoclosure(escaping) work: () -> T, result: (T) -> Void ) {
    GCD.asyncOnBackground {
      let x = work()
      GCD.asyncOnMain {
        result(x)
      }
    }
  } 
}

struct SalesDataCache {
  private var revenueCache = [SalesData : Int]()

  subscript (data: SalesData) -> Int? {
    return revenueCache[data]
  }

  mutating func getAverage(data: SalesData, result: (Int) -> Void)  {
    if let average = self[data] {
      result(average)
    } else {
      AsyncOperation.calculateAverage(data) { average in
        self.revenueCache[data] = average
        result(average)
      }
    }
  }
}

控制生命周期

在我们的代码中,我们使用了 @autoclosure(escaping) 属性。这是一个非常强大的属性,值得详细探讨。还有一个 @noescape 属性。让我们更详细地探讨它们。

应用 @autoclosure 和 @noescape 属性

首先,让我们看看何时以及如何使用这些属性。我们可以将它们应用于具有函数类型的函数参数。函数类型可以表示为方法、函数或闭包,并且具有 (parameters) -> (return) 表示法。这里有一些示例:

func aFunc(f: () -> Void )
func increase(f: () -> Int ) -> Int
func multiply(f: (Int, Int) -> Int ) -> Int

@autoclosure

@autoclosure 属性可以应用于没有参数并返回任何类型的函数类型的参数,() -> T。例如:

func check(@autoclosure condition: () -> Bool)
func increase(@autoclosure f: () -> Int ) -> Int

当我们使用不带 @autoclosure 属性的 increase 函数时,我们需要传递一个函数、一个方法或一个闭包作为参数。

let x = 10
let res = increase( { x } )
check( { x > 10 } )

但在这个用例中,如果我们能够简单地使用一个表达式而不需要将其包装在闭包中会更好,就像这样:

let x = 10
let res = increase(x)
check(x > 10)

正如 @autoclosure 允许我们做的那样。当你使用 @autoclosure 属性的参数时,你传递的表达式会自动为你包装成一个闭包。这使得你的代码更简洁。这就是它所做的全部。没有魔法;它只是为你移除了样板代码。

@noescape

@noescape 关键字更为复杂和有趣。它可以应用于具有任何函数类型的函数参数。

@noescape 属性表示闭包将在函数体内部使用,在函数返回调用之前。这意味着它不会逃离函数体。

当你应用此属性时,它表示闭包将在函数体内部同步使用。这也意味着它将在你离开函数时释放。该闭包参数的生命周期不能超过函数调用。

应用此属性可以启用一些性能优化,但更重要的是,它禁用了在访问实例成员时显式指定 "self." 的要求。

让我们看看一些示例,以更好地理解这一点。对于简单的示例,我们将使用相同的 increase 函数,但现在我们将它作为 struct 的一个方法:

func increase(f: () -> Int ) -> Int {
  return f() + 1
}

struct Data {
  var number: Int

  mutating func increaseNumber() {
    number = increase { number }
  }
}

increase 函数调用中存在错误:闭包中引用属性 'number' 需要显式 'self.' 以使捕获语义明确;我们需要显式引用 self.number

但让我们看看 increase 函数。f: ()-> Int 参数在函数体内部使用,并且它没有离开其作用域。这是应用 @noescape 属性的一个很好的候选。

func increase(@noescape f: () -> Int ) -> Int {
  return f() + 1
}

现在我们不需要做任何进一步的更改,并且明确地引用 self.numbers,因为 @noescape 确保在离开该函数之前会调用闭包,我们可以安全地引用 self

小贴士

尽可能地应用 @noescape。它为代码增加了额外的安全级别。同时,它还使优化更好,并提高性能。

如果我们查看 Swift 标准库中的方法,如 mapreducecontains 等,你会发现它们被标记为 @noescape 属性。黄金法则:如果你在离开函数之前调用闭包参数,请标记为 @noescape

也许在未来的 Swift 中,它将自动为你完成这些,但到目前为止,我们需要自己来做。

@autoclosure (escaping)

@autoclosure 属性也隐式地应用了 @noescape。如果你想将一个参数作为一个自动闭包,同时表明它将比函数有更长的生命周期,请使用 @autoclosure(escaping) 属性。这对于异步代码执行可能很有用,就像我们在 AsyncOperation 的例子中那样。

摘要

在本章的第一部分,我们介绍了多线程并发和多核设备架构。这些一般信息使我们能够理解并发代码执行的核心原则。

在第二部分,我们介绍了在 Swift 中使用线程、GCD 和 NSOperation 运行代码的三个异步方法。我们探讨了它们之间的区别以及每种方法最适合的情况。

在本章的第三部分,我们专注于使用 GCD 架构异步 Swift 代码。我们涵盖了重要的提示,例如传递回调函数参数、避免状态、使用不可变值等。此外,我们还介绍了两个非常有用的 Swift 属性——@noescape@autoclosure

在下一章中,我们将介绍一种重要的性能优化技术:懒加载。

第七章. 懒惰的重要性

提高应用程序性能的另一种方法是推迟代码的执行,直到需要结果。这听起来非常合理;我们运行的代码越少,所需的时间就越少。这种设计模式通常被称为懒惰。有很多事情可以是懒惰的,也有很多不同的方法可以推迟代码执行。在本章中,我们将涵盖以下主题:

  • 懒惰的心态

  • 懒加载

  • 懒惰集合和评估

懒惰的心态

首先,理解懒惰模式的工作原理、它如何提高应用程序的性能以及何时使用它非常重要。同样重要的是不要滥用它,因为这会使代码更加复杂且难以阅读。此外,跟踪执行流程也会变得困难。滥用它还会降低应用程序的整体性能。

懒惰模式的一般思想是在有人请求该指令的结果之前,推迟对该指令的评估。

通常,代码是按指令顺序执行的,从文件的顶部或函数的开始处开始。如今,我们的应用程序更加复杂,由许多文件、窗口、库、组件和层组成,但它们仍然以相同的方式执行代码。由于我们的系统越来越大,使它们变得懒惰非常重要,这样我们就不必在启动应用程序时执行所有工作。让我们学习一些使代码变得懒惰的技术。

分离

非常重要的是将代码分离成具有单一职责模式的组件。组件应该只做一件事,并且要做好。让我们看看这个简单的例子,以了解这如何提高性能:

struct Person {
  let name: String
  let age: Int
}

func analyze(people: [Person]) {
  let names = people.map { $0.name }
  let last = names.maxElement()

  let alphabetOrder = names.sort { $0 > $1 }
  let lengthOrder = names.sort { $0.characters.count < $1.characters.count }
  let longestName = lengthOrder.last

  print(last, alphabetOrder, lengthOrder, longestName)

  let age = people.map { $0.age }
  let youngest = age.minElement()
  let oldest = age.maxElement()
  let average = age.reduce(0, combine: +) / age.count

  print(youngest, oldest, average)
}

let people = [Person(name: "Sam", age: 3),
  Person(name: "Lisa", age: 68),
  Person(name: "Jesse", age: 35)
]

people + EnglandPopulation()
analyze(people)

这段代码的问题在于analyze函数做了两件事。正因为如此,内存使用峰值是原来的两倍。当我们完成对名称的分析后,用于它的内存不会立即释放,而是保留到我们从函数返回。如果我们试图分析英格兰的全部人口,这将需要相当多的内存。通过将它们分成单独的函数,我们可以改善内存使用:

func analyze(people: [Person]) {

  let names = people.map { $0.name }
  analyzeNames(names)

  let age = people.map { $0.age }
  analyzeAge(age)
}

func analyzeNames(names: [String]) {
  ...
}

func analyzeAge(age: [Int]) {
  ...
}

你应该经常将代码分离技术应用于不同的组件,例如。创建简单的模型,并仅使用当前视图所需的数据。将代码拆分为更小的组件是有用的,但你不应将其拆分得过于细小。

按需工作

懒加载代码的主要思想是仅在需要时才执行工作。这样,你可以在没有人请求结果的情况下延迟代码执行或完全删除它。当你规划应用程序的架构时,试着考虑这些问题:这个资源或数据在应用程序中何时会被使用?它现在必须实例化吗?还是可以等待?这份数据会被使用多频繁?如果经常使用,那么你可能应该缓存它;如果不经常使用,那么你可能应该懒加载它。数据是否很重?你需要在之后清理它吗?提出所有这些问题有助于你使用按需工作的方法构建更好的应用程序架构。

近似结果

另一个想法是将懒加载和异步执行结合起来。如果你想执行一个异步任务,因为它需要很长时间,但你又想立即得到结果,你可以立即返回一个近似结果并继续执行。例如,核心数据异步获取实现了这个模式。它返回你将要获取的项目的大致数量。

懒加载

懒加载模式允许你在尝试使用对象之前延迟创建对象。这种模式可以在任何编程语言中实现。在 Objective-C 中,我们使用了属性的获取器并检查它是否已初始化。Swift 为语言添加了对懒加载的支持,这使得应用此模式变得更加容易。有许多东西可以被懒加载,我们将在本章中介绍它们。

全局常量和变量

在 Swift 中,全局变量和常量始终是懒加载的。这意味着每个全局变量仅在第一次访问它时才被初始化。作为一个测试,让我们创建一个新的 Person.swift 文件并将此代码添加到其中:

struct Person {
  let name: String
  let age: Int

  init(name: String, age: Int) {
    self.name = name
    self.age = age
    print("\(name) Created")
  }
}

let Jon = Person(name: "Jon", age: 20)
let Sam = Person(name: "Sam", age: 28)

此文件包含两个全局常量:JonSam。我们还向 Person 结构体的 init 方法中添加了一个日志语句,这样我们就可以看到何时创建了这个人物。现在让我们尝试访问其中一个全局常量并查看控制台输出:

print("Start!")
print("Age: \(Jon.age)")
//print("Age: \(Sam.age)")

Console Output:

Start!
Jon Created
Age: 20

如你所见,只有 Jon 全局常量被创建,并且是在我们尝试访问其年龄属性时创建的。是的,Swift 中的全局常量和变量具有强大的功能,但你几乎永远不应该使用全局常量!有更好的方法来做这件事。

全局变量甚至更脆弱,因为任何人都可以在整个应用程序中更改它们。

类型属性

结构体和类都可以有一个 type 属性。type 属性属于类型本身,而不是该类型的实例。无论你创建多少个该类型的实例,type 属性的副本都只有一个。它们的行为类似于全局变量和常量,但它们具有该类型的命名空间作用域。此外,类型属性可以声明为私有,并从应用程序的其余部分隐藏。

我们可以通过将全局常量移动到 Person 结构定义中来非常容易地改进我们的代码:

struct Person {
  let name: String
  let age: Int

  static let Jon = Person(name: "Jon", age: 20)
  static let Sam = Person(name: "Sam", age: 25)

  ...
}

要访问type属性,我们需要在其前加上它声明的类型名称;要访问Jon类型常量,我们需要写Person.Jon。类与结构体在类型属性方面具有相同的语法和功能:

print("Age: \(Person.Jon.age)")
print("Age: \(Person.Sam.age)")

懒加载属性

还有懒加载实例属性的方法。让我们扩展我们的Person类型并为其添加一个HealthData结构体:

struct HealthData {
  init() {
    print("HealthData Created") 
  }
}

struct Person {
  let name: String
  let age: Int

  var healthData = HealthData()
}

现在,每次我们创建一个新的个人,它都会创建一个healthData实例。在我们的例子中,HealthData可能是一个连接到数据库、获取健康数据并执行大量工作的重对象。我们不需要在创建Person的同时创建HealthData结构体;相反,我们希望在需要时才创建HealthData结构体。

要使一个属性表现得是懒加载的,你只需要在其声明中添加lazy属性:

  lazy var healthData = HealthData()

lazy实例属性必须声明为变量而不是常量。

提示

非常重要不要滥用懒加载实例属性。访问懒加载属性会增加一点性能开销,用于检查它是否已初始化。

因此,分析系统哪些部分需要同时创建非常重要,因为它们会同时使用,而有些可能根本不会使用;因此,我们将它们设置为懒加载以延迟它们的创建。

当你访问懒加载属性时,它实际上会修改一个值,因此实例必须声明为变量。如果你使用具有懒加载存储属性的课程,它可以声明为常量。这是因为它是一个常量引用,更改应用于值:

let ola = Person(name: "Ola", age: 27)
let health = ola.healthData // Error! It's mutating a value

var bobby = Person(name: "Bobby ", age: 5)
let bobbyHealth = bobby.healthData // Works fine

let someClass = SomeClass()
someClass.healthData // Works fine because class is a reference type

关于懒加载属性的一个重要注意事项是,它们只初始化一次。如果你创建了一个懒加载的可选属性,稍后你想将其设置为nil并再次初始化,你需要手动完成。比如说healthData是一个非常重的实例,我们希望在不需要时清除它:

struct Person {
  ...
  lazy var healthData: HealthData? = HealthData()

  mutating func clearHealthData() {
    healthData = nil
  }
}

var ola = Person(name: "Ola", age: 27)
var health = ola.healthData //Get lazy loaded here
ola.clearHealthData() 
health = ola.healthData // nil, nothing happens here.

正如我们所说,懒加载的存储属性只初始化一次。所以,下次我们访问它,在我们清除它之后,就没有额外的初始化代码在运行。如果我们真的需要这种懒加载缓存,那么我们需要自己实现它。这并不难。我们需要一个存储属性和一个计算属性,如下所示:

struct Person {
  ...
  private var _healthData: HealthData?

  mutating func clearHealthData() {
    _healthData = nil
  }

  var healthData: HealthData {
    mutating get {
      _healthData = _healthData ?? HealthData()
      return _healthData!
    }
  }
}

我们在这里必须创建一个可变获取器,因为var属性不能声明为mutating。现在,如果我们再次运行之前的示例,我们会看到每次清理后都会创建HealthData

var ola = Person(name: "Ola", age: 27)
var health = ola.healthData //Get lazy loaded here
ola.clearHealthData() 
health = ola.healthData // HealthData created again

计算属性

延迟属性初始化的另一种方式是使用计算属性。正如其名所示,计算属性每次访问时都会被计算。重要的是要记住,这样的属性每次都会被计算,因为这可能会对性能产生负面影响,或者如果你对其执行任何副作用。

计算属性的最好用途是当你想要提供一个只读属性,该属性使用内部数据进行计算时。一个很好的例子就是一个人的全名:

struct Person {
  let name: String
  let lastName: String
  let age: Int

  var fullName: String {
    print("calculating fullName")
    return "\(name) \(lastName)"
  }
}

var jack = Person(name: "Jack", lastName: "Samuel", age: 21)
print(jack.fullName)
print(jack.fullName)

在执行任何副作用或突变操作时,对计算属性进行操作时,我们必须非常注意。突变操作需要为结构显式指定,但对于类来说,这样做也是合法的。

懒加载集合和评估

另一个我们可以懒加载执行操作的地方是集合和序列。我们在其中存储了许多元素,有时执行如filtermap这样的操作会花费很多时间,可能也是不必要的。

在我们深入细节之前,让我们先看看一个小例子,看看为什么懒加载集合是如此有用。我们有一个集合。我们想要对其执行如映射这样的操作,并从结果中获取一个或几个元素。以下是使用数组实现它的方法:

let numbers = Array(1...1_000_000) 
let doubledNumbers = numbers.map { $0 * 2 }
doubledNumbers.last

当我们在numbers数组上调用map方法时,它会将操作应用于数组中的每个元素,并返回新的映射数组。因此,我们得到了一个新的doubledNumbers数组。我们的map闭包{ $0 * 2 }会根据数组中的元素数量被调用,在我们的例子中是 1,000,000 次。但我们需要的是数组中的最后一个元素。而不是映射每个元素,我们只想映射最后一个。在这种情况下,使用懒加载集合会更好:

let numbers = Array(1...1_000_000) 
let lazyNumbers = numbers.lazy
let doubledNumbers = lazyNumbers.map { $0 * 2 }
doubledNumbers.last

这里的唯一区别是我们添加了一个lazy方法调用来从数组创建LazyCollection

public var lazy: LazyCollection<Self> { get }

当我们在懒加载集合上调用map方法时,它不会立即对每个元素进行映射,而是延迟执行并返回另一个LazyCollection——LazyMapCollection。下次当我们要求doubledNumbers获取最后一个数字时,它会对最后一个数字进行映射并返回给我们。因此,我们只调用了一次我们的map,这正是我们所需要的。

所有的LazyCollectionsLazySequences都按照相同的原理工作;它们在需要时才执行操作,当你从其中拉取一个元素时。当你调用任何应该对序列执行操作的函数,如mapfilter时,它不会立即执行。相反,LazySequence保存需要执行的操作并返回一个新的LazySequence。只有当你从其中拉取数据时,LazySequence才会执行那个操作,比如当你请求最后一个元素时。

序列和集合

为了更好地理解懒加载,了解序列和集合之间的区别是有用的。我们可以将懒加载操作应用于它们,但集合允许我们做更多的事情,我们将会看到原因。

序列

序列表示为可遍历的元素集合。我们对序列的主要操作是从第一个元素开始向前迭代其元素。为了做到这一点,序列使用了一个 GeneratorType 协议。GeneratorType 协议有一个 next 方法,它返回下一个元素,如果没有元素则返回 nil。这是 GeneratorType 上唯一可用的方法:

mutating func next() -> Self.Element?

注意

SequenceTypeGeneratorType 协议非常简单。每个协议都只需要实现一个方法:

protocol SequenceType {
  func generate() -> Self.Generator
}
protocol GeneratorType {
  mutating func next() -> Self.Element?
}

这里是 SequenceType 实例可以执行的最简单和最基本操作,即对其元素的迭代:

let seq = AnySequence(1...10)
for i in seq {
  i
}

小贴士

如果你希望你的自定义类型能在 for…in 循环中使用,你需要为你的类型实现一个 SequenceType 协议。

我们迭代序列的另一种方式是手动从生成器获取下一个元素:

let gen = seq.generate()
while let num = gen.next() {
  num
}

Swift 标准库使用协议扩展来为 SequenceType 提供额外的功能。在 Swift 2.0 中,SequenceType 变得非常强大,拥有许多方法,如 mapfiltersortequal 以及许多其他方法。

集合

另一方面,集合表示一组项目,其中每个项目都可以通过其索引访问。集合的最简单例子是一个数组。集合类型也实现了 SequenceType 协议,因此你可以在需要序列的任何地方使用集合。

除了 SequenceType 协议外,CollectionType 还需要实现三个额外的方法:

subscript (position: Self.Index) -> Self._Element { get }
var startIndex: Self.Index { get }
var endIndex: Self.Index { get }

CollectionType 通过下标方法为我们提供了对元素的随机访问。因为集合知道第一个和最后一个索引,它还可以计算大小,并从开始和结束两个方向迭代其元素。这给了我们更多的能力。

实现我们自己的序列和集合

作为了解序列和集合的总结,让我们实现我们自己的序列和集合类型。我们还可以添加一个 print 语句,这样我们就可以在控制台看到它实际执行工作的情况。这对于检查惰性集合的行为非常有用。让我们从序列开始:

struct InfiniteNums: SequenceType {

  func generate() -> AnyGenerator<Int> {
    var num = 0

    return anyGenerator {
      print("gen \(num)")
      return num++
    }
  }
}

这里是我们简单的无限数列。我们的生成器只是返回下一个整数。现在让我们创建一个自定义集合。它将比序列复杂一些:

struct Collection10: CollectionType {
  let data = Array(1...10)

  var startIndex: Int {
    return data.startIndex
  }

  var endIndex: Int {
    return data.endIndex
  }

  subscript (position: Int) -> Int {
    print("Pos \(position)")
    return data[position]
  }

  func generate() -> AnyGenerator<Int> {
    var index = 0

    return anyGenerator {
      print("Col index: \(index)")
      let next: Int? = index < self.endIndex ? self.data[index++] : nil
      return next
    }
  }
}

对于这个例子,我们创建了一个包含 10 个整数元素的不可变集合。它的实现仍然相当简单。因为我们的集合是封闭的,并且它有 endIndex 属性,生成器需要检查边界,当序列中没有更多元素时,它返回 nil

你可以尝试使用这些新类型,并将它们与 Swift 标准库中的函数一起使用,这些函数接受序列或集合类型作为参数。现在让我们继续到最有趣的部分;让我们使用这些类型与一个惰性函数一起使用。

使用惰性

将集合或序列转换为懒版本非常简单。序列和集合都有一个 lazy 属性,它返回该集合或序列的懒加载版本:

public var lazy: LazyCollection<Self> { get }
public var lazy: LazySequence<Self> { get }
[1, 2, 3].lazy
AnySequence(1...10).lazy

使用懒序列

现在让我们用我们的无限数字序列和 LazySequence 的映射和过滤方法来玩玩:

let infNums = InfiniteNums()
let lazyNumbers = infNums.lazy

let oddNumbers = lazyNumbers.filter { $0 % 2 != 0 }
let doubled = lazyNumbers.map { $0 * 2 }
let mixed = lazyNumbers.filter { $0 % 4 != 0 }.map { $0  * 2 }

var gen = oddNumbers.generate()
var gen2 =  mixed.generate()

for _ in 0...10 {
  gen.next()
  gen2.next()
}

这里的代码相当简单。我们创建一个懒序列,并对其应用 mapfilter 转换。然后,我们从其中提取前 10 个结果。因为集合是懒加载的,所以它只在开始从循环体中提取元素时开始执行映射和过滤,通过调用 next 方法。

因为 LazySequence 是一个 SequenceType 协议,所以你可以用它与任何接受 SequenceType 协议的函数一起使用,或者调用在该 SequenceType 中可用的任何方法。这种操作不会是懒加载的,并且会返回一个结果。如果我们尝试用我们的无限数字使用它们,可能会导致无限循环。这是因为我们的序列是无限的:

lazyNumbers.contains(3) // returns true, stops when found
// lazyNumbers.minElement() // Infinite loop

使用懒集合

与懒序列一起工作非常类似于与懒集合一起工作。LazyCollection 实现了一个 CollectionType,这意味着我们可以使用 CollectionType 中的所有方法与 LazyCollection 一起使用。

首先,让我们创建一个懒集合和一些额外的辅助函数来映射和过滤我们将要使用的集合:

let isOdd = { $0 % 2 != 0 }
let doubleElements = { $0 * 2 }

let col = Collection10()
let lazyCol = col.lazy

懒集合的 mapreversefilter 方法有一些小的区别。它们返回不同的懒集合类型:LazyMapCollectionLazyFilterCollectionReverseRandomAccessCollection

lazyCol.map(doubleElements) //LazyMapCollection<Self.Elements, U>
lazyCol.reverse() //LazyCollection<ReverseRandomAccessCollection<Self.Elements>>
lazyCol.filter(isOdd)  //LazyFilterCollection<Self.Elements>

LazyMapCollectionLazyFilterCollection 类型都实现了 LazyCollectionType,并且它们有类似的方法。正因为如此,一些懒集合可能有一组不同的可用方法。

这些懒集合的一个有趣的行为差异是某些方法工作方式的不同。作为一个例子,让我们看看 countisEmpty 方法。首先,让我们尝试用 LazyMapCollection 使用它们:

let lazyMap = lazyCol.map(doubleElements)
let count = lazyMap.count

lazyMap.isEmpty
lazyMap.reverse().isEmpty

调用 countisEmpty 不会强制懒集合执行任何映射操作。因为映射不会改变源集合的元素数量,它可以使用底层集合数据——startIndexendIndex——来计算这些方法的结果。

然而,在 LazyFilterCollection 上调用 countisEmpty 确实需要懒集合执行一个过滤操作。对于 isEmpty 方法,它一旦找到元素就会停止;但对于 count 方法,它需要将过滤操作应用于每个元素。在这种情况下,调用 count 方法的行为与在常规集合上调用它相同:

lazyCol.filter(isOdd).isEmpty
lazyCol.filter(isOdd).count

值得注意的是,mapreverse 方法之间还有一个区别,那就是它们使用的下标方法和索引:

//Query elements
lazyCol.map(doubleElements)[3]

let revCol = lazyCol.reverse()
let ind = revCol.startIndex.advancedBy(2)
revCol[ind]

let revMapCol = lazyCol.reverse().map(doubleElements)
let index = revMapCol.startIndex.advancedBy(2)
revMapCol[index]

LazyMapCollection允许我们使用用于集合的原始索引;在我们的例子中,这是一个Int类型。reverse方法返回一个具有ReverseRandomAccessIndex类型索引的ReverseRandomAccessCollection。要使用其上的索引方法,我们需要使用startIndexendIndex以及advance方法来移动索引到所需的位置。

这是对懒集合和序列的一般概述。然而,懒类型还有一个非常重要的特性需要我们介绍。

懒集合或序列在开始从其中拉取元素时执行操作,例如,元素的映射。这里的区别是,常规数组对每个元素只应用一次映射并立即返回结果。

懒集合每次从其中拉取元素时都会应用映射。如果您打算经常使用映射的结果,那么使用常规数组或添加某种缓存或记忆功能可能更好。让我们通过一个例子来看看为什么这可能是危险的:

let col = Array(0...10)
let lazyCol = col.lazy

var x = 10
let mapped = lazyCol.map { $0 + x++ }

for i in mapped {
  print(" \(i)", terminator:"") //10 12 14 16 18 20 22 24 26 28 30
}

print("")
for i in mapped {
  print(" \(i)", terminator:"") //21 23 25 27 29 31 33 35 37 39 41
}

这个例子展示了使用懒集合和在映射函数中的状态时存在的两个重要问题。当我们使用映射的懒集合时,我们期望结果在所有循环迭代中都是相同的,因为我们没有做任何改变。然而,实际上,它将是不同的,因为map函数被调用了两次,我们在映射函数中使用的状态已经改变。

我们可以通过从映射函数中移除状态来部分解决这个问题。现在,懒映射集合会产生相同的结果,但这并不能改变映射函数将在两个循环中执行的事实。如果我们把一个昂贵的操作放在映射函数内部,它会使我们的应用速度减半:

// No state
let mapped = lazyCol.map { $0 + $0 + 10 }

在结束懒集合的话题时,我想说,懒集合非常强大,但您应该像使用任何其他有助于提高性能的工具一样谨慎地使用它。

摘要

在本章中,我们介绍了两个可以提高应用程序性能的工具:懒加载和懒执行技术。首先,我们解释了为什么让代码表现出懒性很重要,何时应该这样做,以及如何实现。接下来,我们展示了 Swift 语言内置的懒加载功能以及如何使用它与全局变量、类型属性和懒存储属性一起使用。

本章的其余部分,我们介绍了如何使集合表现出懒性。我们使用了许多与懒集合相关的函数,并展示了集合与序列之间的区别。

在下一章和最后一章中,发现所有底层的 Swift 力量,我们将探讨一些更高级的工具,这些工具将帮助您分析 Swift 的力量,并对您在本书中学到的内容进行快速回顾。

第八章. 发现所有 Swift 的潜在力量

在前面的章节中,我们讨论了许多关于 Swift、其强大功能和如何提高应用程序性能以及构建稳固的应用程序架构的话题。在本章中,我们将探讨一些工具并涵盖以下主题:

  • Swift 为什么会这么快

  • Swift 编译器和工具

  • 分析汇编代码

  • 回顾学习的重要信息

  • 功能阅读

Swift 为什么会这么快

首先,让我们快速回顾一下——也就是为什么 Swift 会这么快——并看看 Swift 及其酷炫功能有什么特别之处。

Swift 是一种强类型编译型编程语言。这使得它成为一个非常安全的编程语言。Swift 对类型非常严格,并验证源代码中所有类型的使用是否正确。它在编译时捕捉了大量问题。

Swift 也是一种静态编程语言。它的源代码被编译成汇编代码,然后使用 LLVM 工具将汇编代码编译成机器代码。运行本机机器代码指令是完成这项任务最快的方式。相比之下,Java 和 C# 被编译成一段中间代码,需要虚拟机来运行,或者另一个将其翻译成机器指令的工具。因为 Swift 在运行时不会这样做,所以它有非常大的性能提升。

通过混合强类型规则和编译成汇编代码,Swift 能够很好地分析代码并执行很好的汇编代码优化。Swift 还被构建得易于编写,拥有愉快且干净的语法、现代特性和类型。这种独特的语法、强大功能、安全性和性能的组合使 Swift 成为一个非常出色的编程语言。

Swift 命令行工具

在这本书中,我们已经使用了一个终端工具——Swift REPL 控制台。我们通过在终端中输入 xcrun swift 命令来启动它。它启动了 REPL,然后我们可以输入 Swift 代码并评估它。

xcrun

要启动一个 REPL,我们实际上使用了两个工具:xcrunswiftxcrun 是一个 Xcode 命令行工具运行器。它可以帮助你通过名称从活动开发者目录中运行命令行工具。当你安装了多个版本的 Xcode 时,你可以在执行命令行工具时选择使用哪一个。你可以在 Xcode 中通过访问Xcode | 偏好设置 | 位置 | 命令行工具,或者通过在终端中运行 xcode-select 命令来实现。这样,xcrun 允许你避免在要运行的命令行工具中指定完整路径,并使运行过程变得更加简单。

xcrun 有一些更有趣的选项,你应该尝试一下。要获取帮助,请运行 xcrun -h 帮助命令,这个命令通常对大多数其他命令行工具也是可用的。

小贴士

一些命令行工具需要使用完整的命令表示法:–help。一个例子是 xcrun --help

这意味着通过运行 xcrun swift 命令,我们实际上是在启动一个 swift 命令行工具。如果你运行 xcrun swift -h 命令以获取帮助,你会看到 swift 实际上是一个具有许多选项可供选择的 Swift 编译器。

xcrun 中可用的另一个有趣的功能是通过名称获取工具的路径。这非常有用,因为这样我们可以探索这个工具所在的文件夹,并找到其他可用的工具。例如,让我们看看 swift 命令行工具的位置。为此,你需要运行 xcrun -f swift 命令,结果如下:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift

我们可以通过更改目录使用 cd 命令并执行 open . 命令简单地从这个终端打开这个文件夹:

cd /Applications ... /usr/bin/
open .

在这个文件夹中,有超过 50 个不同的命令行工具可用,例如 libtoolotoolswiftswift-demangle 以及其他工具。还有一个包含许多有趣工具的另一个目录,例如 ibtoolsimctlxcrun

/Applications/Xcode.app/Contents/Developer/usr/bin

我们不会在这里涵盖所有这些;我们将把它们留给你作为作业去探索它们的强大功能。

另一种通过 xcrun 在当前目录中查找可运行工具的方法是开始输入命令并按 Tab 键。这将显示可用命令的建议。

xcrun

Swift 编译器

在上一步中,当我们玩弄 xcrun 时,我们发现有两个不同的 Swift 编译器工具:swiftswiftc。如果你使用 -h 命令获取帮助,你会注意到它们都是具有相似选项的 Swift 编译器,但存在一些差异。

swift

swift 工具用于编译和执行 Swift 代码。如果你不带任何参数运行它,它将启动一个交互式解释器(REPL),并给你评估 Swift 代码的能力。

swift

你也可以传递一个你想要运行的 Swift 文件作为参数。我们可以简单地从命令行创建一个 Swift 文件:

echo 'print("Hello World")' > main.swift 
xcrun swift main.swift
Hello World

你也可以传递额外的选项,例如 -O 以启用优化:

xcrun swift -O main.swift

swiftc

swiftc 编译器编译 Swift 代码并生成结果,但它不会执行它。根据选项的不同,你可以得到一个二进制文件、汇编、LLVM IR 表示或其它。

swift 工具不同,swiftc 有一个必需的参数:一个输入的 Swift 文件。如果你尝试不传递此参数运行它,你会得到一个错误:

xcrun swiftc
<unknown>:0: error: no input files

如果你运行它并传递一个 Swift 文件作为参数,它将生成一个可执行文件。让我们使用相同的 main.swift 文件:

xcrun swiftc main.swift

结果,你将得到一个与名称相同的可执行文件——main。如果你运行它,你将在控制台得到相同的输出,Hello World。关于swiftc的有趣之处在于,它允许你传递许多 Swift 文件作为输入,将它们一起编译,并生成可执行文件。让我们再创建一个 Swift 文件,命名为file.swift,并将此函数添加到其中:

func bye() {
  print("Bye bye")
}

现在,我们将编辑我们的main.swift文件以及其中的bye()函数调用。如果我们现在尝试编译main.swift文件,它将显示一个错误:

xcrun swiftc main.swift 
main.swift:2:1: error: use of unresolved identifier 'bye'
bye()
^

毫不奇怪,Swift 编译器找不到bye函数声明,因此它失败了。我们需要做的是同时编译file.swiftmain.swift文件:

xcrun swiftc main.swift file.swift

你传递给swiftc的文件顺序并不重要;我们也可以将它们命名为swiftc file.swift main.swift。如果你现在运行这个可执行文件,你将在控制台看到两个句子:

Hello World
Bye bye

[Process completed] 

现在你已经知道了如何使用 Swift 编译器,让我们继续到有趣的部分。让我们使用 Swift 编译器来产生不同的结果。为了简单起见,我们将我们的 Swift 代码与main.swift文件合并,并添加更多指令以获得更有趣的结果。以下是最终的代码:

func bye() {
  print("bye")
}

print("Hello World")

let a = 10
let b = 20
let c = a  + b
print(c)
bye()

Swift 编译过程和 swiftc

Swift 源代码的编译过程相当有趣,涉及多个步骤。Swift 编译器使用 LLVM 进行优化和二进制生成。为了更好地理解整个过程,请参考此流程图:

The Swift compilation process and swiftc

首先,Swift 源代码被转换为一个AST(抽象语法树)。然后,它被转换为一个SIL(Swift 中间语言),首先是一个原始 SIL,然后是一个规范 SIL。之后,它被转换为一个 LLVM IR(中间表示)。在这一步中,LLVM 负责剩下的工作。它处理 IR,进行优化,并生成一个汇编,然后为特定架构生成一个可执行文件。

在前面的图中,有趣的部分是生成 SIL 的步骤。这是一个 Swift 特有的优化,它专门为 Swift 创建。其他编程语言,如 C,在生成 LLVM IR 之前不会进行这种优化,并且它们少了一个优化步骤。

The Swift compilation process and swiftc

使用swiftc,可以生成每个步骤的结果。这对于代码优化分析非常有用。要查看所有可用模式,只需运行xcrun swiftc -h。现在,让我们快速看一下它们。

Swift AST

Swiftc 有三种不同的选项用于生成 AST。每个选项都生成不同详细程度的 AST。AST 代码表示显示了 Swift 编译器如何查看和分析代码:

xcrun swiftc -dump-ast main.swift
xcrun swiftc -dump-parse main.swift
xcrun swiftc -print-ast main.swift

-dump-ast命令的输出包含最大细节,可能难以分析。我们先来看一下-dump-parse的示例:

(source_file
  (var_decl "a" type='<null type>' let storage_kind=stored)
  (top_level_code_decl
    (brace_stmt
      (pattern_binding_decl
        (pattern_named 'a')
        (integer_literal_expr type='<null>' value=10)))

这个 AST 代码代表了 var a = 10 Swift 代码。每条指令都被解析成单独的树节点,然后组合成树形表示。你可以在 clang.llvm.org/docs/IntroductionToTheClangAST.html 找到更多关于 Clang 的 AST 信息。

SIL

Swift 中间语言SIL)是分析 Swift 代码最有用的工具之一。它包含许多细节,非常可读且易于分析。为了生成 SIL,xcrun 有两种模式;-emit-silgen 生成原始 SIL,-emit-sil 生成规范 SIL:

xcrun swiftc -emit-silgen main.swift
xcrun swiftc -emit-sil main.swift

原始 SIL 和规范 SIL 几乎相同。原始 SIL 简单一些,它不包括私有函数实现的细节和一些全局对象。让我们看看生成的原始 SIL:

sil_stage raw

import Builtin
import Swift
import SwiftShims

// main.a : Swift.Int
sil_global [let] @_Tv4main1aSi : $Int
...

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<UnsafeMutablePointer<Int8>>) -> Int32 {
...
}

// main.bye () -> ()
sil hidden @_TF4main3byeFT_T_ : $@convention(thin) () -> () {...
}

// static Swift.+ infix (Swift.Int, Swift.Int) -> Swift.Int
sil [transparent] [fragile] @_TZFSsoi1pFTSiSi_Si : $@convention(thin) (Int, Int) -> Int

SIL 的一个真正不错的特性是它包含注释,解释生成的代码。let a: Int 语句会被翻译成 @_Tv4main1aSi : $Int,我们可以从位于生成的 SIL 上方的注释中看到这一点:

// main.a : Swift.Int
sil_global @_Tv4main1aSi : $Int

SIL 以混淆格式表示 Swift 代码。名称包含大量关于类型、名称中符号的数量等信息。一些混淆名称可能非常长,难以阅读,例如 _TZvOSs7Process11_unsafeArgvGVSs20UnsafeMutablePointerGS0_VSs4Int8__

我们可以使用 swift-demangle 工具将名称反混淆回其正常表示。让我们尝试反混淆 @_Tv4main1aSi 并看看它是否真的翻译成 main.a : Swift.Int

xcrun swift-demangle _Tv4main1aSi
_Tv4main1aSi ---> main.a : Swift.Int 

如果你想了解更多关于名称混淆的信息,你可以阅读 Mike Ash 写的一篇关于它的精彩文章,链接为 mikeash.com/pyblog/friday-qa-2014-08-15-swift-name-mangling.html

LLVM IR

中间表示IR)是一种更底层的代码表示。它不如 SIL 人类友好和可读。这是因为它为编译器提供了比人类更多的信息。我们可以使用 IR 来比较不同的编程语言。要获取 Swift 的 IR,使用 -emit-ir 选项,要获取 C 的 IR,我们可以使用 clang -emit-llvm

xcrun swiftc -emit-ir main.swift
clang -S -emit-llvm  main.c -o C-IR.txt

其他 swiftc 选项

swiftc 编译器非常强大,拥有许多模式和选项。你可以创建汇编、二进制文件、链接库和对象文件。你还可以指定许多选项,例如使用 -o 选项指定输出文件、优化 -O-Onone 以及许多其他选项:

xcrun swiftc -emit-assembly main.swift -o assembly

分析可执行文件

分析 swiftc 编译器生成的汇编代码非常困难。为了使我们的工作更轻松,我们将使用 Hopper Disassembler 工具来反汇编可执行文件,生成伪代码并进行分析。你可以从 www.hopperapp.com 下载 Hopper 的免费版本。

Hopper 反汇编工具可以与二进制、可执行和对象文件一起工作。使用它的最简单方法是使用 swiftc main.swift 命令生成一个可执行文件,然后在 Hopper 中打开它。您只需将 main 可执行文件拖放到 Hopper 中即可打开。

分析可执行文件

在左侧,您可以找到所有函数和变量的标签,并导航到它们。当您分析一个具有许多函数的大项目时,搜索功能非常有用。在中间是汇编代码;您可以按 Alt + Enter 来查看当前过程的伪代码。分析高级伪代码要容易得多。

我们还可以在 Xcode 中编译应用程序,并在 Hopper 中反汇编我们的 SimpleApp.app。这使得我们能够分析非常庞大和复杂的应用程序。

作为一项实验,让我们以两种方式编译相同的 swift 文件——启用优化和不启用优化——并比较生成的汇编代码。这样,您将看到优化选项的力量:

swiftc main.swift -O -o mainOptimized

摘要

本章介绍了 Swift 编译器、命令行工具以及 Swift 源代码的编译过程。了解可用的工具并掌握它们非常重要,因为它会使你更加高效。Xcode 有许多工具,我们向您展示了如何找到并使用它们。

对于分析和优化 Swift 代码,了解和理解编译过程非常有用。在本章中,我们带您经历了一次 Swift 编译器的完整旅程,从源代码开始,到可执行文件结束。我们还向您展示了如何获取特定编译步骤的结果,例如获取 SIL 或 IR 代码表示。

最后的想法

在本书中,我们的学习之旅即将结束,现在您已经掌握了 Swift 创建高性能应用程序的技术。让我们快速回顾一下。

到现在为止,您已经学会了如何利用 Swift 的力量并优化您的 Swift 代码,但您应该记住优化的主要规则——只在需要时优化,而不是一开始就优化。

稳定的架构和结构良好且干净的代码是优秀应用程序的两个最重要的特征。我们一直在本书中表达这一点,并有一个整个章节(第二章, 在 Swift 中创建良好的应用程序架构)是专门为此而设的。

性能优化并不总是需要给源代码带来很多复杂性。有时,通过应用一些小的改动,例如添加@noescape属性、移除一些print语句、使用正确的数据结构,以及书中提到的其他技术,可以在不负面影响源代码的情况下提高性能。有时,源代码甚至可以变得更加简洁和易读;例如,通过使用@noescape,在引用实例成员时我们不需要显式指定self.

在开始创建应用程序之前,学习 Swift 的功能和可用的工具非常重要。通过花一些时间进行规划和准备,从一开始就创建一个良好的应用程序要比后来尝试重构和修复性能以及代码架构问题容易得多。

现在,你已经准备好开始使用 Swift 创建令人难以置信的应用程序的旅程了!

posted @ 2025-10-24 10:07  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报