在 Swift 中使用工厂模式进行依赖注入

原文:Dependency injection using factories in Swift
原作者 @johnsundell
翻译:@OgreMergO

依赖注入是一项使得代码更加可测试的关键工具。我们不需要持有某些对象,或者创建这些对象的自有依赖,或者通过单例来获取他们,而是尽可能使那些对象能够正常运转所必须的一切内容(其他对象)通过外界传入,这样做的好处在于,一方面能清晰的看得到某个对象的所有依赖项,另一方便也使得测试工作变得更为简单(因为我们可以模拟这些依赖项来捕获、验证状态以及值。)

然而,尽管依赖注入确实很有用,但是当在工程中广泛使用的时候还是会有一些痛点。随着某个对象的依赖项越来越多,初始化该对象就变得越来越蹩脚。虽然使得代码可测没毛病,但是如果像下面这种每次需要这样来写初始化方法,也太不爽了。

1
2
3
4
5
6
class UserManager {
init(dataLoader: DataLoader, database: Database, cache: Cache,
keychain: Keychain, tokenManager: TokenManager) {
// ...
}
}

所以,这周咱们来深入了解一下某种依赖注入的技巧,使得我们的代码不失去可测性,我们也不需要再强迫自己去写一团初始化方法或者复杂的依赖管理的代码。

传递依赖项

我们遇到上面代码 demo 中的问题,最主要的原因是我们需要把这么多依赖项传递给某个对象,以便之后来使用。举例来说,我们在构建一个消息收发的 App,这里有一个 view controller 需要展示某个用户所有的消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MessageListViewController: UITableViewController {
private let loader: MessageLoader

init(loader: MessageLoader) {
self.loader = loader
super.init(nibName: nil, bundle: nil)
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

loader.load { [weak self]() messages in
self?.reloadTableView(with: messages)
}
}
}

如上代码能看到,我们给 MessageListViewController 传入某个依赖项 MessageLoader,之后其被用来加载数据。这里其实并没有太大的问题,因为仅仅只有一个依赖而已。然而,我们的列表视图并不是一个死气沉沉的展示而已,某些状态下还需要们进行导航到某视图控制器的工作。

具体来讲,我们想让用户在点击消息列表中某个 cell 的时候,导航到一个新的视图中。我们为这个新的视图创建一个视图控制器 MessageViewController,使得用户能够单独查看某条消息,并且能够回复该消息。为了该功能,我们实现了 MessageSender 类,当创建该类的时候,我们将前面那个新的视图控制器传递给他,代码类似下面这样:

1
2
3
4
5
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let message = messages[indexPath.row]()
let viewController = MessageViewController(message: message, sender: sender)
navigationController?.pushViewController(viewController, animated: true)
}

问题来了,MessageViewController 需要有一个 MessageSender 实例,我们也需要使得 MessageListViewController 看到该类。一种办法就是简单的,将 sender 加入到 列表视图控制器的初始化方法中,如下所示:

1
2
3
4
5
class MessageListViewController: UITableViewController {
init(loader: MessageLoader, sender: MessageSender) {
...
}
}

一旦如上面这样开始写代码,我们就逐步的进入庞大初始化方法的不归路上咯,然后使得 MessageListViewController 会变得越来越难用(也会让调用这很困惑,为什么一个列表视图控制器还需要关心某个发送消息的人?)。

另外一个可能的解决方案(也是一个很常用的解决方案),就是把 MessageSender 做成一个单例,这样的话,我们可以很容易在任何地方取到他的值,也可以随时将单例对象注入MessageViewController 中:

1
2
3
4
let viewController = MessageViewController(
message: message,
sender: MessageSender.shared
)

然而,就如 Avoiding singletons in Swift 这篇文章中讲的,单例这种方式会伴随一些明显的缺陷,导致我们会难以看清依赖关系,从而对整个框架都难以理解。

工厂模式来救场

Wouldn’t it be nice if we could just skip all of the above, and enable MessageListViewController to be completely unaware of MessageSender, and all other dependencies that any subsequent view controllers might need?

如果我们能够避免掉上面这些问题,能够使得 MessageListViewController 完全不关心 MessageSender,甚至是后续的视图控制器的其他依赖,岂不是很爽?

如果我们有某种形式的工厂,我们可以给其传入指定的 message,然后很方便的产出一个 MessageViewController 出来,类似下面这样,就能够很方便并且简洁的实现上面的理想:

1
let viewController = factory.makeMessageViewController(for: message)

Using the factory pattern to avoid shared state in Swift 这篇文章中我们看到的,关于工厂模式中,我最喜欢的一点就是,他能够使得你将某个对象的创建和使用两者解耦,也能使得许多对象和这些对象的依赖之间有一个相对解耦的关系,进而能使得我们想重构代码或者修改某些部分的时候相对更容易一些。

那我们该怎么做呢?

首先,我们定义一个工厂协议,该协议使得我们能够在并不知道某个视图控制器的依赖项或者其初始化方法的前提下,很容易的在我们的应用中创建出我们需要的任意的视图控制器。

1
2
3
4
protocol ViewControllerFactory {
func makeMessageListViewController() -> MessageListViewController
func makeMessageViewController(for message: Message) -> MessageViewController
}

到这里我们还不能停止。我们同样为工厂添加一些附件的协议用来创建视图控制器的依赖,比如下面这个协议,使得我们可以为某个列表视图控制器生成一个 MessageLoader 出来:

1
2
3
protocol MessageLoaderFactory {
func makeMessageLoader() -> MessageLoader
}

单例依赖

一旦我们准备好这些工厂协议之后,回到上面 MessageListViewController 的地方,重构这段代码,无需使用其依赖项的实例而是简单的引入一个工厂实例即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MessageListViewController: UITableViewController {


typealias Factory = MessageLoaderFactory & ViewControllerFactory

private let factory: Factory

private lazy var loader = factory.makeMessageLoader()

init(factory: Factory) {
self.factory = factory
super.init(nibName: nil, bundle: nil)
}
}

通过上面这么做,我们可以做到两点:

  1. 我们将一堆依赖项简化成了一个单一的工厂;
  2. MessageListViewController 无需再需再关心 MessageViewController 的依赖项了

一个使用 Container 的例子

