混编framework实践

开篇

在开发中,由于某些原因不开源代码,我们常常将其制作成framework来隐藏源代码。 除此之外我们还会遇到OC、Swift混编的情况,又或者是遇到一些很老,non-modular(非module)化的库,在这种情况下如何保证我们顺利产出framework并能够被工程无障碍的使用呢?
文章分为2个大篇幅, 方法篇和原理篇。
方法篇:
主要讲述一个极端的案例,此Framework既是OC和Swift混编, 通过Cocoapods依赖很多第三方库。且部分第三方库并没有开源只是一个.a文件或者framework文件,而且这些库是几年前的老库,并没有(modularized)模块化. 我需要编译出一个可以同时被OC和Swift工程使用的模块化的framework。
原理篇:
主要讲述什么是module模块化,什么是module map,以及framework混编的原理。

原理篇:

一.module的由来。

我们先引用下 Apple公司对 Mail App的一个分析。 横轴是文件 纵轴是文件大小。 初一看文件都不大,多数都是几十K。


5A139281-F1D6-4894-AAF4-387A443B4339.png

假如我们的项目中需要一些核心文件,就是几乎所有的文件都会依赖的那种,那么,我们最常用的就是通过 import 文件.h的方式,将头文件写入。
比如你的核心文件A.h 依赖了UIKit这个基础UI库的头文件,那么你的所有文件都会写入这些头文件.


79054EAF-9BEB-457D-96FD-AB62E88F9287.png

哪怕你的文件很小,但也得先覆盖这几百K的头文件。所以编译时间肯定会加长,显然不是最好的解决方案。

PCH的到来

如何优化它呢,我们可以想到,如果让这组公共依赖只编译一次呢,其他文件来共享它不就解决了? 对! 这就是PCH(PreCompiled Header)苹果预编译文件的由来。 大体原理就是我们在编译.m的时候,xcode会先编译pch里面的内容,当开始编译.m的时,如果需要PCH中的内容直接获取即可,无需再次编译。

那么苹果后来为什么通不建议使用了呢? 因为PCH也有不方便的一面。

https://qualitycoding.org/precompiled-header/ 感兴趣的同学可以看看这篇文章,核心就是这几点。

1. Source files can’t be copied to different projects
Say you’ve added to your prefix header. A particular source file uses QuartzCore. Try copying that source to a different project.
Chances are good that it won’t compile, because the other project has a different set of precompiled headers. You’ve managed to create a nonportable source file!
2. Dependencies are hidden
One of the benefits of any system of importing other files is that it reveals the file’s dependencies. You can scan the beginning of a .h or a .m file and see what other files it uses. This gives you a quick sense of its scope.
Not so if your imports are implicitly bound up in the prefix header.
3. Dependencies are buried
A large project may have a large number of precompiled headers. Say you’re looking at a source file, and trying to find its dependencies. You’re clever enough to realize that earlier programmers relied on precompiled headers to save typing, omitting many #imports. So you look at the prefix file as well.
But if Prefix.pch has more than a handful of #imports, which ones does your source file need? All of them? None of them? Some of them? Which ones?
4. Dependencies get out of hand
Even if you make all #imports explicit, it’s easy to create an explosion of file dependencies. Keeping the dependency tree tamed is hard enough.
But if no effort has gone into a) making all #imports explicit, and b) taming them, these dependencies can silently grow out of hand. Dependency rot can spread unnoticed, for years—until it’s too late. Suddenly you’re working on a new project and have no clean way of reusing earlier code, without bringing it all in as a massive, wasteful glob of cruft.

总结为:
代码无法直接复制粘贴在别的工程中,因为没有相同的PCH文件,可能引发找不到依赖的文件。
依赖的头文件被隐藏了或者传递依赖很深,不直观。
如果没有有效的管理,依赖会渐渐的膨胀,最终越来越大。

Clang Module

于是 Clang提出了 Module的概念。
官网的解释如下:
Modules provide an alternative, simpler way to use software libraries that provides better compile-time scalability and eliminates many of the problems inherent to using the C preprocessor to access the API of a library.
总结为:
Modules提供了一种更简洁使用library的方案,提供了更优化的编译耗时和可扩展性,并且还消除了C预编译处理器固有的许多问题。

