14、iOS强化 --- Module与Swift库(OC方法转Swift)

Module(模块)

  • Module(模块) : 最小的代码单元。
    一个Module是机器代码和数据的最小单位,可以独立于其他代码单位进行链接。通常,Module是通过编译单个源文件生成的目标文件。
    例如:当前的test.m被编译成目标文件test.o,当前的目标文件就代表一个Module。但是有一个问题,Module在调用的时候会产生开销,比如我们在使用一个静态库的时候可以这样使用:
@import TestStaticFramework;

这个静态库中可能包含了许多的.o文件。岂不是要导入很多的Module
并不需要,在静态链接的时候,也就是静态库链接到主项目或者动态库,最后生成可执行文件或者动态库时,静态链接器可以把多个Module链接优化成一个,来减少原本多个Module直接调用的问题。

  • module.modulemap 用来描述头文件module之间的映射关系。
    下面使我们经常用到的AFNetworking产生的.modulemap文件:
    image.png

    那么.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 * }
}

image.png

如果要显示指明子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
    image.png
  • Module到底有什么用呢?
    在我们传统的#include引入头文件,并且没有开启Module的情况下。头文件被引入多少次就要被编译所少次。比如:
    A.h,此时被use.cuse_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:

image.png

可以看到生成了两个二级制文件,这就是我们头文件预先编译的产物:

image.png


Swift库

  • 我们都知道,在平常的开发中,swiftOC代码的混编,我们都是使用桥接文件(Bridging-header)
    但是在Framework中,是不允许使用桥接文件的。因此:我们不能使用桥接文件的方式进行混编 Objective-C 代码的引用,需要用 Swift Module 进行模块间的引用。
    i :首先创建自己的.modulemap文件:

    image.png

    ii :在FrameworkBuild Settings 里面配置Module Map File:
    image.png

    iii :此时已经可以在swift文件中使用OC的代码了(同时OC文件中使用swift代码也是一样的):
    image.png

  • 这样就引出了另外一个问题:如果我不想将YSStudent暴露到Framework之外怎么办?
    这个时候我们就可以引入.private.modulemap文件

framework module YSSwiftFramework_Private {
    module YSOCStudent {
        header "YSOCStudent.h"
        export *
    }
}
  • 注意:①_Private首字母大写,② 文件名中要有.private
    然后配置一下Build Settings
    image.png
  • 注意:这里虽然配置了.private.modulemap文件,但并不是真正的隐藏。我们通过模块访问,依然是可以访问到的:
@import YSSwiftFramework_Private.YSOCStudent;

这样做虽然做不到完全的隐藏,但是可以达到提醒使用者的作用,告诉用户这是一个私有库,不要随便使用。

这里还有另外一种方法可以达到上面的要求:swiftOC遵守同一个协议,通过协议来调用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,名字和函数一模一样(埋点,看看后面会不会报错):
    image.png
  • 我们将生成的库文件拷贝出来,做一下合并:
libtool -static SwiftA SwiftB -o libSwiftC.a

此时会有一个警告,提示我们合并的两个库文件中有相同的文件。

image.png

这就是我们使用libtool的好处,我们之前讲过可以使用ar来合并静态库,但是使用ar的情况下,先解压再合并,可能发生文件的替换。
我们来查看下当前静态库里面有哪些.o文件:
image.png

可以看到,两个库里面的同名.o文件都在,并没有产生替换。

  • 2、接下来我们将.framework里面原先的签名文件配置文件删除,只留下HeadersModules。因为我们需要的是合并后的静态库,所有原先的签名和配置没有用了。
    image.png
  • 3、将Modules里面的文件,提到和``Headers一个等级,这样做是为了后续SwiftC编译器能够找到对应的Module文件。要不然可能会找不到。
    image.png
  • 4、将我们合并后的静态库libSwiftC.a拖到工程里面,并且配置xcconfig文件
    image.png
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代码用。

image.png

  • 5、编译运行我们发现两点不同:
    iOC文件中同时使用AB没有问题:
    image.png

    iiSwift文件中不允许这样使用:
    image.png

这也与两门语言的特性有关系,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

image.png

这个时候我们就可以结合脚本批量修改。
另外还有很多的用法,可以参考:API Notes

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

推荐阅读更多精彩内容