如果使用得当,预编译头文件可以为您节省宝贵的编译时间。但如果使用不当,预编译头文件可能会隐藏源代码中的问题,而这些问题可能会在你尝试在另一个项目中重复使用部分源代码时才被发现。
本文是Objective-C 中的代码气味系列文章中的一篇。
预编译头文件的用途
发明预编译头文件的目的只有一个:"加快编译速度"。与反复解析相同的头文件相比,这些文件只需提前解析一次。速度非常重要!编译速度越快,就能越快查看最近的更改是否成功,越快完成反馈循环。
在 Xcode 中,您可以将所需的头文件包含在 "prefix header"中,并启用 "Precompile Prefix Header",从而对其进行预编译。但前缀头文件背后的理念与预编译不同。前缀头文件隐含在每个源文件的开头。例如,如果你的前缀头是 Prefix.pch
,那么每个源文件就会偷偷地
#import "Prefix.pch"
将其放在文件顶端,比其他任何东西都先。这对于整个项目的 #defines
来说很方便。(请记住,一般来说,#defines
是一种代码气味)。
对于预编译头文件来说也很方便。事实上,每个源文件都包含这些预编译的头文件,这也是前缀头文件的一个特点。
这就是事情开始出错的地方......
预编译头文件的存在并不是为了让你省去打字的麻烦
Apple 的 iOS 项目模板以 Prefix.pch
开始,其中包括 Foundation
和 UIKit
。从编译速度的角度来看,这非常合理。问题是,人们注意到了这一点,并说:"这些文件已经隐含包含了。所以我不需要再次包含它们"。发现这种副作用后,一些程序员开始向 Prefix.pch
添加更多的头文件。因为这样就不用再 #import
(导入)了。
目的从 "尽可能快地编译这个项目 "转变为 "节省自己的打字时间"。Stack Overflow 的一个问题就反映了这一点,它问道:"为什么有重复的#import?甚至维基百科的前缀词条也反映了这一不正确的结论:"因此,没有必要明确包含上述任何文件"。这种误解非常普遍。
这完全是错误的。
过度依赖预编译头文件的四个问题
问题在于,要成功编译一个文件,仅有成对的头文件(.h)和实现文件(.m)已经不够了。你还需要 Prefix.pch
——不是因为它们是预编译的,而是因为它们是隐式包含的。
"所以呢?"你问。"是什么阻碍了你?"基本上,你最终会创建不完整的源文件。至少有四种方式会导致问题:
1、源文件无法复制到不同的项目中
假如你在前缀头文件中添加了 <QuartzCore/QuartzCore.h>
。某个源文件使用了 QuartzCore
。试着将该源文件复制到另一个项目中。
很有可能无法编译,因为另一个项目的预编译头文件不同。你设法创建了一个不可移植的源文件!
2、依赖关系被隐藏
任何导入其他文件的系统都有一个好处,那就是可以显示文件的依赖关系。你可以扫描 .h 或 .m 文件的开头,看看它还使用了哪些其他文件。这可以让你快速了解文件的范围。
如果你的导入是隐式绑定在前缀头文件中,情况就不一样了。
3、依赖关系被掩藏
一个大型项目可能有大量的预编译头文件。假设你正在查看一个源文件,并试图找到它的依赖关系。你很聪明地意识到,早期的程序员依赖预编译头文件来节省输入,省略了许多 #import
。所以你也查看了前缀文件。
但是,如果 Prefix.pch
中的 #import
语句不只几条,你的源文件需要哪些语句?全部?不需要?一些?哪些?
4、依赖关系失控
即使将所有 #imports
明确化,也很容易产生爆炸性的文件依赖关系。让依赖树保持稳定已经很不容易了。
但是,如果没有努力做到:
a)使所有 #import
明确化;
b)驯服它们,
这些依赖关系就会悄无声息地发展到不可收拾的地步。依赖关系的腐烂会在不知不觉中蔓延多年,直到为时已晚。突然间,你要开发一个新项目,却没有一种简洁的方法来重用以前的代码,而又不会把它们都变成大量浪费的垃圾。
查找并修复缺失的 #import
由于 Xcode 将前缀头文件与预编译头文件结合在一起的方式,省略 #import
语句是一种常见的 Objective-C 代码气味。但这是一种不寻常的现象,因为这种气味本身可能在很长时间内都不会被注意到。(无声却致命!)。
要解决问题,就必须找到问题所在。而要想找到问题,就必须暂时移除阻嗅器:
- 编辑你的前缀文件。暂时注释掉所有
#import
和#include
语句。(译者注,PS: 个人感觉对于一些明确的基类或者基础的三方库就别注释了