接下来,我们该实现工厂协议了。首先,我们需要定义一个 DependencyContainer,该对象会包含我们应用中那些正常情况下会被直接用来作为依赖的核心工具对象们。这些不仅仅包括类似之前 MessageSender,也包括更加底层的业务逻辑上的类,比如我们可能会用到 NetworkManager

1
2
3
4
class DependencyContainer {
private lazy var messageSender = MessageSender(networkManager: networkManager)
private lazy var networkManager = NetworkManager(urlSession: .shared)
}

从上面这段代码,你能看到,我们使用了懒加载属性以便能够在初始化该对象的时候能够引用相同类中的其他属性。这是设置你依赖关系的一种非常方便而且优雅的方式,你可以利用编译器帮助你避免比如引用循环等问题。

最后,我们为 DependencyContainer 实现我们的工厂协议,使得我们能够将该工厂注入各种视图控制器或其他对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension DependencyContainer: ViewControllerFactory {
func makeMessageListViewController() -> MessageListViewController {
return MessageListViewController(factory: self)
}

func makeMessageViewController(for message: Message) -> MessageViewController {
return MessageViewController(message: message, sender: messageSender)
}
}

extension DependencyContainer: MessageLoaderFactory {
func makeMessageLoader() -> MessageLoader {
return MessageLoader(networkManager: networkManager)
}
}

分布式的所有权

最后一步了,我们在哪里实际储存依赖存储器,谁应该拥有它?它应该在哪里设置?这里有些比较 cool 的事情就是,由于我们把依赖容器作为对象们所需要的工厂的一种实现,而对象们强持有其工厂,所以,我们其实无需在任何地方储存该依赖容器。

举例来说,如果 MessageListViewController 是我们应用的初始化视图控制器,我们可以很简单的创建一个 DependencyContainer 的实例传入:

1
2
3
4
5
6
let container = DependencyContainer()
let listViewController = container.makeMessageListViewController()

window.rootViewController = UINavigationController(
rootViewController: listViewController
)

无需保留任何全局的变量或者在 app delegate 中使用可选属性。

总结

使用工厂协议和容器配置依赖注入是一种很好的方式,其可以避免需要传递大量依赖而创建很复杂的初始化方法。它可以使得依赖注入使用起来更加方便,使得你能够对自己创建的对象实际的依赖关系有很明晰的判断,也使得测试更加简单。

因为我们能够把工厂定义为协议,因此可以很容易的在测试中通过给定不同测试指定版本的具体实现来模拟输出。未来我会写大量关于模拟数据以及如何在测试中充分利用依赖注入的博文。

模块化 Xcode 工程

使用 Xcode 构建模块化的工程就需要对工程结构以及其基础概念有很好的理解才行。我们平时不怎么关注工程结构本身,只有在工程逐渐变大,要添加更多依赖的时候才会注意的到。而即使到了这个时候,我们大多数的工程都会使用 CocoaPods 来设置那些依赖项,或者 Carthage, 后者虽然没有帮我们做依赖性的设置,但是使得我们会更容易的,通过在工程的 build phase 选项中添加一些内容,达到同样的目的。当配置项越来越复杂,我们就很容易产生困惑,这是因为我们并没有完全掌握 Xcode 工程中所涉及的所有元素。我经常被问到的问题如下:

  • 我能不能在工程里同时使用 Carthage,Cocoapods 以及自己个人的依赖设置?
  • 我添加了依赖,但是当模拟器打开 App 的时候 Crash 了。
  • 为什么我需要只在某些 targets 里嵌入 framework?
  • 我的 framework 应该是静态的还是动态的?

在这篇博文中,我会引导你遍历 Xcode Project 中的各个元素,指导你如何通过改变这些元素来模块化你的设置项。我希望下次你遇到上面这些问题的时候,你不需要再花大量时间取 Stack Overflow 上查这些确定的答案。

Elements ⚒

Target

工程(Projects)都是由多个更小的叫做 target 的单元组成的。这些 target 包含编译特定平台产品,比如 frameworks, libraries, apps, testing bundles, extensions 等所需要的配置。 你可以在这里看到 target 所有可用的类型。 Target 可以相互依赖,当一个 target A 依赖另外一个 target B 的时候,target B 就会被先构建出来以便 target A 使用其产出。而 target 的配置项会涉及以下几个地方:

  • Info.plist 文件: 该文件包含产出特定的设置项,比如 版本、App 的名字或者 App 的类型,你可以在这里详细了解这个文件。
  • Entitlements: 其指定了应用的能力。如果在授权文件中指定的能力和开发者平台上设置的无法匹配,签名过程就会出错。
  • Build settings: 如其名字所描述的那样,这些都是构建 target 所必要的设置项。构建设置项要么在 target 自身定义或者在 xcconfig 文件中定义。一个 target 的配置项可以继承而来,首先是配置文件本身,其次是 target 的配置项,最后是 project 配置项。
  • Build phases: 构建流水线由 build phase 定义。当一个 target 被创建出来之后,其包含默认的构建阶段(包含 构建源码、拷贝资源等),但是你可以自行添加你需要的。举个例子,这些阶段里,有个 shell script phase 允许你在构建过程中执行一些脚本。这些脚本可以读取 Xcode 暴露出来的那些构建参数

基于.xcconfig文件的可组合性以及其可重用性的考虑,强烈建议你在这些文件中定义你那些编译设置。Target 的配置,比如 build setting 、build phase 等的变更都体现在 .pbxproj 文件中,这个文件是一种特殊的plist 文件,当我们使用 Git 管理我们的工程的时候,这个文件很容易出现冲突。当然,更新 pbxproj 文件中配置的最简单方式就是使用 Xcode,其了解如何从这些文件中读取配置和向其中写入配置。如果你对不使用 Xcode 更新 pbxproj 文件感兴趣的话,你可以试试 Xcodeproj 或者 xcproj

构建这些 target 的输出要么是比如 app,extension 或者测试文件等 bundles,要么就是intermediate products ,例如 library 或者那些封装了代码和资源文件用来给别的 target 使用的 framework。这些 Target 的输出内容你可以在工程文件中的 Products 的 Group 下找到,如果有红色的文件引用表示没有 product 输出,很大可能是你还没有构建过这个 target。

