原文地址 www.jianshu.com
假设有 A.h、B.h 两个头文件、c.m、d.m 两个实现文件,两个. m 文件都使用 #include 引入 A、B 两个头文件。当编译两个. m 文件会导致 A、B 两个头文件分别被编译两次。
为了解决头文件重复编译这个问题现在基本上都使用 #import 引入头文件,使用 #import 会默认开启 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 就不用重新编译了。
// 声明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
的路径。
module 官方介绍
如果我们的Framework
中需要用到Swift-OC
混编,但是Framework
中不能使用桥接文件,因此这种情况下可以使用Module
解决。
创建NASwiftFramework
和NAOCFramework
项目时选择Framework
由于NASwiftFramework
中使用了Swift-OC
混编,因此编译出现错误,现在我们需要创建Module
文件解决这个问题。
NASwiftFramework.modulemap
文件(也可以从其他地方Copy
,Copy
时需要勾选Add to targets
才能参与编译)Module Map File
文件路径现在NASwiftFramework
能够编译成功,并且在NAApp
项目中也能使用NAOCStudent
如果我们不想直接对外暴漏我们的OC
类,我们可以创建NASwiftFramework.private.modulemap
framework module NASwiftFramework_Private { // _Private必须添加,且首字母大写
module NAOCStudent {
header "NAOCStudent.h"
export *
}
}
然后在 Private Module Map File 中指定路径。切换到NASwiftFramework
项目进行**重新编译**
现在NAApp
项目中#import
会报错,但是我们可以通过
@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
)。
在 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
也能看到:
两个库均是静态库并且有一个相同的类
@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
在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
@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 使用中做一定的规范,可以进行以下操作。
NS_SWIFT_NAME(<#*name#>): 给 OC 方法取别名
NS_TYPED_ENUM:让编译器使用 enum
NS_TYPED_EXTENSIBLE_ENUM:让编译器使用 Struct
NS_REFINED_FOR_SWIFT 在 Swift 方法中, 编译器会在名称前加上双下划线__
通过宏配置的弊端:
需要手动修改每个地方的源代码,工作量大
官方文档
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