目录
- 一、Module简介
- 二、分析Module文件
- 三、Swift Framework中使用Module
- 四、Swift静态库合并
- 五、OC代码映射到Swift的方式
- 六、Module 相关的 Build Setting 参数
- 总结
一、Module简介
Module(模块)-最小的代码单元。
- 一个Module是机器代码和数据的最小单位,可以独立于其他代码单位进行链接。
- 通常,Module是通过编译单个源文件生成的目标文件。例如,当前的test.m被编译成目标文件test.o时,当前的目标文件就代表了一个Module。
- 但有一个问题,Module在调用的时候会产生开销,比如我们在使用一个静态库的时候,可以这样使用
假设有A.h、B.h两个头文件、c.m、d.m两个实现文件,两个.m文件都使用#include引入A、B两个头文件。当编译两个.m文件会导致A、B两个头文件分别被编译两次。
为了解决头文件重复编译这个问题现在基本上都使用#import引入头文件,使用#import会默认开启Module,这样头文件会预先编译成二进制,再有文件导入时就不会重新编译。
二、分析Module文件
2.1、通过Module编译代码
准备如下文件:
代码如下:
/* A.h */
#ifdef ENABLE_A
void a() {}
#endif
/* B.h */
#import "A.h"
/* module.modulemap */
module A {
header "A.h"
}
module B {
header "B.h"
export A
}
/* use.c */
#import "B.h"
void use() {
#ifdef ENABLE_A
a();
#endif
}
build.sh
文件代码如下:
# -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
执行build.sh
文件会在prebuilt文件夹中生成两个pcm文件:
两个文件就是预编译好的二进制代码,如果其他文件再引入A和B就不用重新编译了。
2.2、查看AFNetworking文件的modulemap文件
// 声明framework的module名称为AFNetworking
framework module AFNetworking {
// 导入文件的集合(如果没有关键字header那么umbrella后面需要跟上头文件的文件夹名称)
umbrella header "AFNetworking-umbrella.h"
export * //把引入的头文件重新导出。
module * { export * } //把导入头文件修饰成子module,并把符号全部导出(第一个通配符*表示子module名称和父module名称一致)
// 如果要指定子module的名称需要使用explicit关键字
// eg:
explicit module NANetworking {
header "NANetworking.h"
export *
}
}
由于我们的项目中会默认开启module
,因此无论我们使用#include
,#import
都会自动转变为@import
,编译的时候都会被优化成module
形式,也就是同一个文件只会被编译一次。
如果希望使用我们自定的module
文件,那么需要在Build Setting
中设置module map file
的路径。
三、Swift Framework中使用Module
如果我们的Framework
中需要用到Swift-OC
混编,但是Framework
中不能使用桥接文件,因此这种情况下可以使用Module
解决。
3.1、创建如下项目文件:
创建NASwiftFramework
和NAOCFramework
项目时选择Framework
由于NASwiftFramework
中使用了Swift-OC
混编,因此编译出现错误,现在我们需要创建Module
文件解决这个问题。
3.2、创建NASwiftFramework.modulemap
文件(也可以从其他地方Copy
,Copy
时需要勾选Add to targets
才能参与编译)
3.3、设置NAOCStudent.h头文件为Public
3.4、设置Module Map File
文件路径
现在NASwiftFramework
能够编译成功,并且在NAApp
项目中也能使用NAOCStudent
3.5、Private Module
如果我们不想直接对外暴漏我们的OC
类,我们可以创建NASwiftFramework.private.modulemap
framework module NASwiftFramework_Private { // _Private必须添加,且首字母大写
module NAOCStudent {
header "NAOCStudent.h"
export *
}
}
然后在Private Module Map File 中指定路径。切换到NASwiftFramework
项目进行重新编译
现在NAApp
项目中#import <NASwiftFramework/NAOCStudent.h>
会报错,但是我们可以通过
@import NASwiftFramework_Private.NAOCStudent;
来访问NAOCStudent
。如果这一步报如下错误:
Undefined symbols for architecture arm64:
"_OBJC_CLASS_$_NAOCStudent", referenced from:
objc-class-ref in ViewController.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
需要将NASwiftFramework.framework
拖到NAApp
项目中
因此Private Module
不是真正意义上的私有,只是供开发者区分。如果确实希望隐藏OC
代码可以定义相关的协议,Swift
通过协议调用OC
代码,只对协议进行公开(Build Phases->Headers
中设置协议为Public
,OC
头文件为Private
。如果上面Private Module例子中将NAOCStudent.h
设置为Private
那么Swift
类中也不能使用NAOCStudent
)。
四、Swift静态库合并
4.1、Swift头文件
在Xcode 9之后,Swift开始支持静态库。Swift没有头文件的概念,那么我们外界要使用Swift中用Public
修饰的类和函数怎么办?
Swift库中引入了一个全新的文件.swiftmodule
。
.swiftmodule
包含序列化过的(AST
抽象语法树,Abstract Syntax Tree
),也包含SIL
(Swift 中间语言,Swift Intermediate Language
)。
在上面编译的NASwiftFramework.framework
->Show in Finder
->Modules
->NASwiftFramework.swiftmodule
也能看到:
4.2、创建两个Framework库,分别为MySwiftA和MySwiftB
两个库均是静态库并且有一个相同的类
@objc open class MySwiftTeacher: NSObject {
public func speek() {
print("speek!")
}
@objc public func walk() {
print("walk!")
}
}
并把两个静态库编译后的Framework
Copy 放到Products
目录下(两个项目均添加以下脚本)
cp -Rv -- "${BUILT_PRODUCTS_DIR}/" "${SOURCE_ROOT}/../Products"
编译后结果如下:
合并两个静态库(由于静态库是.o文件的合集,因此合并这两个静态库会产生冲突)
cd Products目录
libtool -static MySwiftA.framework/MySwiftA MySwiftB.framework/MySwiftB -o libMySwiftC.a
//日志警告,两个静态库都包含MySwiftTeacher.o
我们通过ar -t libMySwiftC.a查看libMySwiftC.a中的目标文件
__.SYMDEF
MySwiftA_vers.o
MySwiftTeacher.o
MySwiftB_vers.o
MySwiftTeacher.o
4.3、我们手动组合MySwiftC库
在MyApp
项目中新建MySwiftC文件夹,并Copy上面生成的相关文件
拖入静态库(勾选Copy item if need
):
首次拖入静态库时没有
Frameworks
文件夹,需要先将静态库拖到General->TARGETS->Frameworks,Libraries,and Embedded Content
,然后将Frameworks
文件夹中的静态库删除重新拖入并勾选Copy item if need
配置MyApp.Debug.xcconfig文件
HEADER_SEARCH_PATHS = $(inherited) '${SRCROOT}/MySwiftC/Public/MySwiftA.framework/Headers' '${SRCROOT}/MySwiftC/Public/MySwiftB.framework/Headers'
// OTHER_CFLAGS:传递给用来编译C或者OC的编译器,当前就是clang
// -fmodule-map-file: 要加载的module map文件路径
// OC文件中使用静态库需配置如下参数
OTHER_CFLAGS = $(inherited) '-fmodule-map-file=${SRCROOT}/MySwiftC/Public/MySwiftA.framework/module.modulemap' '-fmodule-map-file=${SRCROOT}/MySwiftC/Public/MySwiftB.framework/module.modulemap'
// SWIFT_INCLUDE_PATHS: 传递给SwiftC编译器,告诉他去下面的路径中查找module
// Swift文件中使用静态库需配置如下参数
SWIFT_INCLUDE_PATHS = $(inherited) '${SRCROOT}/MySwiftC/Public/MySwiftA.framework' '${SRCROOT}/MySwiftC/Public/MySwiftB.framework'
现在MyApp
项目中就可以使用静态库了:
ViewController.m
#import "ViewController.h"
#import <MySwiftA-Swift.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
MySwiftTeacher *t = [MySwiftTeacher new];
}
MySwiftTest.swift
import Foundation
import MySwiftA
@objc open class MySwiftTest: MySwiftTeacher {
public override init() {
super.init()
}
}
五、OC代码映射到Swift的方式
为了让OC代码在Swift使用中做一定的规范,可以进行以下操作。
5.1使用宏
NS_SWIFT_NAME(<#*name#>):给OC方法取别名
NS_TYPED_ENUM:让编译器使用enum
NS_TYPED_EXTENSIBLE_ENUM:让编译器使用Struct
NS_REFINED_FOR_SWIFT 在Swift方法中, 编译器会在名称前加上双下划线__
通过宏配置的弊端:
需要手动修改每个地方的源代码,工作量大
5.2.使用apinotes文件
官方文档
apinotes文件命名规则:前面是项目或者SDK的名称后缀是apinotes
apinotes文件必须放到SDK目录中
---
Name: OCFramework
Classes:
- Name: NAToSwift
SwiftName: ToSwift #Swift代码中使用的类名
Methods:
- Selector: "changeTeacherName:"
Parameters:
- Position: 0
Nullability: O
MethodKind: Instance
SwiftPrivate: true
# Availability: nonswift #在Swift中是否可用
# AvailabilityMsg: "prefer 'deinit'" #在Swift中不可用的原因
- Selector: "initWithName:" #设置其他方法
MethodKind: Instance
DesignatedInit: true
六、Module 相关的 Build Setting 参数
6.1对module自身的描述:
- DEFINES_MODULE:YES/NO,module 化需要设置为 YES
- MODULEMAP_FILE:指向 module.modulemap 路径
- HEADER_SEARCH_PATHS:modulemap 内定义的 Objective-C 头文件,必须在 HEADER_SEARCH_PATHS 内能搜索到
- PRODUCT_MODULE_NAME:module 名称,默认和 Target name 相同
6.2对外部module的引用
- FRAMEWORK_SEARCH_PATHS:依赖的 Framework 搜索路径
- OTHER_CFLAGS:编译选项,可配置依赖的其他 modulemap 文件路径 -fmodule-map-file={modulemap_path}
- HEADER_SEARCH_PATHS:头文件搜索路径,可用于配置源码中引用的其他 Library 的头文件
- OTHER_LDFLAGS:依赖其他二进制的编译依赖选项 SWIFT_INCLUDE_PATHS:swiftmodule 搜索路径,可用于配置依赖的其他swiftmodule
- OTHER_SWIFT_FLAGS:Swift 编译选项,可配置依赖的其他 modulemap 文件路径 -Xcc -fmodule-map-file=
参考:https://www.jianshu.com/p/d5ca6f0b9ec8
总结
- module -> 头文件->目标文件的关系
- modulemap ->头文件 -> 目标文件的映射
- module:定义一个module
export :导出当前代表的头文件使用的头文件
export * :匹配目录下所有的头文件
module * :目录下所有的头文件都当作一个子module
explicit : 显式声明一个module的名称 - Swift库使用OC代码:不能使用桥接文件
1. oc的头文件放到modulemap下
2. oc的头文件放到私有的modulemap下
3. 协议的方式 投机取巧 - Swift静态库的合并
难点:.swiftmodule 文件(相当于Swift的头文件)- libtool 合并静态库本身
- 用到的头文件和Swift头文件和modulemap文件通过目录的形式放到一起
- OC要用合并的静态库:clang: other c flags :-fmodule-map-file <modulemap path>
- Swift要用合并的静态库 : SwiftC :other swift flags 显式告诉SwiftC <modulemap dir>
- OC映射到Swift方式
- 宏
- <工程名称>.apinotes