Scheme

Xcode 工程中另外一个要素是 scheme。 一个工程可以包含多个 scheme,他们可以被共享,作为工程的一部分被人们使用。这些 scheme 指定了 Xcode 中每个具体动作的配置,这些动作包括:runtestprofileanalyze 以及 archive。 细的来讲,可以指定哪些 target 需要被构建,以什么顺序构建甚至针对每一种动作指定不同的配置。

关于 scheme 的编译配置有一些东西要讲。当我们指定针对哪些动作构建哪些 target 的时候,在下面两种情况下,我们不需要指定每个 target 的依赖项:

  1. 如果依赖项是是相同 project 中的一部分,并且已经在 Target dependenciesBuild Phases 中定义过;
  2. 开启了 Find implicit dependencies

第 2 点中开启的标识,构建过程必须找到 target 的依赖项,并且先行构建。另外,如果你开启了Parallelize build 的话,一旦 target 相互之间没有依赖的话,就能够并行构建,因而会节省一部分时间。

一个有问题的构建配置会导致你编译 target 的时候出现错误,比如 Framework XXX not found。如果你曾经或者当前遇到了类似的报错,检查一下在构建每个 scheme 的时候,你的 target 的所有依赖是否已经被构建。

scheme 文件定义是存储在 Project.xcodeproj/xcshareddata/xcodeproj.xcscheme 路径下的一个标准的 XML 文本,因此你可以很容易的使用任意 XML 编辑器来修改它。

Workspace

多个 project 文件被组合成一个 workspace。当 project 被添加到一个 workspace 的时候:

  1. 其 schemes 会出现在 workspace 的 scheme 列表中;
  2. project 彼此可以产生依赖关系,后文会讲到。

和 scheme 类似,workspace 也是普通的 XML 文件,修改起来很方便。

工程构建组合中的每一个基础元素: target、scheme、project 以及 workspace 是如何被组织起来的

Dependencies 🌱

每个 target 都可以有依赖,这些依赖是 target 需要链接的那些 framework 以及 library等,其包含了能够被 target 共享的源代码以及资源。这些依赖项可以被静态或者动态的链接。

静态链接:

  • 发生在编译阶段;
  • 库(Library)中的代码文件会被包含到应用的二进制文件中(会增大应用的二进制大小);
  • 库使用 .a 作为文件后缀,其来自 (ar)chive file3 type;
  • 如果相同的库被多次链接,编译器会由于 duplicated symbols 编译失败。

动态链接:

  • 模块在应用启动或者运行过程中被加载;
  • 应用或者扩展的 target 都可以共享相同的动态库(仅被复制一份)

关于 framework 和 library(无论是静态链接还是动态链接)的区别在于前者可以在相同的 bundle 中包含多个版本,还可以包含额外的资源文件。

一个 Library 是一个 .a 文件,其来源于 归档(archive)文件类型。一个单一的归档文件仅支持单一的架构。如果需要打包多个架构,则需要将其打包成胖Match-O二进制(fat Match-O binary),该二进制文件是一种容器格式,其将支持不同架构的Mach-O打包在一起。如果我们想生成、修改一个胖二进制文件或者从其中提取某个特定架构的库的话,可以使用命令行工具lipo

你可以在这里了解更多关于 frameworks/libraries 以及 static/dynamic 的内容。

动态链接和静态链接的区别

应用的依赖项分为预编译过的(precompiled)和未经编译过(not-compiled)两类。

Precompiled dependencies

Carthage 是这类型依赖的典型代表。某些 SDK,比如 Firebase 就是作为编译过的依赖来发布的。当预编译过的依赖是库(library)的时候,这些依赖就包含 .a 的库及一个公共头文件,包含了该库所暴露出的公共接口。当这些依赖是 framework 的时候,这些依赖就以包含了库和资源文件的 .framework 文件发布。

当我们的 app 依赖的是预编译依赖的时候,很重要的一点是,这些依赖也是依照我们 app 所构建架构来构建出来的。一旦其中缺失某个架构的代码,我们就会在编译 app 的时候收到编译错误。一会儿后文会看到,Carthage 使用 lipo 工具生成那些包含模拟器或者真机所必须的架构的 framework 的,同时根据构建配置来剔除掉那些不需要的 framework。

Non-compiled dependencies

CocoaPods 是该种类型的典型代表。依赖项被定义在我们要链接的 frameworks/libraries 的 target 中。这里有多种方式在 Xcode 中指定我们的 target 依赖其他 target 的输出。

  • 如果这些 target 分布在同一个 project 中,你可以在 Build Phase 的Target dependencies 中指定依赖。 Xcode 会在编译该 target 的时候首先编译这些指定的依赖项;
  • 如果这些 target 分布在不同的 project 中,我们就可以使用Scheme来定义这些 target 之间的依赖关系。在 scheme 的 Build 部分,我们可以定义要被构建的 target 以及以什么顺序构建(基于他们之间的依赖关系)。如果你开启了Find implicit dependencies标识,Xcode 能够根据每个 target 的输入输出来猜测依赖。如果 scheme 中有错误配置,你就会得到类似xxxx.framework not found的错误。如果在 framework 之间出现了循环依赖也会报类似的错误。

关于依赖项和配置项有个需要注意的地方:所有依赖项的配置一定要完全匹配。如果你在使用 Alpha 配置项构建你的 app,但是其依赖项中但凡出现了不包含这种配置,编译过程都会因为找不到某个 framework 而失败。当这种情况发生的时候,Xcode 不会编译该 framework 但是不报任何错误。

各个依赖项是如何基于 project 的配置得到编译的

Linking with Xcode

Target 本身可以链接其他 target 的输出,我们可以使用 Xcode 中的工具,比如 scheme 或者 target dependencies 来指定依赖,但是,我们是如何通过定义这些依赖的链接关系来将它们融为一体的?

1. 动态或者静态链接 libraries 和 frameworks

