iOS-开发进阶07:Module与Swift库

iOS 开发进阶 文章汇总

目录


一、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 <modulemap path>
    4. Swift要用合并的静态库 : SwiftC :other swift flags 显式告诉SwiftC <modulemap dir>
  6. OC映射到Swift方式
    1. <工程名称>.apinotes
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容