源起
四年前,在我强大的忽悠能力下,成功说服了领导使用 Swift 来开发新 APP。那时候自己也没有多想太多,例如稳定性问题、效率问题、维护成本问题等等。选择 Swift 开发的理由,仅是怀着学习的心态,以及对 OC 反人类的语法感到厌恶。也正因选择了 Swift,这几年来,我每年都得以学习一门新语言,它们分别为 Swift 2、Swift 3、Swift 5…...
在这几年时间了,我做两件非常有意思的小事情,第一件是将 OC 的 APP 用 Swift 来重写,以跟进时代的步伐;第二件是为了满足公司对"热"的要求,而将 Swift 的代码改成 OC。虽说这两件微不足道的小事情并不是发生在同一家公司中,但却感触深刻,每每想起,总有那么一种冲动,觉得自己需要写点什么。
现在,Swift 来到了 5.0 这个重要的里程碑上,作为一名 Swifter,希望自己能对这门语言有更深入的了解,同时尝试给大家分享下自己对这门语言的一些经验与见解。在这个系列中,主要会针对 Swift 的 "现代"、"安全"、"快" 三个方面,从源码层去了解其具体的实现,当然在这个系列中还会包含 Swift 标准库的应用与实现进行分析,从底层角度了解 Swift 这门有那么一点意思的语言。
在立下这个 flag 的同时,也给自己挖了一个巨坑,这希望自己在填坑的过程中,可以与大家共同学习成长,在 Swift 造诣上,更上一层楼。
环境准备
开发环境
masOS 10.14.5,Xcode 10.3,默认使用当下最新的正式版本。
编译 Swift 源码
我们可以在 GitHub 上的 Swift 仓库 查看支 Swift 的所有源码代码,然后 Clone 到本地进行阅读,如果直接阅读 Clone 下来的 Swift 库,你们发现大量的 .swift.gyb 的文件,GYB 文件是苹果团队预处理的一种文件,里面包含着其它语言,会影响到我们对 Swift 源码的阅读,所以我们需要对从 GitHub Clone 的代码进行一次编译,生成阅读性更强的 Swift 代码。具体操作如下:
# 安装编译工具
brew install cmake ninja
# 创建源码放置目录
mkdir swift-source
cd swift-source
# 拉取源码
git clone https://github.com/apple/swift.git
# 拉取编译所需依赖的仓库
./swift/utils/update-checkout --clone
最后一行命令会拉取编译 Swift 库所依赖的仓库,在天朝,如果下载失败或者下载慢,可能要多试几次才能成功,我们都懂。完成上面的步骤后,我们可以执行 apple 给我们提供好的 build 脚本进行编译:
./swift/utils/build-script -x -R
- -x 会生成一个 Xcode 的项目,我们可能通过 Xcode 来阅读源码
- -R 指的是使用 release 进行编译,编译更快,产物也更小,机器快,硬盘大的小伙伴们可无视。
整个编译过程是比较漫长的,当然最终编译速度最终还是得看机器的性能,欧洲人请无视。
更新 Swift 源码
随着 Swift 的迭代开发,我们想查看 Swift 新特性的源码时,就需要我们去更新 Swift 源码,当然我们不需要重新执行上面的操作,我们只需要执行 update-checkout 脚本并重新编译:
./swift/utils/update-checkout
./swift/utils/build-script -x -R
切换 Swift 版本
如果我们想查看某个版本中特有的特性,这个时候我们需要切换 Swift 的版本,但是如果只是简单地切换 Swift 仓库的分支是无法编译通过的,他对应的依赖也需要变更,我们可以通过 apple 提供的脚本进行切换版本:
./swift/utils/update-checkout --tag swift-5.0-RELEASE
gyb 转换为 Swift 代码
因为 Swift 的编译需要较长时候,并且占用大量硬盘空间,那么我们可以针对单个 gyb 文件进行转换:
./swift/utils/gyb \
--line-directive '' \
-o ./xxxxxxx/Sequence.swift \
./swift/stdlib/public/core/Sequence.swift.gyb
- --line-directive '' 用于在生成的文件中去掉不必要的说明;
- -o 指定输出目录;
- 最后是指定需要转换的 gyb 文件目录。
Hello World
在编译完成 Swift 源码后,可以通过 Xcode 来查看 Swift 的源码。当然,我们还是从万码起源的 Hello World 开始去尝试阅读 Swift 源码。
print("Hello World")
这可能是你曾经写下的第一行代码,那么,我们来看看在 Swift 中,这行代码到底是怎么实现的。
查看源码
如果你想查看一个方法的源码的方式有两种:
- 如果你知道是具体的那个文件的类,则可以通过打开对应的文件查看,但是这种方法通常都是不太好使的,毕竟我们记不住那么多文件名;
- 通过全局搜索:**public func 方法名( **的方式来定位 public 方法的具体位置,协议、类也是同理。
因为 print() 方法的源码放在 Print.swift 文件中,所以上面所说的两种方式都是可行的。
print 源码解读
通过上述方式,我们可以找到 print() 的源码实现如下:
public func print(
_ items: Any...,
separator: String = " ",
terminator: String = "\n"
) {
if let hook = _playgroundPrintHook {
var output = _TeeStream(left: "", right: _Stdout())
_print(items, separator: separator, terminator: terminator, to: &output)
hook(output.left)
}
else {
var output = _Stdout()
_print(items, separator: separator, terminator: terminator, to: &output)
}
}
首先我们可以看到 print() 方法是一个全局的 public 方法,所以我们可以在任意的地方调用他,其次该方法支持三个参数,最后两位是带默认值的参数。这就是给我们最直观的感觉,接下来我们来看下他的内部实现:
if let hook = _playgroundPrintHook {...} else {...}
判断当前开发环境是不是 playground,如果是则将 output 定向到了 _TeeStream,如果不是则使用 _Stdout。
这样我就可以很好理解 Playground 右边显示值的功能是怎么实现的了。
接下来我们继续来阅读 print() 的源码,不管是否 plyground,最终都会调用 _print() 方法:
internal func _print<Target : TextOutputStream>(
_ items: [Any],
separator: String = " ",
terminator: String = "\n",
to output: inout Target
) {
var prefix = ""
output._lock()
defer { output._unlock() }
for item in items {
output.write(prefix)
_print_unlocked(item, &output)
prefix = separator
}
output.write(terminator)
}
我们可以看到,首先将 output 加锁,然后通过 defer 关键字在函数 return 前将 output 解锁,保证了 output 在 write 过程中是线程安全的。然后我们再看 _print_unlocked() 方法:
internal func _print_unlocked<T, TargetStream : TextOutputStream>(
_ value: T, _ target: inout TargetStream
) {
if _isOptional(type(of: value)) {
let debugPrintable = value as! CustomDebugStringConvertible
debugPrintable.debugDescription.write(to: &target)
return
}
if case let streamableObject as TextOutputStreamable = value {
streamableObject.write(to: &target)
return
}
if case let printableObject as CustomStringConvertible = value {
printableObject.description.write(to: &target)
return
}
if case let debugPrintableObject as CustomDebugStringConvertible = value {
debugPrintableObject.debugDescription.write(to: &target)
return
}
let mirror = Mirror(reflecting: value)
_adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false)
}
从源码来看,可以看出来,如果输入的对象如果实现 TextOutputStreamable,则打印出来的是它的值,如果它实现的是 CustomStringConvertible 或者 CustomDebugStringConvertible 时,print 实际打印出来的 description 内容。
CustomStringConvertible 和 CustomDebugStringConvertible 都是熟悉的协议,没有必要过多但是绝对,但对于 TextOutputStreamable 这种比较陌生的协议,必要时需要我们去查阅读文档:
遇到陌生的关键字或者协议时,不需要着急,果爸爸的文档一般都很详细的,可以优先查看文档,阅读 API 文档可以快速了解学习一门语言。
继续阅读源码发现,如 value 实现以上协议时,则可以输出相应的描述内容,如果未实现以上协议:
let mirror = Mirror(reflecting: value)
_adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false)
则通过 Mirror 映射输出相应的值, _adHocPrint_unlocked() 的具体实现就不继续纠缠,否则就没完没了,感觉兴趣的读者可以自行研究下去。
小结
作为开篇,写到这里就结束了,介绍了如何搭建环境和一个简单例子,在后面章节里,我们再来详细聊聊 Swift 的那些事。
作为一名作者,最大的成就感就是读者在看了你的文章后,又有提笔写文的冲动。作为一名程序员的我,最大的成就感莫过于你看了我的文章后,有想打开电脑写下几行代码的冲动。