我们可以通过以下的方式定义链接:

  • 一个构建阶段(build phase):,在所有可用的 build phase 中,有一个是定义链接的,Link Binary With Libraries。你可以在这里添加某个 target 的依赖项,这些依赖项可以来自于同一个 project,也可以来自同一个 workspace 中的其他 project。这个 build phase 被 Xcode 用来识别 target 被构建时所需的依赖项;
  • 编译器构建设置:一个 build phase 中所定义的内容会被转换成编译器参数。其中某些内容你也可以通过定义编译设置项做到:
  • FRAMEWORK_SEARCH_PATHS:定义编译器所链接的 framework 所在路径
  • LIBRARY_SEARCH_PATHS:定义编译器所链接的 library 所在路径
  • OTHER_LDFLAGS (Other Linker Flags):我们可以使用-l参数指定链接的 library,比如-l"1PasswordExtension" -l"Adjust"。如果需要链接一个 framework,就需要使用-framework参数,比如:-framework "GoogleSignIn" -framework "HockeySDK"。如果我们尝试链接一个无法在上方指定路径中找到的 framework 或者 library 的话,编译过程就会失败。

2. 暴露库的头文件

Library 的头文件需要暴露给依赖该库的 targe。为了做到这个,有一个编译设置项:HEADER_SEARCH_PATHS用来指定头文件所在路径。如果我们链接某个库,但是忘记暴露该库的头文件,编译过程就会因为找不到其头文件而失败。

3. 将 Framework 嵌入到应用中

App 的 target 链接动态 framework,需要把这些依赖项复制到应用的 bundle 中。这个过程被称作 framework embedding。为了达到这个目的,我们需要使用 Xcode 的Copy Files Phase,其拷贝 这些 framework 到 Frameworks目录中。不仅仅需要把这些直接依赖项嵌入应用中,还包括直接依赖所依赖的项目。如果缺少任意的 framework,当我们尝试打开 app 的时候都会抛出错误。


案例学习 👨‍💻

在这个部分,我们会分析以下 Cocopods 和 Carthage 是如何贯彻上面这些概念来管理你的工程依赖的。

CocoaPods

Cocoapods 解析你的工程依赖,并将它们融合到你的工程中。虽然直接修改你的工程配置是不太推荐的,但是它从最初的版本已经有了很大的提升,用这种方式,我们几乎不需要对 project 做很多改变。那么它底层到底是怎么做到的?

  • 它创建一个工程(project)(*Pods.xcodeproj*) ,其包含了所有的依赖项,每个依赖项以 target 的形式存在。每个 target 各自编译需要被链接到 app 中的依赖项;
  • 它创建一个额外的 target,其依赖于其他所有的依赖项。该 target 是一个 umbrella target,用来触发其他 target 的编译。这样做也最小程度的减少了你的 project 中所需要的改变。通过链接这个 target,Xcode 会先编译其所有依赖项,然后是你的 app;
  • 它创建了一个 workspace,包含了你的 project 以及 Pods project;
  • Frameworks 和 libraries 使用.xcconfig文件链接。这些文件被加到了你的 project 群组中,并且被设置为你 project 中 target 的配置项;
  • 嵌入过程是通过一个构建阶段脚本(build phase script)来做到的。类似的,所有的 framework 所需要的资源也通过一个构建阶段(build phase)来完成。

下面这张图展示了整个设置过程:

CocoaPods 如何将依赖项融合到整个 Project 中

Carthage

Carthage 的方式和 CocoaPods 比起来大不同。除了依赖项的解析,该工具是还一种去中心化的模式,其生成那些需要被链接或者嵌入到 app 的依赖项的预编译版本。

  • Carthage 解析依赖项,并且编译它们生成你能够链接到 app 中的动态 framework,或者为了调试所需要的符号。这些 framework 是 fat framework,支持模拟器和真机的架构;
  • 这些 framework 被用户使用 Link Binary With Libraries 的构建阶段(build phase)手动的链接;
  • 嵌入过程使用 Carthage 提供的脚本完成。这个脚本会剔除那些我们正在构建目标所不必要的架构版本;
  • 使用同样的脚本,复制符号到合适的文件夹,使得调试能够正常进行。

Carthage 是如何生成依赖项的 framework 和 symbol

References

处理 Swift 中非可选的可选值类型

原文:Handling non-optional optionals in Swift
原作者 @johnsundell
翻译:@OgreMergO

可选值(optionals)无可争议的是 swift 语言中最重要的特性之一,也是和其他语言,例如 Objective-C 的最大区别。通过强制处理那些有可能出现 nil 的地方,我们就能写出更有预测性的以及更健壮的代码。

然而,有些时候可选值可能会致你于尴尬的境地,尤其是你作为开发者了解(甚至是有些猜测的成分在),有的特定变量始终是非空(non-nil)的,即使它是一个可选类型。例如,我们在一个视图控制器中处理视图的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TableViewController: UIViewController {
var tableView: UITableView?

override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView(frame: view.bounds)
view.addSubview(tableView!)
}

func viewModelDidUpdate(_ viewModel: ViewModel) {
tableView?.reloadData()
}
}

这也是对于很多 Swift 程序员争论比较激烈的地方,程度不亚于讨论 tabs 和 spaces 的用法。有的人会说:

既然它是一个可选值,你就应该时刻使用 if let 或者 guard let 的方式进行解包。

然而另外一些人则采用完全相反,说:

既然你知道这个变量在使用的时候不会为 nil,使用 ! 强制解包多好。崩溃也要比让你的程序处于一个未知状态要好吧。

本质上来讲,我们这里讨论的是要不要采用防御性编程(defensive programming)的问题。我们是试图让程序从一个未知状态恢复还是简单的放弃,然后让它崩溃掉?

如果非得让我对这个问题给出一个答案的话,我更倾向于后者。未知状态真的很难追踪 bug,会导致执行很多不想执行的逻辑,采用防御性编程就会使得你的代码很难追踪,出现问题很难追踪。

但是,我不太喜欢给出一个二选一的答案。相反,我们可以寻找一些技术手法,用更精妙的方式的解决上面提到的问题。

它真的可选的吗?

那些可选类型的,但是被代码逻辑真实需要的变量和属性,实际上是架构瑕疵的一个体现。如果在某些地方确实需要它,但是它又不在,就会使得你的代码逻辑处于未知状态,那么它就不应该是可选类型的。

当然,在某些特定场景下,可选值确实很难避免(尤其是和特定的系统 API 交互的时候),那对于大部分这种情况,我们有一些技术来处理从而避免可选值。