Clang module主要围绕着 C 预编译处理器所存在的问题展开了优化工作

问题1:Compile-time scalability 编译耗时

这个就是上述mail app所面临的header include大量代码的问题,尤其是C++的话尤其显著,因为模板编译模型会强制在header里面加入大量代码。

问题2:Fragility 脆弱性

#include 和 #import的代码都被预编译器视作文本写入的,因此很容易导致宏定义的冲突。任何一个library的宏定义冲突都会导致别的library的API定义被破坏二编译失败。
例如:当你在某个framework中定义如下宏,那么后果就是导致所有的继承自UIViewController的类无法有效编译。

#define UIViewController @" "

又或者

#define std "The C++ Standard"

那么所有的C++标准库都无法实现了。 而且在真实场景还会有很多细微的东西会导致两个library的header冲突。开发者为了安全起见,不得不通过改变 #include的顺序,或者使用#undef名来编译这种不可控的问题。

问题3:Conventional workarounds 依靠传统变通写法

C系列开发者不得不掌握很多约定俗成的写法来绕开C预编译器的脆弱性。比如include guards,大多数header都需要include保护来避免重复冲突。宏定义的时候常常通过LONG_PREFIXED_UPPERCASE_IDENTIFIERS 这种长名字,加后缀再大写字母的方式来避免冲突,甚至有些时候使用下划线来避免和传统宏定义冲突 - - 传统宏定义不应该有下划线。这些约定俗成的东西也给不了解C的开发者造成了巨大的困难。

问题4:Tool confusion 工具的困惑

在以C为基础的语言中,很难有完美的library工具。因为很难定义这个library的语言边界点。比如这个header到底属于哪个库?到底哪种header的引用顺序才能确保无误?这个header到底是由C++,Objective-C++,还是混合写的?哪些申明的的确确是当前API的,哪些申请又是不得不在这个header中写的?

Module的解决方案

官方解释:
However, this module import behaves quite differently from the corresponding #include : when the compiler sees the module import above, it loads a binary representation of the std.io module and makes its API available to the application directly. Preprocessor definitions that precede the import declaration have no impact on the API provided by std.io, because the module itself was compiled as a separate, standalone module.

总结为:
大致就是,将以前那种通过文本引入的方式转换为了一种二进制引入方式。 编译器会给libary创建一个独立的Module产物,你现在引用的是一个可以直接运行的被编译过的module。它既可以重复引用,也提高了健壮性和拓展性。

1.关于Compile-time scalability:

每个模块仅编译一次,并且将它引入到一个translation unit(转义单元)的时间是个恒定的时间操作(独立于模块系统).每个library的API只需要解析一次,这样就将模块引用从 M x N变成了 M + N。

2.关于Fragility:

每个模块都是一个独立的实体,因此拥有恒定不变的预编译环境。这样完全避免了需要通过 "__"下滑线之类的小手段来避免冲突。而且即便是当预编译在处理一个library的时候遇到了某个library中的定义被忽略了,也不会影响到其他library的编译。

3.关于Tool confusion:

因为每个module都是独立编译的,因此libary工具可以通过当前module的定义获得完整的API定义,这个module也能够指明它到底是哪种语言编译的。 比如:它不会突然从C++模块一下了变成了C模块。

Module目前办不到的事儿

官方解释:
Many programming languages have a module or package system, and because of the variety of features provided by these languages it is important to define what modules do not do. In particular, all of the following are considered out-of-scope for modules:

总结为:
很多语言都有模块和包系统的概念,并且因为这些语言提供了各种各样的功能,那么决定module哪些事儿该做那些事儿不该做是一件很重要的事儿。 如下几点列为 out-of-scope ,超出了module范围。

1.Rewrite the world’s code (重写世界代码)

关于继续用header的概念,是因为要让library或者应用向下兼容,Module必须要和现存的libray交互然后一点点改变。

2.versioning (版本)

module没有加入版本的概念,开发者还是需要依赖现在的版本机制。

3.Namespaces (命名空间)

