想直接了解modules更官方解释的可参考官方文档:
https://clang.llvm.org/docs/Modules.html#introduction
import 和 include
在了解Module化之前我们需要先了解一下OC的import机制。#import <SDWebImage/UIImage+Gif.h>
,日常开发中都写这样的代码,用来引用其他的头文件。在C和C++
里是没有#import
的,只有#include
,用来包含头文件。#include
做的事情其实就是简单的复制粘贴,将目标.h文件中的内容一字不落地拷贝到当前文件中,并替换掉这句include
,而#import
实质上做的事情和#include
是一样的,只不过OC
为了避免重复引用可能带来的编译错误(这种情况在引用关系复杂的时候很可能发生,比如B和C都引用了A,D又同时引用了B和C,这样A中定义的东西就在D中被定义了两次,重复了),而加入了#import
,从而保证每个头文件只会被引用一次
import 导入
预处理之后的
AppDelegate
include
include单次导入是和import是一样的,这里不再赘述
重点看下重复导入的情况
使用include会把
ViewController.h
头文件拷贝两次,并且编译报错重复定义使用import 重复导入
import = #pragma once + include
使用#pragma once
修饰后可正常编译, 预编译后的文件也只copy一次
具体实现原理是通过 #ifndef #define 一个头文件标记,在处理头文件引用的时候则判断该标记,如果已经定义过就不再对目标文件进行粘贴。
无论是include
和import
本质都是对头文件的复制粘贴,这样就带来一个问题:当引用关系很复杂,或者一个头文件被非常多的实现文件引用时,编译时引用所占的代码量就会大幅上升, 因为被引用的头文件在各个地方都被copy
了一遍。为了解决这个问题,C系语言引入了预编译头文件(PreCompiled Header)
,将公用的头文件放入预编译头文件中预先进行编译,然后在真正编译工程时再将预先编译好的产物加入到所有待编译的Source中去,来加快编译速度。比如iOS开发中的.pch文件
就是一个预编译头文件
,默认情况下,它引用了UIKit和Foundation
两个头文件
于是理论上说,想要提高编译速度,可以把所有头文件引用都放到pch
中。但是这样面临的问题是在工程中随处可用本来不应该能访问的东西,而编译器也无法准确给出错误或者警告,无形中增加了出错的可能性。
swift
在Objective-C
中,我们需要通过#import 或#include
导入头文件,编译器在预处理阶段
将这些头文件里面的API复制到当前文件,然后可以访问, swift
缺少预处理阶段,头文件不能像 Objective-C
一样使用#import 或#include
导入了,但还是需要类似的操作才能访问到这些API。那么swift该怎么引用呢?
OC的编译过程
- 预处理
- 词法分析
- 语法分析
- 静态分析
- 生成汇编指令
- 汇编
- 链接
Swift编译
- 解析
- 语义分析
- Clang 导入器
- SIL 生成
- SIL 保证转换
- SIL 优化
- LLVM IR 生成
对于OC和Swift混编中,肯定会涉及到头文件的引用,swift文件中直接导入肯定是不行的!
苹果提供了两种方案来实现混编调用
1.桥接文件
调用 Objective-C
文件,主要通过桥接文件来实现,在桥接文件中导入需要暴露给 Swift
模块的 Objective-C
类头文件,即可在 Swift
模块中直接调用,如下图所示:
调用 Objective-C 编写并打包封装的静态/动态库
不同于文件调用,Swift 调用 Objective-C 库可以通过 LLVM 的 Module
系统来实现,Xcode 打包时默认支持Module
系统,它对当前开发者来说没有任何成本,只是 Framework 中自动生成 modulemap 文件(后文会介绍),我们主要通过这个描述文件来访问。
桥接文件、Module 是目前我们实现 Swift 调用 OC 的主要方案,对Swift开发者来说,这两种方案的成本都比较低,桥接文件相当于我们的头文件汇总声明,而Module对调用方来说,基本是零成本
Modules
Apple于2012年引入了Module
机制,不同于#include
,Module
采用了更高效的语义模型。用树形的结构化描述来取代以往的平坦式#include
并缓存下来,生成一个树形结构的modulemap
,用于描述框架、系统头文件、控制导出的范围、依赖关系、链接参数等等。
对于支持Module
的模块,在编译时会被当做一个独立的编译单元
,该单元只会被编译一次,编译器会维护一个已经编译的单元列表。如果在目标文件中引用到了某个Module
,首先会在这个单元列表中进行查找,如果没有找到会进入编译流程并添加进来,如果找到了则直接使用已编译好的Module
单元,类似于App中常用的缓存机制。在Module
这个引用机制下,预处理消耗由M*N
减低到了M+N
的级别。
在实际使用中,可以将Module
看作一个框架接入的中间件,这个中间件维护了编译单元和具体headers的路由关系
,这种路由关系是通过modulemap
这种形式来表达的。我们通过UIKit的modulemap
来具体解释一下:
-
umbrella header
我们应该非常熟悉<UIKit.h>
这个文件,在modulemap
中,它起到一个汇总的作用,我们查看UIKit.h
这个文件,会发现它其实是对UIKit
所有可调用头文件的汇总,这里主要是为了语法上的便利,避免在该文件中描述所有的头文件。 -
export*
export *
语义作用是描述所有子Module
,即:假如调用方可能需要调用UIKit
中所有的头文件,只需要声明UIKit即可. -
explicit module
explicit module
语义是针对子Module
进行的描述,它将Module
根据实际需要拆分成了不同的子Module
。即:调用方需要针对调用手势识别相关的内容,只声明UIGestureRecognizerSubclass
这个Module
即可。
import UIKit.UIGestureRecognizerSubclass
Module 实践
在Xcode内开启Module支持选项
手动为工程开启支持module
选项很简单,只需要将Target
配置选项开启即可。
Framework如何支持
创建支持Module的Framework
在Xcode中创建framework并支持Module也是很简单的: 在新建Framework的时候,Xcode默认在build settings中设置了Defines Module为YES:
当你编译framework的时候,在对应路径下就已经生成了module文件,格式为:
如果没有引用其他framework, xcode中默认不显示Products目录,在General->Frameworks and Libraries 中随便添加一个就可以显示了