lazy 要比非可选的可选值更好

某些属性的值需要在其父类创建之后再生成(比如视图控制器中的那些视图,应该在 loadView()或者 viewDidLoad()方法中被创建),对于这种属性要避免其可选类型的方法就是使用 lazy 属性。一个lazy属性是可以是非可选类型的,同时也不在其父类的初始化方法里被需要,它会在其第一次被获取的时候创建出来。

让我们改一下上面的代码,使用 lazy 来改造 tableView 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TableViewController: UIViewController {
lazy var tableView = UITableView()

override func viewDidLoad() {
super.viewDidLoad()
tableView.frame = view.bounds
view.addSubview(tableView)
}

func viewModelDidUpdate(_ viewModel: ViewModel) {
tableView.reloadData()
}
}

这样,没有可选值了,也不会有未知状态咯🎉

适当的依赖管理要比非可选的可选值要好

可选值类型另外一种常用的场景就是用来打破循环依赖(circular dependencies)。有的时候,你就陷入 A 依赖 B,B 又依赖 A 的情况,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class UserManager {
private weak var commentManager: CommentManager?

func userDidPostComment(_ comment: Comment) {
user.totalNumberOfComments += 1
}

func logOutCurrentUser() {
user.logOut()
commentManager?.clearCache()
}
}

class CommentManager {
private weak var userManager: UserManager?

func composer(_ composer: CommentComposer
didPostComment comment: Comment) {
userManager?.userDidPostComment(comment)
handle(comment)
}

func clearCache() {
cache.clear()
}
}

从上面的代码,我们可以看到,UserManagerCommentManager 之间有一个循环依赖的问题,它们二者都没法假设自己拥有对方,但是它们都在各自的代码逻辑里依赖彼此。这里就很容易产生 bug。

那要解决上面的问题,我们创建一个 CommentComposer 来做一个协调者,负责通知UserManagerCommentManager二人一个评论产生了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CommentComposer {
private let commentManager: CommentManager
private let userManager: UserManager
private lazy var textView = UITextView()

init(commentManager: CommentManager,
userManager: UserManager) {
self.commentManager = commentManager
self.userManager = userManager
}

func postComment() {
let comment = Comment(text: textView.text)
commentManager.handle(comment)
userManager.userDidPostComment(comment)
}
}

通过这种形式,UserManager 可以强持有 CommentManager 也不产生任何依赖循环。

1
2
3
4
5
6
7
8
9
10
11
class UserManager {
private let commentManager: CommentManager

init(commentManager: CommentManager) {
self.commentManager = commentManager
}

func userDidPostComment(_ comment: Comment) {
user.totalNumberOfComments += 1
}
}

我们又一次的移除了所有的可选类型,代码也更好预测了🎉。

优雅的崩溃(Crashing gracefully)

通过上面几个例子,我们通过对代码做一些调整,移除了可选类型从而排除了不确定性。然而,有的时候,移除可选类型是不可能的。让我们举个例子,比如你在加载一个本地的包含针对你 App 的配置项的 JSON 文件,这个操作本身一定会存在失败的情况,我们就需要添加错误处理。

继续上面这个场景,加载配置文件失败的时候继续执行代码就会使得你的 app 进入一个未知状态,在这种情况下,最好的方式让它崩溃。这样,我们会得到一个崩溃日志,希望这个问题能够在用户感知之前早早的被我们的测试人员以及 QA 处理掉。

所以,我们如何崩溃。。。最简单的方式就是添加 ! 操作符,针对这个可选值强制解包,就会在其是 nil 的时候发生崩溃:

1
let configuration = loadConfiguration()!

虽然这个方法比较简单,但是它有个比较大的问题,就是一旦这段代码崩溃,我们能得到的只有一个错误信息:

fatal error: unexpectedly found nil while unwrapping an Optional value

这个错误信息并不告诉我们为什么发生这个错误,在哪里发生的,给不了我们什么线索来解决它。这个时候,我们可以使用 guard 关键字,结合 preconditionFailure() 函数,在程序退出的时候给出定制消息。

1
2
3
4
guard let configuration = loadConfiguration() else {
preconditionFailure("Configuration couldn't be loaded. " +
"Verify that Config.JSON is valid.")
}

上面这段代码发生崩溃的时候,我们就能获得更多更有效的错误信息:

fatal error: Configuration couldn’t be loaded. Verify that Config.JSON is valid.: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17

这样,我们现在有了一个更清晰的解决问题的办法,能够准确的知道这个问题在我们代码里的哪个未知发生的。

引入 Require 库

使用上面的 guard-let-preconditionFailure 的方案还是有一些冗长,确实让我们呃代码更难驾驭。我们也确实不希望在我们的代码里占很多篇幅去些这种代码,我们想更专注于我们的代码逻辑上。

我的解决方案就是使用 Require。它只是简单的在可选值添加简单的 require() 方法,但能够使得调用的地方更简洁。用这种方法来处理上面加载 JSON 文件的代码就可以这样写:

1
let configuration = loadConfiguration().require(hint: "Verify that Config.JSON is valid")

当出现异常的时候,会给出下面的错误信息:

fatal error: Required value was nil. Debugging hint: Verify that Config.JSON is valid: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17

Require 的另一个优势就是它和调用 preconditionFailure() 方法一样也会抛异常 NSException,就能使得那些异常上报工具能够捕获异常发生时候的元数据。

你如果想在自己代码中使用的话,Require 现在在 Github 上开源了

总结

所以,总结来看,在 Swift 语言里处理那些非可选的可选值,我有几点自己的贴心小提示给大家:

  1. lazy 属性要比非可选的可选值要更好
  2. 适当的依赖管理要比非可选的可选值要好
  3. 当你使用非可选的可选值的时候,优雅的崩溃

如果有任何问题、建议或者反馈,都欢迎随时在 Twitter 上联系我,我非常乐意听到你们希望我在接下来的文章里谈论哪些主题哦。

谢谢阅读。

理解响应者和响应链

Apps 是通过响应者(responder)对象来接收和处理事件的。一个响应者对象是 UIResponder 类的一个实例,我们常见的 UIView,UIViewController 以及 UIApplication 都是 UIResponder 的子类。 UIKit 自动帮你管理着这些 responder 相关的行为,包括事件是如何从一个 responder 传递给另一个 responder 的等等。当然,你也可以修改你的 app 中事件传递的默认行为。

