重拾iOS-import

关键词:#import,#include,@class,Modules,预处理(preprocessor)

一、概述

#include是C/C++导入头文件的关键字;
#import是Objective-C导入头文件关键字;
@class告诉编译器某个类的声明,当执行时,才去查看类的实现文件,可以解决头文件的相互包含的问题;

二、#import"xxx.h" 和 #import 的区别

1)区别

#import: 引用系统文件,它用于对系统自带的头文件的引用,编译器会在系统文件目录下去查找该文件。

#import"xxx.h": 用户自定义的文件用双引号引用,编译器首先会在用户目录下查找,然后到安装目录中查。

双引号是用于本地的头文件,需要指定相对路径,尖括号是全局的引用,其路径由编译器提供,如引用系统的库。

2)use header map

Enable the use of Header Maps, which provide the compiler with a mapping from textual header names to their locations, bypassing the normal compiler header search path mechanisms. This allows source code to include headers from various locations in the file system without needing to update the header search path build settings。

意思是Xcode开启这个开关后,在本地会根据当前目录生成一份文件名和相对路径的映射,依靠这个映射,我们可以直接import工程里的文件,不需要依靠header search path。

如果关闭use header map那么就涉及到xcode build settings中的header search pathuser header search path了。

两者都是提供search path的,区别在于一个指明是用户的。并且提到如果编译器不支持user headers概念,会从header search paths中去寻找。


三、存在的问题

传统的#include/#import都是文本语义: 预处理器在处理的时候会把这一行替换成对应头文件的文本。

这样就会导致以下问题:

问题1)大量的预处理消耗;

假如有N个头文件,每个头文件又#include了M个头文件,那么整个预处理的消耗是N*M。这可能会在你的头文件里面引入数量非常庞大的代码。假如换成 C++,那情况就更糟了,因为它还包含了一些模板代码,数量比 C 还要多好几倍;

问题2)文件导入后,宏定义容易出现问题;

因为是文本导入,并且按照include依次替换,当一个头文件定义了#define std hello_world,而第另一个头文件刚好又是C++标准库,那么include顺序不同,可能会导致所有的std都会被替换。那最终运行时结果就出乎意料了,这使得预处理变得非常“脆弱”;

问题3)边界不明显;

什么时候用什么工具、库来开发软件,仅仅从头文件上面看其实你并不能看得懂,因为它并不是“语义化”的,比如哪些命名空间属于特定的库,比如拿到一组.a和.h文件,很难确定.h是属于哪个.a的?这些命名空间又该以如何的顺序包含,需要以什么样的顺序导入才能正确编译?或者你又只想引入这个库的一部分定义,只通过 “#include”之类的预处理真的是太难搞清楚了;

问题4)引用泛滥;

A 类导入了 C 类的头文件,B 类也导入了 C 类的头文件,D 类又同时导入 A 和 B 类,这就是重复导入;

问题5)交叉引用;

如果有循环依赖关系,如:A–>B, B–>A这样的相互依赖关系。如:



当在TestA类的.h中 #import "TestB.h" 并声明TestB属性时报错:
Unknown type name 'TestB'; did you mean 'TestA'?

解决办法:在.h文件中使用@class,然后在.m文件中#import "TestB.h"即可。

原因: @class TestB;这句话的意思就是,告诉编译器,确实有TestB这个类,具体细节你不用管,别报错就行了。所以显然,到了.m里,它只知道有这个类,却不知道这个类有什么属性,有哪些方法。所以需要在.m再 import这个头文件。

【注意】在.m文件中#import "TestB.h" 并声明TestB属性是不会报错的;

尽量在.m而不是.h里使用import引用

推荐尽量在.m里引用头文件,而不是在.h里,必要时使用@class

在编译效率方面考虑,如果你有100个头文件都#import了同一个头文件,或者这些文件是依次引用的,如A–>B, B–>C, C–>D这样的引用关系。当最开始的那个头文件有变化的话,后面所有引用它的类都需要重新编译,如果你的类有很多的话,这将耗费大量的时间。而是用@class则不会。

但是也有一些情况,是不可避免要在.h里引用的。比如:继承某个类,必须在.h里 import 父类的.h;类实现某个接口,必须在.h里引用接口的.h等等。


四、Clang Module

Modules 是一种语义化的引入定义的方法。

clang module不再使用文本模型, 而是采用更高效的语义模型。clang module提供了一种新的导入方式:@import,module会被作为一个独立的模块编译,并且产生独立的缓存,从而大幅度提高预处理效率,这样时间消耗从M*N变成了M+N。

// Swift
@import WebKit.WebKitLegacy; //in Objective-C
import WebKit.WebKitLegacy   //in Swift
// OC
#import "TestA.h"
#import 

可以看到 Objective-C 和 Swift 都非常好地支持了 Modules import,你可以非常清晰地引入 API 声明。

当你使用 Modules 引入时,预处理器并不会像 “#include”那样使用 M*N 量级的重复拷贝粘贴。而是巧妙地通过一个列表来存放已经编译处理过的 Modules 列表,而声明的引入会首先在这个表内查找,如果没有找到会去编译添加进来。所以 Modules 的引入只会被处理一次,可以解决前面提到的引用泛滥问题。

自 Xcode5以来,build settings 都默认开启了 “-fmodules”,一般来讲你的代码里面都可以使用 Modules 来引入其他库。其实 modules 是一种头文件编译后的 map,所以 Modules 始终都能保证你所引入的定义是存在的、有意义的。(其实 Modules 是一种从 precompile headers 演变过来的技术)

modules 和 headers 通过一个 map 来进行一种关系映射,这个 map 文件就叫做 modulemap. 这个文件从语义上描述了你的函数库物理结构。

举个例子,用 std 这个 module 来描述 C 的标准库。那么 C 标准库里面的那些头文件:stdio.h, stdlib.h, math.h 都可以映射到 std 这个 module 里面,他们就组成了几个 子模块(submodule): std.io, std.lib, std.math。通过这样一个映射关系,C 的标准库就可以构建出一个独立的 module。所以通常地,一个库就只有一个 module.modulemap 文件用于描述它的所有头文件映射。

那么实际在编译过程中 Modules 到底代表着什么呢?我们前面说过其实 Modules 是一种预编译技术,当一个模块被导入时,编译器在处理它时会生成一个新的子进程(非 fork),这个子进程拥有干净的 context来编译这个 module(这样就不会产生命名空间冲突等干扰),然后 module 的编译结果会被持久化到这个模块的二进制缓存中,那么下次引用编译的时候就会非常快。 modules 由头文件映射而成,所以当这些头文件改动时,module 还会自动重新编译刷新缓存,不需要我们主动干预。


相关参考

1)LLVM的 Modules https://www.stephenw.cc/2017/08/23/llvm-modules/

2)关于Objective-C中的import https://juejin.im/post/58d88c7ab123db199f442aec

3)相互引用头文件问题 https://juejin.im/post/5aaf6943518825556e5de48e

你可能感兴趣的:(重拾iOS-import)