Module与Swift库

原文地址 www.jianshu.com

一、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的路径。

module 官方介绍

三、Swift Framework 中使用 Module

如果我们的Framework中需要用到Swift-OC混编,但是Framework中不能使用桥接文件,因此这种情况下可以使用Module解决。

3.1、创建如下项目文件:

创建NASwiftFrameworkNAOCFramework项目时选择Framework

由于NASwiftFramework中使用了Swift-OC混编,因此编译出现错误,现在我们需要创建Module文件解决这个问题。

3.2、创建NASwiftFramework.modulemap文件(也可以从其他地方CopyCopy时需要勾选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 中设置协议为PublicOC头文件为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

总结

  1. module -> 头文件 -> 目标文件的关系
  2. modulemap -> 头文件 -> 目标文件的映射
  3. module: 定义一个 module
    export :导出当前代表的头文件使用的头文件
    export * :匹配目录下所有的头文件
    module * :目录下所有的头文件都当作一个子 module
    explicit : 显式声明一个 module 的名称
  4. Swift 库使用 OC 代码:不能使用桥接文件
    1. oc 的头文件放到 modulemap 下
    2. oc 的头文件放到私有的 modulemap 下
    3. 协议的方式 投机取巧
  5. Swift 静态库的合并
    难点:.swiftmodule 文件(相当于 Swift 的头文件)
    1. libtool 合并静态库本身
    2. 用到的头文件和 Swift 头文件和 modulemap 文件通过目录的形式放到一起
    3. OC 要用合并的静态库:clang: other c flags :-fmodule-map-file
    4. Swift 要用合并的静态库 : SwiftC :other swift flags 显式告诉 SwiftC
  6. OC 映射到 Swift 方式
    1. <工程名称>.apinotes
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值