UIKit 会把大部分的事件都传递给最适合的 responder 对象来处理。如果该 responder 无法处理该事件,UIKit 就会继续把该事件沿着当前的响应者链传递到下一个 responder。响应者链就是你的 App 中所有响应者的动态配置,也因为其是动态的,你的 App 中不可能只存在单一的响应者链。由于事件总是从特定的响应者那里流转到更通用的响应者那里,因此很容易确定某响应者链中下一个响应者是谁。举个例子,一个 view 的下一个响应者是其 superview 或者负责管理它的 view controller。事件就是这样在响应者链中传递直到其被处理掉。

下图1 表明一个界面中包含了 一个 Label,一个 text field,一个 button 以及两个 background view 的 App 中响应链是什么样子的。如果 text field 不处理某个事件,UIKit 就会把该事件发送给 text field 的父级 UIView 对象,同样的,如果该对象依然处理不了,就会传递给该 view 对象的 window。如果 window 对象也依然无法处理该事件,UIKit 最终会把该事件传递给 UIApplication 对象,一般上该 UIApplication 对象是 App 的 delegate 对象并且是 UIResponder 实例,当然这个时候已经脱离了响应者链了。

图1 - 一个响应者链的例子

针对每一个事件,UIKit 都会指定一个第一响应者(first responder),然后把该事件首先发送给对象处理。这个第一响应者基于事件类型而不同。

  1. Touch events. 第一响应者是 Touch 发生所在的 view。
  2. Press events. 第一响应者是当前焦点所在的响应者。
  3. Motion event. 第一响应者是你显式指定用以处理事件的对象。 Core Motion 处理所有的和加速器、气压仪以及磁力计相关的事件。Motion events 不随响应者链流动。
  4. Shake-motion events. 第一响应者是你或者 UIKit 框架指定的对象。
  5. Remote-control events. 第一响应者是你或者 UIKit 框架指定的对象。
  6. Editing-menu messages. 第一响应者是你或者 UIKit 框架指定的对象。

Controls 向其关联的对象发送动作消息本身不是事件,但是依然能够享受到响应者链的好处。当一个 Control 的 target object 是 nil 的时候,UIKit 就会在该 target object 的响应者链上寻找合适的对象用以妥善处理动作消息。例如,UIKit 编辑按钮使用这种行为来寻找响应者对象来执行cut:, copy: 或者 paste: 等方法。

如果一个 view 自身还有附加的手势识别器的话,该手势识别器会延迟针对该 view 的 touch 和 press 事件的传递。delaysTouchesBegan, delaysTouchesEnded 以及 UIGestureRecognizercancelsTouchesInView 属性都是用来决定这些 touches 什么时间以及以什么方式被延迟处理。

识别包含 Touch 事件的响应者

UIKit 使用基于视图的碰撞检测(hit-testing)来决定 touch 事件发生的地点。具体而言,UIKit 拿该 touch 的位置和视图层级中所有的视图对象的 bounds 进行比较。UIView 的 hitTest:withEvent: 方法会游历整个视图层级,找到该 touch 事件发生所在的视图树最底端的视图,其也就是处理该 touch 事件的第一响应者。

如果一个 touch 的位置发生在某个视图的外围,hitTest:withEvent:方法就会忽略该视图和其所有子视图。所以,如果你将 view 的clipsToBounds属性设置为 NO 的化,即使其子视图将该 touch 包含在自己领域也是无效的。

就这样,UIKit 持续不断的把每个 touch 指派给包含该 touch 的视图。当 touch 发生的时候,UIKit 创建一个 UITouch 对象,直到 touch 结束该对象才会被释放。当 touch 位置或者其他参数发生变化的时候,UIKit 就更新这个 UITouch 对象的信息。当然,其中有的属性当然是不会变化的,比如该 touch 附属的 view,甚至当 touch 位置已经超出原始 view 的外围的时候,UITouch 对象中的 view 属性依然保持和之前一样。

变更响应者链

你可以通过覆写响应者的 nextResponder 属性来改变响应者链。许多 UIKit 的类已经覆写了该方法并且返回了特定的对象。

  1. 如果你覆写了任意类的 nextResponder 属性,那该对象的下一个响应者就是你返回的那个;
  2. UIView
    2.1 如果该视图是某个视图控制器的根视图(root view),其下一个响应者就是该视图控制器;
    2.2 如果该视图不是某个视图控制器的根视图,其下一个响应者就是该视图的父视图;
  3. UIViewController
    3.1 如果该视图控制器的视图是某个 window 的根视图(root view),其下一个响应者就是 window;
    3.2 如果该视图控制器是由另一个视图控制器展示出来的,其下一个响应者就是那个视图控制器(presenting view controller);
  4. UIWindow window 的下一个响应者就是 UIApplication 对象
  5. UIApplication UIApplication 对象的下一个响应者是 App delegate,而且要求该 delegate 必须是 UIRepsponder 的实例并且不能是一个视图、视图控制器或者 UIApplication 对象自己。

Capturing objects in Swift closures

原文:Capturing objects in Swift closures
原作者 @johnsundell
翻译:@OgreMergO

自从 Block 在 iOS4 被引入 Objective-C 的世界之后就成为了 Apple 各平台上最时髦的 API 的重要组成部分了。当 Swift 语言出现的时候,blocks 的概念就摇身一变通过 closure 的形式引入,成为了目前我们可能每一天都在用的语言特性之一了。

Closure 目前已经被我们广泛的使用了,即使如此,我们在使用它的时候还是需要有很多需要注意的点,并且需要做很多额外的操作。这篇文章,我们来近距离的了解 closure,主要是了解其捕获变量的机制以及那些能够更好的让我们来处理变量捕获的技术。

伟大的 escape

Closure 有两种类型:escaping 和 non-escaping。当一个 closure 是 escaping(使用 @escaping 修饰闭包参数)的,也就意味着其会被以各种形式存储下来(无论是通过 property 还是被其他 closure 捕获)。相反,Non-Escaping 的 closure 意味着其不能被存储,而且在使用它的地方就必须直接被执行。