和别的语言不一样,module没有加入命名空间的概念,因此如果你在两个module中定义了同名的struct一样会发生冲突,就像在两个header中定义的一样。这也是为了考虑向下兼容的问题,因为在已经引用的module中是无法再次改名的。

4.Binary distribution of modules (module的二进制分发)

Header,尤其是C++的header是非常复杂的。因此维护一个稳定的二进制版本module横跨所有的架构版本和不同的编译器并不是一件现实的事情。因此相对于文本转义,二进制的module在系统升级或者跨平台的时候需要考虑的兼容性问题。

二.Module Map

有了module之后,又是什么方式让header有效的连接的呢?就是接下来这个 module.modulemap文件。

来看下官网的定义:
The crucial link between modules and headers is described by a module map, which describes how a collection of existing headers maps on to the (logical) structure of a module. For example, one could imagine a module std covering the C standard library. Each of the C standard library headers (, , , etc.) would contribute to the std module, by placing their respective APIs into the corresponding submodule (std.io, std.lib, std.math, etc.). Having a list of the headers that are part of the std module allows the compiler to build the std module as a standalone entity, and having the mapping from header names to (sub)modules allows the automatic translation of #include directives to module imports.

大致意思如下:
它是一个链接module和header的关键,它描述了头文件是怎么映射到module的(逻辑结构上)。比如 一个module std包含了几个C保准库,这些标准库,,等都会成为std的响应子模块(std.io, std.lib, std.math). 拥有这组子module后,编译器就可以独立的将 std编译成一个单独的实体,并且因为拥有从header名到子module的映射,可以直接将#include转换成导入成对应的module。

module.modulemap 的由来

官方解释:
Module maps are specified as separate files (each named module.modulemap) alongside the headers they describe, which allows them to be added to existing software libraries without having to change the library headers themselves (in most cases).

大致意思:
module.modulemap 其实就是module map的具体体现。它会伴随在header文件旁,这样就可以让现存的library文件也可以不用做任何修改就实现模块化了。
为了直观下面我新建一个framework,然后打开编译好后的库文件


CB8CB18F-CD9E-4515-BF98-51FB253F35B1.png

B1DF967C-EF48-4600-9D23-63CF15F38044.png

这就是伴随着Headers而生成的.modulemap 文件.Framework现在自动就能生成,有了他就能让Clang开始模块化。
为什么几句描述就让他生效了呢?接下来我们看看clang的源码。
clang中有一个叫ModuleMap的类


23E24C84-5A71-4621-BB2A-C8463C01B74F.png

那么它是干什么的呢,我们来看头描述
76D1E2EC-2045-4766-B29C-EB461FA3BCD6.png

"这个文件是ModuleMap接口文件,描述了相关header的呈现。"
接下来再看它的初始化方法
95BD9895-9A49-4E7A-92E9-AE92EE73FFFC.png

第一个参数 SourceMgr是一个资源管理类它可以找到我们需要的headers 想module files。
接下来再看findOrCreateMdoule函数。


96982338-2481-4EA4-969F-F223FA6A11CA.png

参数
name就是module的名字,
parent:表明这个module到底是父module还是子module
isFramework:是否是framework的module
isExplicit:是否是显示的module.
这个函数会先看缓存里是否已经加载了,有就直接加载,没有就直接Module。
然后我们再进入Module这个类看看
这里有一个很重要的结构图就是Header. 看注释表明,在module map中直接找到的关于header的信息。
E66A3662-5EEE-4319-8885-59E6E41ECE3A.png

Header由 string表明了它的名字, FileEntry则是具体的文件实体。

Module 对宏定义冲突的解决

相对于C和C++预编译处理器对于所有的输入文本都当作是一个单线性缓冲区,module则是视作单独的实体,因此让冲突的宏可以共存,不管是否是名字冲突了,还是一个宏定义了#define另一个宏又定义了#undefines。
module对于宏的原则:
每个宏的定义都是一个不同的实体。
每个宏仅对当前编译单元,子module,和引用它的区域可见。
宏对于引用它的单元是可见的,如果被覆盖了,则它变成不可见的。
如果一组宏指令仅包含#undef,那么他是不变的。或者#define指令定义为了同样的标记序列,它也是不变的。
如果使用的宏名称和指令集不一样,将被视作错误格式,否则将使用宏的唯一含义。
下面来看一个直观的例子:

