Module(模块)
-
Module(模块)
: 最小的代码单元。
一个Module
是机器代码和数据的最小单位,可以独立于其他代码单位进行链接。通常,Module
是通过编译单个源文件生成的目标文件。
例如:当前的test.m
被编译成目标文件test.o
,当前的目标文件就代表一个Module
。但是有一个问题,Module
在调用的时候会产生开销,比如我们在使用一个静态库的时候可以这样使用:
@import TestStaticFramework;
这个静态库中可能包含了许多的.o
文件。岂不是要导入很多的Module
?
并不需要,在静态链接的时候,也就是静态库链接到主项目
或者动态库
,最后生成可执行文件
或者动态库
时,静态链接器可以把多个Module
链接优化成一个,来减少原本多个Module
直接调用的问题。
-
module.modulemap
用来描述头文件
与module
之间的映射关系。
下面使我们经常用到的AFNetworking
产生的.modulemap
文件:
那么.modulemap
里面的这些代码又是什么意思呢?
/// 声明一个module A,它映射的头文件是 A.h
module A {
header "A.h"
}
/// 声明一个module B,它映射的头文件是 B.h
module B {
header "B.h"
/// 导出 A,这里假设 "B.h" 映入了 "A.h";
/// 那么 导出 的意思就是将"B.h"引入的其他的"头文件" 也暴露出来。
export A
}
---
通常我们看到的`module`里面是 {export *} ,如上面的`AFNetworking.modulemap`
"*" 是通配符,意思是所有引入的`头文件`,全部 导出。
我们来读一下AFNetworking.modulemap
:
/// framework module 名称 AFNetworking
framework module AFNetworking {
/// umbrella <目录> 伞柄 <目录>/.h
/// AFNetworking-umbrella.h 伞柄
/// AFNetworking-umbrella.h/.h 伞骨(里面引入的所有的其他头文件)
umbrella header "AFNetworking-umbrella.h"
/// 重新导出
export *
/// module: 子module*
module * { export * }
}
如果要显示指明
子module
的名称,要加上explicit
关键字:
/// 假设在 `AFNetworking.modulemap` 中显示指明 `子module`
framework module AFNetworking {
/// umbrella <目录> 伞柄 <目录>/.h
/// AFNetworking-umbrella.h 伞柄
/// AFNetworking-umbrella.h/.h 伞骨(里面引入的所有的其他头文件)
umbrella header "AFNetworking-umbrella.h"
/// 重新导出
export *
/// module: 子module*
module * { export * }
/// 假设 `子module` 叫 `SubAFN`
explicit module SubAFN {
header "SubAFN.h"
export *
}
}
- 我们的Xcode是自动开启
Module
的
所有开发中我们引入头文件的三种形式:
#include
、#import
、@import
最终都会被转换成@import
。
-
Module
到底有什么用呢?
在我们传统的#include
引入头文件,并且没有开启Module
的情况下。头文件被引入多少次就要被编译所少次。比如:
A.h
,此时被use.c
和use_1.c
引入,那么此时A.h
就要被编译两次。
此时Module
的好处就提现出来了。它会预先把A.h
编译成二进制文件,那么后面不管有多少个文件使用到A.h
,只有一个A.h
的二进制文件(除非A.h被改动)。
下面我们来演示一下:
1、module.modulemap
:
module A {
header "A.h"
}
module B {
header "B.h"
export A
}
2、终端指令:
# -fmodules:允许使用module语言来表示头文件
# -fmodule-map-file:module map的路径。如不指明默认module.modulemap
# -fmodules-cache-path:编译后的module缓存路径
clang -fmodules -fmodule-map-file=module.modulemap -fmodules-cache-path=../prebuilt -c use.c -o use.o
3、B.h
引用 A.h
, use.c
引用 B.h
:
可以看到生成了两个二级制文件,这就是我们头文件
预先编译的产物:
Swift库
-
我们都知道,在平常的开发中,
swift
和OC
代码的混编,我们都是使用桥接文件(Bridging-header)
。
但是在Framework
中,是不允许使用桥接文件
的。因此:我们不能使用桥接文件的方式进行混编Objective-C
代码的引用,需要用Swift Module
进行模块间的引用。
i
:首先创建自己的.modulemap
文件:
ii
:在Framework
的Build Settings
里面配置Module Map File
:
iii
:此时已经可以在swift
文件中使用OC
的代码了(同时OC
文件中使用swift
代码也是一样的):
这样就引出了另外一个问题:如果我不想将
YSStudent
暴露到Framework
之外怎么办?
这个时候我们就可以引入.private.modulemap
文件
framework module YSSwiftFramework_Private {
module YSOCStudent {
header "YSOCStudent.h"
export *
}
}
-
注意:①
_Private
首字母大写,② 文件名中要有.private
。
然后配置一下Build Settings
:
- 注意:这里虽然配置了
.private.modulemap
文件,但并不是真正的隐藏。我们通过模块访问,依然是可以访问到的:
@import YSSwiftFramework_Private.YSOCStudent;
这样做虽然做不到完全的隐藏,但是可以达到提醒使用者的作用,告诉用户这是一个私有库,不要随便使用。
这里还有另外一种方法可以达到上面的要求:
swift
和OC
遵守同一个协议,通过协议来调用OC
的代码,从而做到隐藏OC
代码的效果。
Swift Module
- Xcode 9 之后,Swift 开始支持静态库。
Swift 没有头文件的概念,那么我们外界要使用 Swift中用public
修饰的类
和函数
该怎么办呢?
Swift库中引入了一个全新的文件.swiftmodule
。
.swiftmodule
包含序列化过的AST
(抽象语法树,Abstract Syntax Tree),也包含STL
(Swift 中间语言,Swift Intermediate Language)。
比如我们刚刚的工程中,Xcode就给我们自动生成了.swiftmodule
文件。
我们要怎么用Swift Module
呢?下面我们通过swift静态库
来看一下。
Swift 静态库的合并
- 1、首先我们创建
SwiftA
&SwiftB
两个静态库,并且两个静态库中都包含YSSwiftTeacher
,名字和函数一模一样(埋点,看看后面会不会报错):
- 我们将生成的库文件拷贝出来,做一下合并:
libtool -static SwiftA SwiftB -o libSwiftC.a
此时会有一个警告,提示我们合并的两个库文件中有相同的文件。
这就是我们使用
libtool
的好处,我们之前讲过可以使用ar
来合并静态库,但是使用ar
的情况下,先解压再合并,可能发生文件的替换。我们来查看下当前静态库里面有哪些
.o
文件:可以看到,两个库里面的同名
.o
文件都在,并没有产生替换。
- 2、接下来我们将
.framework
里面原先的签名文件
,配置文件
删除,只留下Headers
和Modules
。因为我们需要的是合并后的静态库,所有原先的签名和配置没有用了。
- 3、将
Modules
里面的文件,提到和``Headers一个等级,这样做是为了后续SwiftC
编译器能够找到对应的Module
文件。要不然可能会找不到。
- 4、将我们合并后的静态库
libSwiftC.a
拖到工程里面,并且配置xcconfig
文件
HEADER_SEARCH_PATHS = $(inherited) "SwiftC/SwiftA.framework/Headers" "SwiftC/SwiftB.framework/Headers"
// OTHER_CFLAGS: 传递给 用来编译C或者OC的编译器,当前就是clang
OTHER_CFLAGS="-fmodule-map-file=${SRCROOT}/SwiftC/SwiftA.framework/module.modulemap" "-fmodule-map-file=${SRCROOT}/SwiftC/SwiftB.framework/module.modulemap"
// SWIFT_INCLUDE_PATHS: 传递给SwiftC编译器,告诉它去下面的路径查找module.file
SWIFT_INCLUDE_PATHS="${SRCROOT}/SwiftC/SwiftA.framework" "${SRCROOT}/SwiftC/SwiftB.framework"
这里强调一下: 配置
OTHER_CFLAGS
是为了给OC
代码用;配置SWIFT_INCLUDE_PATHS
是为了给Swift
代码用。
- 5、编译运行我们发现两点不同:
i
:OC
文件中同时使用A
和B
没有问题:
ii
:Swift
文件中不允许这样使用:
这也与两门语言的特性有关系,
OC
是运行时语言,我们已经告诉编译器,头文件的地址,所以只要运行时能够找到对应的符号就不会报错。但是Swift
就不一样,在编译的时候检测到可能存在的隐患就会报错。
另外,不管是静态库
还是动态库
的合并,大家尽量用不同的文件夹隔开要合并的库的库文件
,这样可以预防Header
里面有相同的文件(也就是我们上面的埋点)。
Swift配置
在我们日常的开发过程中,Swift
去使用OC
的一些方法的时候,Swift
会进行一些优化。
比如:
- 函数 :
/// OC 定义
- (void)ysOCFuncationWithValue:(NSString *)value WithKey:(NSString *)key;
/// Swift 使用
let obj = YSObject.init()
obj.ysOCFuncation(withValue: "123", withKey: "key")
/**************此时我们如果想要规范一下Swift中的函数名可以这样*****************/
- (void)ysOCFuncationWithValue:(NSString *)value WithKey:(NSString *)key
NS_SWIFT_NAME(ysAction(key:value:));
/// Swift 使用
let obj = YSObject.init()
obj.ysAction(key: "key", value: "123")
如果想要定义私有方法:
// NS_REFINED_FOR_SWIFT从现在开始,Swift的Clang Importer将做一些额外的工作并将该方法导入为私有方法,并以双下划线字符开头__,例如:
//- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> *)options;
- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> *)options
NS_REFINED_FOR_SWIFT;
- 上面的写法虽然可行,但是也存在一些弊端。如果我们要大批量的去修改已经稳定的
OC
的库的时候,就会是一个繁重的工作。 - 此时我们可以使用
.apinotes
文件来惊醒修改。
命名规则:SDK名称.apinotes
。并且该文件一定要放到SDK
的目录里面去。
Name: OCFramework // SDK 名称
Classes:
- Name: LGToSwift // 类名
SwiftName: ToSwift // 在Swift中的名称
Methods: // 方法
- Selector: "changeTeacherName:" // 方法名
Parameters:
- Position: 0
Nullability: O
MethodKind: Instance
SwiftPrivate: true
Availability: nonswift // 设置在Swift中不能用
AvailabilityMsg: "这个不能用" // 提示
- Selector: "initWithName:"
MethodKind: Instance
DesignatedInit: true
这个时候我们就可以结合脚本批量修改。
另外还有很多的用法,可以参考:API Notes