译者注: 可以参见 《@autoclosure && @escape》 一文。

一个显而易见的例子就是当你在一个集合类型上使用函数式操作的时候,例如 forEach

1
2
3
[1, 2, 3].forEach { number in
...
}

如上代码所示,closure 都直接作用于集合的每一个元素上,也就无需把该闭包变为 escaping 的。

而 escaping 的 closures 最常见的就是在那些异步 API 中,例如 DispatchQueue。例如,当你异步执行某 closure 的时候,这个 closure 就会 escape。

1
2
3
DispatchQueue.main.async {
...
}

所以,这两种的区别在哪里呢? 由于 escaping 的 closure 会被以某种形式存储下来,因此,这些 closure 会同时存储当前其所处的上下文,同时就会把上下文中用到的值或者对象都捕获(capture)到,以至于当该 closure 被执行的时候,所需用到的内容没有丢失。实践中最常见的就是在 closure 中使用 self 的 API,此时,我们就需要某种办法显式的捕获 self。

捕获 & 引用循环

因为 escaping 的 closures 会自助捕获在其内部使用的任何值和对象,因此很容易发生引用循环。举个例子,下面描述的是一个 view controller 被其存储的 viewModel 的 closure 捕获的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ListViewController: UITableViewController {
private let viewModel: ListViewModel

init(viewModel: ListViewModel) {
self.viewModel = viewModel

super.init(nibName: nil, bundle: nil)

viewModel.observeNumberOfItemsChanged {
// This will cause a retain cycle, since our view controller
// retains its view model, which in turn retains the view
// controller by capturing it in an escaping closure.
self.tableView.reloadData()
}
}
}

最常见的方式,也就是你们大部分人也都会用的解决方式,通过弱引用的方式打破这个循环引用。

1
2
3
viewModel.observeNumberOfItemsChanged { [weak self] in
self?.tableView.reloadData()
}

捕获 context 而不是捕获 self

上面提到的 [weak self] 的解决方案已经是你希望避免引用循环的最常用,也常常是最有效的解决方案了。但是这种方式也有一些问题:

  1. 很容易忘掉写,尤其是编译器又没检查出来潜在的引用循环的时候;
  2. 当你希望从 weak self 中强持有 self 的时候还需要写一堆代码(weak strong dance),例如下面这段代码所示:
1
2
3
4
5
6
7
8
9
dataLoader.loadData(from: url) { [weak self] data in
guard let strongSelf = self else {
return
}

let model = try strongSelf.parser.parse(data, using: strongSelf.schema)
strongSelf.titleLabel.text = model.title
strongSelf.textLabel.text = model.text
}

这里其实有一个可选的解决方案,也就是不要捕获 self,而去捕获那些闭包中所需要的对象即可。例如上面例子中的 labels 和 schema 等,我们可以直接捕获它们而不至于引发引用循环(因为其也并不持有 closure 本身),下面是个解决方案,通过使用 context 的 tuple 来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// We define a context tuple that contains all of our closure's dependencies
let context = (
parser: parser,
schema: schema,
titleLabel: titleLabel,
textLabel: textLabel
)

dataLoader.loadData(from: url) { data in
// We can now use the context instead of having to capture 'self'
let model = try context.parser.parse(data, using: context.schema)
context.titleLabel.text = model.title
context.textLabel.text = model.text
}

通过显式传递参数而不是隐式的捕获

这里,还有另外一种捕获对象的方式,就是显式的把这些对象通过参数传入。这种手法我在设计我的 Image Engine 项目中的 Event API 的时候用到了,这个 API 就是当使用 closure 来监听 event 的时候,需要你传递一个 observer 给它。如下所示,你把 self 传入进来的同时也使得其被传递到了 event 的 closure 中了,这也使得 self 被隐式的带入,你也无需手动的捕获它了。

1
2
3
actor.events.moved.addObserver(self) { scene in
...
}

我们回到之前的 ListViewController 的例子中,看一看当我们要监听其 viewModel 的时候,我们是如何通过上面这种手法来实现同样的 API 的。这种方式正好使得我们可以将要 reload 的 tableView 作为观测者传递,实现一个很优雅的调用:

1
2
3
viewModel.numberOfItemsChanged.addObserver(tableView) { tableView in
tableView.reloadData()
}

当然,需要实现上面这段代码,我们还需要做一些事情,就像 Image Engine 的事件系统如何工作类似。我们首先定义一个简单的 Event 类型,其可以记录那些观测闭包。

1
2
3
class Event {
private var observers = [() -> Void]()
}

然后,我们添加一个方法,该方法会传两个参数进来,一个是引用类型的观测者,另外一个是一个闭包,当观察动作一旦触发,该闭包就会被调用。核心就在这里,我们会封装该闭包,并且在内部闭包中弱捕获该观测者:

1
2
3
4
5
func addObserver<T: AnyObject>(_ observer: T, using closure: @escaping (T) -> Void) {
observers.append { [weak observer] in
observer.map(closure)
}
}

这样就使得我们只需要做这么一次 weak/string 的操作,也不影响其调用的地方。

最后,我们添加一个 trigger 方法来使得我们能够触发事件本身。

1
2
3
4
5
func trigger() {
for observer in observers {
observer()
}
}

然后回到 ListViewModel,为 numberOfItemsChanged 方法添加事件,当某个条件满足的时候,就会触发该事件。

1
2
3
4
5
6
7
8
9
10
class ListViewModel {
let numberOfItemsChanged = Event()
var items: [Item] { didSet { itemsDidChange(from: oldValue) } }

private func itemsDidChange(from previousItems: [Item]) {
if previousItems.count != items.count {
numberOfItemsChanged.trigger()
}
}
}

如上面所看到的,基于 event 的 API 的最大优势就是最大程度上避免了引用循环的发生。我们也可以在我们的代码中为任意类型的观察事件重用相同的执行代码。当然,上面的 demo 中 Event 实现非常简单,缺乏一些高级特性,比如针对观察者的移除等等,但是对于简单使用已经足够了。

我们会在之后的博文中详细的讲述事件驱动的编程范式,你也可以详细看下在 Image Engine 项目中的 Event 类型的完整实现

总结