BA8EC0AB-6B28-414A-BBB5-8AC807CC97EA.png

#define AA @"123"

在FrameworkTest.h中我定义了一个宏,然后在Person类的sayHello函数中打印。


DF2DCE5A-B339-48FB-9262-21C48ECBB158.png

新建一个工程引入这个FrameworkTest,然后又定义了一次宏AA,然后调用Person的sayHello后结果如下。


DD73C778-D426-4014-A1C8-15F842AF5FCA.png

三.Module Map语言

接下来我们看看module map的语法

官方解释
The module map language describes the mapping from header files to the logical structure of modules.

总结为:module map语言用于描述header的映射和逻辑结构.

module的常用方式

1.最简洁的Module

module OCFile {
    header "Person.h"
 }

这个定义告诉了Clang,我有一个Module名字叫OCFile,里面有一个Person头文件. export *表示当前所有的header文件都要引入到module。
那么我们在使用的时候只需要通过导入OCFile这个module 就可以使用了。

@import OCFile
{
    Person.init()
}
{
    [[Person alloc]init];
}

无论是OC还是Swift都可以使用。
2.多个子module

AE67C280-E589-4B44-A28B-239FFEF7D90A.png

定义2个子module A,B, 分别指向类 AClass, BClass.
注意 这个的 A多了一个修饰 explicit. 表示这个moduleA需要显示的调用才能使用.
B0105D88-BE59-4C89-A331-5C71722C3384.png

当我们直接@import OCFile 用AClass的时候提示 cannot find AClass' in scope. B可以直接使用。
8C20C7EF-A2FC-4332-B0D9-48D818B03711.png

所以想要使用子module A,我们必须要显示的指明 import OCFile.A 才能使用。
3.umrella header