Closure 自动捕获其内部所使用的对象和值本身是一个非常棒的特色,它使得 closure 本身变得非常好用。但是,捕获同时也引入了一些 bug 和引用循环的问题,甚至最后使得代码变得复杂和难以理解。

当然,我并不是建议大家在所有的场景下去避免捕获发生,而是想通过这篇文章提供给大家一些捕获 self 的选择。在某些场景下,使用经典的 [weak self] 是最有效的解决方案,另外一些场景则可以使用某些手法来帮助你把自己的闭包代码写的更容易使用,也更容易理解吧。

@autoclosure && @escape

我们知道在 swift 中,闭包(closure)是一等公民,因此可以被当作参数传递,在学习 swift 的过程中经常会看到某些关键字修饰该闭包,@autoclosure@escape 就是其中比较常见的两种关键字。

@escape 和 @nonescape

当一个闭包被当作参数传递给一个函数,但是当该函数内容执行完毕返回之后,该闭包才会被执行,我们就称该闭包要 escape 某个函数,那 @escape 关键字就是用来表示该闭包是允许在函数返回之后被调用的。

我们用 swift 官方文档的例子来看,如下所示

1
2
3
4
5
var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_:) 的参数是一个闭包,函数内部会把传入的闭包存到之前声明的数组里以便之后进行调用,可以看到,在函数参数的声明部分添加了 @escaping 关键字,如果这里不添加的话,就会在编译的时候报错:

1
2
error: passing non-escaping parameter 'completionHandler' to function expecting an @escaping closure
completionHandlers.append(completionHandler)

针对标记了 @escaping 关键字含义代表你必须在该闭包内部显式的使用 self 关键字,官方文档中又列举了另外一个例子,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}

class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100”

someFunctionWithEscapingClosure(_:) 是一个可逃逸的闭包,意味着你需要显示的调用 self 关键字, 而 someFunctionWithNonescapingClosure(_:) 是非逃逸的闭包,意味着你可以隐式的调用 self。

@autoclosure

例子讲解

通过一个🌰来说明 @autoclosure 关键字到底起到什么作用。

考虑下面这个函数 f,其需传入一个参数,类型是 ()-> Bool 的闭包。

1
2
3
4
5
func f(predicate: () -> Bool) {
if predicate() {
print("It's true")
}
}

然后通过传入符合此类型的闭包进行调用

1
2
f(predicate: {2 > 1})
// "It's true"

但是,如果我们忽略传入闭包的 {} ,编译就会错误。

1
2
f(predicate: 2 > 1)
// error: '>' produces 'Bool', not the expected contextual result type '() -> Bool'

如文档中所说,一个 autoclosure (自主闭包?)是这样一种闭包,当某个表达式被当做参数传递给一个函数的时候会被 wrap 成一个闭包,该闭包没有任何参数,其被调用的时候,返回的是其 wrap 的表达式的值。swift 提供这种语法糖就能够让你省略 {} 而直接写一个表达式。

结合上面的例子来看,当你写个表达式类似 2 > 1 传给函数 f 的时候,该表达式会被自动包裹到一个闭包中,会自动处理为 { 2 > 1 } 而传递给函数 f。

1
2
3
4
5
6
7
8
func f(predicate: @autoclosure () -> Bool) {
if predicate() {
print("It's true")
}
}

f(predicate: 2 > 1)
// It's true

⚠️ @autoclosure 并不支持带有输入参数的写法,也就是说只有形如 () -> T 的参数才能简化

Delay Evaluation

swift 提供了 ?? 操作符,如下所示:

1
2
3
let nickName: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickName ?? fullName)

如果某 Optional 存在就会返回其值,如果没有就会返回后面的默认值,当我们去看 ?? 的实现的时候能看到如下定义:

1
2
3
func ??<T>(optional: T?, defaultValue: @autoclosure () -> T?) -> T?

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T

看得出来 ?? 是一个二元操作符,optional 指代 ?? 前面的输入,defaultValue 指代 ?? 后面的参数,那我们就会想,我们上面的例子中 fullName 只是一个 String,怎么变成 () -> T 类型的呢? 这个就看前面的 @autoclosure 的威力了,前面讲过了,该关键字把表达式的值封装成闭包并且返回该表达式的值了。 其实传入该方法的第二个参数是 { fullName }

所以可以想到该方法的实现应该如下所示,(当然 fullName 为 String 类型,应该会重载第二个函数实现)

1
2
3
4
5
6
7
8
func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
switch optional {
case .Some(let value):
return value
case .None:
return defaultValue()
}
}

这里我们还需要注意一点的是,使用 @autoclosure 来修饰的表达式可以实现延迟计算,也就是说直到该闭包被调用之前,闭包里所被包裹的表达式都不会进行取值计算,也就避免了一定的开销,尤其是上面默认值是复杂计算得到的话。

关于 iOS10 Notification 的那些事儿

概览

推送通知我们大家都不陌生,可以说几乎每个使用智能手机的人每天都会被不同的通知 打扰 到,正式因为合适的推送是吸引用户注意力的利器,其成为了各 App 吸引用户,将用户带回到 App 本身,提升用户的活跃度的一种必要的方式。当然要注意的是,推送本身是一件对用户影响特别大的事情,毕竟注意力被打断,因此合适的推送时机也是各个 App 开发者所要注意的,否则就会成为用户勿扰名单里的一员了。

之前刚开始学习 iOS 开发的时候还整理了下当时部署 iOS 远程推送的流程,详见:iOS 远端推送部署详解

接下来,我们大致回顾一下 iOS 平台关于推送都有哪些历程?

阅读全文

阅读《垃圾回收的算法与实现》

前一阵子 《垃圾回收的算法与实现》 这本书比较火,正好本人也对垃圾回收这个概念挺感兴趣的,就耐着性子一点一点啃,到今天只能说磕磕绊绊的看了大部分,实现篇只看了 Python 的部分,剩余的关于 Dalvik VM、Rubinius 以及 V8 的垃圾回收并未多看,主要还是自己对 Javascript,Ruby 等语言未有深入学习,我深以为只有结合这种语言本身的语言特性来看对应的垃圾回收实现才有意义。这篇文章主要是总结下学习到的一些主要的知识点。

阅读全文


Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2017 Writing, Thinking and Coding. All Rights Reserved.

访客数 : | 访问量 :