官方解释:
A header with the umbrella specifier is called an umbrella header. An umbrella header includes all of the headers within its directory (and any subdirectories), and is typically used (in the #include world) to easily access the full API provided by a particular library.

总结为:如果我们有很多类要暴露的时候,与其写很多header 'name.h',不如集中在一个umbrella header里面引用.


A5B09082-E910-46E5-8CBF-D408F31651E2.png

如果使用了 umbrella header。 module * {export *}是固定搭配。
然后在umbrella header (Header.h) 里面统一引入 AClass BClass.


1220CF1E-6101-4D28-9FED-702524A3CAC9.png

对于经常使用第三方库的同学来说,这种方式肯定很熟悉。

其他关键字

除此之外还有一些别的关键字可能会用到。
config_macros, export_as, private conflict, framework, requiresexclude, header, textual explicit,link,umbrella, extern, module, use export
1.link framework "MyFramework"
指明依赖的别的framework. 但需要注意的是,这个即便你写了,目前也不会自动帮你引入,类似于注释的效果。 具体有点像 Microsoft Visaul Studio 的 #pragma comment(lib ...)
2.framework module COFile
表明这个module符合于 Darwin-style(达尔文风格) framework,目前达尔文风格framework只用于 macOS 和 iOS. 有兴趣的同学可以查阅下。
3.module OCFile [system]
指明这个是一个系统module,当clang编译的时候,会考虑到它使用了系统的header,因此而忽略到警告。 方法类似的 #pragma GCC system_header. 因为系统header往往不能完全遵循C语法,所有头文件中警告信息往往不显示,除非#warning显示.
4.module OCFile [system] [extern_c]
表明module中的C代码可以被C++使用
5.private 模块

7FA4294D-084E-4EC9-9FD0-399ED495E0DC.png

在某些时候,可能会遇到某些类在特定版本,或者特定的library才会使用。 所以clang准备了一个module.private.modulemap 来为我们放置这些类。
给你的private module命名为 "module名"_Private
这也是官方推荐的命名写法

Clang has extra logic to work with this naming, submodule trigger warnings and might not work as expected.

官方对这类文字有特殊的逻辑处理,不建议用别的方式或子module的形式。


E746DDBF-E90A-4612-8732-883F3D92AD86.png

具体可以看下clang 源码 ModuleMap.cpp的这里。


C66BF4AF-FFAB-49B8-B143-B340D71487C1.png

指明当我在用module C的时候, C指明我需要用到B。 注意 这个只对第一层module有效。

三.Framework和modulemap

在平时混编swift 和 OC的时候,我们常用的做法就是让系统自动给我们生成一个桥接文件 .


1ED5F169-B3BF-42B1-B695-C432ACBFC18E.png

分别是 "工程名-Briding-Header.h" 导入C家族类给swift。 "工程-Swift.h" 告诉OC swift相关的头文件。
我们打开xcode的配置文件project.pbxproj 看看到底做了什么。


17D0B4DC-0C02-40A1-B3C5-E60C27F449E0.png

FCB312E2-63F0-431B-A3EA-61C67A3A6FC7.png

其实通过字面意思就可以推断出,工程启用了Clang_module. 并且创建了一个 MyDemoTest-Briding-Header.h的桥接文件。

Framework混编

然而开发过framework的同学会发现,当我们在framework工程中,系统并不会帮我们创建桥接文件。那么,我们如果混编呢?

方法一:利用Framework自带的头文件。

9CF93E0D-D09D-41C8-86F4-2B6911D0E7E2.png

新建一个Framework文件,分别加入OC和Swift类然后编译,打开对应的framework,看看里面有啥。
918DF793-4772-46CF-B833-66291E8A1A89.png

是不是有一种熟悉的味道?没错,工程自动帮我们把这个Framework创建一个module。 而且这个自动生成的头文件 MyFramework.h也正好是我们的 umbrella header.
注意:这里是因为默认的framework配置 Build Settings -> Packaging ->Defines Module = YES, 所以系统会自动生成modulemap.
umbrella header干嘛的?不就是桥接所有的C家族类吗,那么只需要将Person.h这个类引入到 MyFramework.h中,就可以了。
92E1A641-FBBE-4675-80E7-8C6C64A02DB9.png

注意看,这个报错了。Include of non-modular header inside framework module
为什么呢?想想umbrella header在这里是干嘛的?让别的工程使用你的framework的时候,可以@import这个framework后调用这个Person类的。那么你的Person肯定要Public的才能找到。
而且注释也已经说明了 In this header, you should import all the public headers of your framework 。
"你应该在这里加入你framework中的public header."
3DCA2A98-6A81-46C5-BDF3-8AFF4BDA3BF8.png

所以我们要把Person放在 public里就可以了。
AE704D83-E06A-4B8F-AE58-F71A4554BDCE.png

Swift类调用正常。
OC调用Swift.h
回到刚刚我们打开的Framework文件里面,成了modulemap之外,还有一个Myframework.Swfit.h. 这就是给OC调用Swift的头文件。
DA231811-9799-46DC-A547-B3DF9498332B.png

那么我们只需要#import 就可以在OC里使用swift的类。 当然swift不要忘记通过 @objcMember 关键字声明。
这么做虽然可以实现桥接功能,但是存在一个弊端,那就是如果你的某个OC不想要暴露怎么办?也就是说不想指定为public,我们来看方法二。

方法二:Framework自建module

1.新建一个文件夹OCFile. 加入文件 module.modulemap 和 头文件 OCFile.h。

21DB8B8C-21DA-4D56-A723-5995AD6529B7.png

在module.modulemap中指定OCFile.h为umbrella header.
在OCFile.h中引入 Person.h
8E3FD037-BC01-4D7E-BAD6-04611D64CF1C.png

注意:这里名字必须是 module.modulemap,我们看下clang的官方解释

-fimplicit-module-maps
Enable implicit search for module map files named module.modulemap and similar. This option is implied by -fmodules. If this is disabled with -fno-implicit-module-maps, module map files will only be loaded if they are explicitly specified via -fmodule-map-file or transitively used by another module map file.

总结为:clang会隐式的查找叫 module.modulemap的文件。
2.指定modulemap文件的位置

FD95462B-E6D9-492D-829A-3354444EDEA4.png

在Build Settings -> Swift Compiler - Search Paths -> Import Paths中指明需要查找的modulemap文件夹。
3.引入自定义的module使用
EAB50F68-A0F3-494B-9264-7B5475F50A82.png

在swift中 直接import OCFile, 就可以直接使用了。
OC引用swift的方式和上面一样。
再次打开Framework 可见,我们既使用了OC类,也保证了它没有暴露在public里面。
41BE0522-8AEA-4977-BCBC-A5DCE86994C0.png

方法篇

如果你对clang的module已经有了解,可以直接看方法篇。此篇幅的所有知识点依托于原理篇。
我引入了两个库。最新的AFNetworking,和一个老版本的SDWebImage库。 它俩最大的区别如图可见,一个是模块化的可以通过 import 导入模块给swift使用,一个是非模块化的swift无法使用,因为clang的module概念是后来引入的,前期编译的库自然没有。(关于模块化,可参见原理篇:module的由来)


D53443F1-2B92-4518-893D-8A0489E20A24.png

那么我如何才能既使用这个老版本库又编译出一个符合module化规则的库呢?

原则:

核心就是一切遵循模块化原则。遵循模块化原则的代码可以对外暴露,而不遵循模块化原则的代码对内自己使用。 也就是说,模块化的framework想要继续模块化,那么所有暴露的代码和公共依赖依旧要满足模块化原则。

实践:

要知道,如果是工程,可以直接建立一个birdge的桥接文件,就可以完成混编了,在framework制作中并没有这组文件。(原理篇: Framework和modulemap)
Framework自带header法
通常情况下,很多人会默认想到通过framework自带的header当作bridge文件,桥接OC和Swift达到混编的目的.(原理篇: 方法一:利用Framework自带的头文件)


8F0C621E-06C2-4FF4-820F-155EF737D34F.png

可以发现,原本支持modularity的AFNetworking 可以正常引用,而不支持的SDWebImage却报错了。Include of non-modular header inside framework 这个错误就是典型的打破modularity(模块化)的错误。
我很来看看根源:

官网对umbrella的解释
A header with the umbrella specifier is called an umbrella header. An umbrella header includes all of the headers within its directory (and any subdirectories), and is typically used (in the #include world) to easily access the full API provided by a particular library.

意思就是,umbrella header 典型是用于帮库文件表明它包含了哪些header的。
再看看这段讨论:

I think the majority of people running into this issues is caused after they switch from Application Target to Framework Target and start adding C and Objective C headers into framework's umbrella header expecting it to have a same behaviour as application's Bridging Header, which behaves differently. The umbrella header is actually designated for mixed swift, obj-c framework and its purpose is exposing the APIs to the outer world that your framework has in objective-c or c.

来源: https://stackoverflow.com/questions/24103169/swift-compiler-error-non-modular-header-inside-framework-module
也就是说,大多数情况下,都是我们切换到了framework工程后,利用这个umbrella header作为桥接文件,又期待着它和app工程拥有一样的功能。可是framework中 umbrella header设计的目的是为了让外部知道你的framework中拥有的OC 或者 C文件。
我们打开编译后的Framework看看我们干了什么:

76E251A7-E537-490F-8743-CAA848AC54F3.png

6B007649-2C68-4625-A3B9-1AEA287A02C3.png

Framework在模块化的过程中向外界暴露了我有两个依赖的头文件信息。分别来自于AFNetworking和SDWebimage. 当发现暴露的SDWebimage是非模块化的,就出错了。

正确引入步骤

接下来,我们可以通过在framework内部建立一个私有的modular,对内达到桥接的目的,对外并不暴露。
1.新建内部module 桥接SDWebimage(原理篇:Framework自建module)

1B0A5A9D-6AD4-4344-8AD2-359A0EFD9F67.png

将要使用的SDImageCache通过 umbrella header引入
2.swift 使用
577BECAA-5B52-40FC-8600-D9FEC38CF99F.png

对外暴露的OC类如何使用依赖non-modular文件

接下里,我新建一个OCClass类,并将它作为public的类,对外使用,通过自带的header MyFrmework.h 引入。(如何制作参见原理篇:Framework混编)

6FE4199C-C4D6-4A53-AA51-464DD5FFE1CC.png

编译一切正常,然后我们将OCClass中定义一个属性,类型是SDImageCache
7B3CA246-F68B-446E-AA92-AEA46341F748.png

又出现 non-modular header inside framework 了, 结合我们上面说的原理,OCClass引用了 SDImageCache, 那么在这个在umbrella的import树中一步步是会找到SDImageCache.h的,然后发现它打破了modularity。
所以我们有两种做法:
方法1:将属性和引入写在 .m中
CCClass.m
EA26191A-855D-4B0B-9EE1-97A3078BD320.png

这样就没有打破modularity原则,因为umbrella树中不会去找SDImageCache.h。
方法2:在内部模块OCFile的umbrella header中隐藏OCClass类
不再把 OCClass写入Framework自带的umbrella header(MyFrameworkA.h),这样你可以放心的把属性写在 .h中, 如果swift要使用它,使用我们上面提到的内部modulemap方法。
ED88D42F-CA35-4B67-9AA2-7F0629EE83C1.png

既要对外暴露公共类, 有想要在外部声明non-modular的类作为属性怎么办?

既:我想要将一个类作为public的形式暴露,又想要暴露的同时引入non-modular的类将其作为属性。
有两种可行性
方法一:对外暴露Swift作为公共类, non-modular的OC类作为属性

631D1366-700A-4CDA-B82E-20F4598A12CF.png

新建public类 SwiftClass,使用@objcMembers桥接OC。 然后声明一个 non-modular的类作为公共属性。编译。
打开FrameworkA工程 -> Headers -> MyFrameworkA-Swift.h 这个是对外混编时,转义的swift 文件。 (原理篇:Framework混编)
09F527F3-62E8-41D3-8678-6B69F0B97C4C.png

之所以这样可以正常使用,使用为编译器在转义的时候没有 import它 而是通过@class声明了一下这里有一个类。并不会想import一样去引入对应的方法和变量。所以它并没破坏modularity。
方法二:如果是OC作为public类,可通过桥接模式,定义一个新类在 .m包裹住 non-modular类的所有功能,实现相同功能。
新建立一个Class,将non-modular包裹在这个class的.m中,实现一样的功能。
B6890F3D-BF7B-43D4-AB1B-35ED909A5CFA.png

例如:Person类有一个功能sayHello(). 新建一个类Myperson,在sayHello()中调用Person.sayHello().

class Person {
    static func sayHello() {
        print("hello")
    }
}

class MyPerson {
    static func sayHello() {
        Person.sayHello()
    }
}

将新的class作为属性对外暴露,而non-modular包裹在.m中。 方法类似我们的上文的方法1:将属性在 .m中

如果有继承自non-modular类的情况

我们将SwiftClass继承自 SDImageCache,然后编译通过。

A8514A73-40AF-4F26-AD27-AE697D7E115D.png

然后将framework加入应用工程种,发现报错。 Module 'OCFile' not found. 我们打开framework中的 headers -> MyFrameworkA-Swift.h桥接文件,看看多了什么变化.
834EA21D-F0D5-446B-A9CA-DD254DBB4F7B.png

这里多了引入了一个OCFIle包,然而OCFile是不对外暴露的。 SwiftClass既然继承了 这个类,那么它一定要读里面的方法和变量信息,所以肯定要引用。所以我们的正确做法 和上面类似
方法一:
SwiftClass不在暴露成Public,这样MyFrameworkA-Swift.h自然不会去引用
方法二:
继承的类依然通过桥接模式包裹一层后再继承。
灵活运用以上方法,对应各种混编中的情况。
参考:
https://clang.llvm.org/docs/Modules.html#compilation-model
http://faculty.sist.shanghaitech.edu.cn/faculty/songfu/course/spring2018/CS131/llvm.pdf
https://clang.llvm.org/doxygen/files.html
https://stackoverflow.com/questions/24103169/swift-compiler-error-non-modular-header-inside-framework-module
https://tech.meituan.com/2021/02/25/swift-objective-c.html

对以上感兴趣或有疑问的朋友可以加QQ小群 839813029 探讨。

你可能感兴趣的:(混编framework实践)