Objective-C Swift 混编的模块二进制化 1:基础知识

6,648 阅读6分钟

Objective-C 与 Swift 混编

Objective-C 与 Swift 混编在使用上主要依赖两个头文件:ProjectName-Bridging-Header.h 和 ProjectName-Swift.h。

对于 Swift 调用 Objective-C,在 ProjectName-Bridging-Header.h 中 import 要使用的 Objective-C 头文件。

对于 Objective-C 调用 Swift,需要编译过程中生成的 ProjectName-Swift.h 文件,此文件会将 Objective-C 需要使用的 Swift 类转成 Objective-C 格式的 .h 文件。下面就是一段例子:

Build Pipeline

当 Objective-C 与 Swift 进行混编时,编译的过程(Pipeline)是:

  • 首先编译 Swift Module。预编译 Bridging Header 后,再编译 Swift 源文件。
  • Swift 编译完成后,生成 ProjectName-Swift.h 的头文件供 Objective-C 使用。
  • 最后编译 Objective-C 源文件。

Circle Reference

在开发过程中,有时候会遇到 ’ProjectName-Swift.h’ file not found 错误,而 ProjectName-Swift.h 又是在编译时动态生成的,当出现这样的错误时就比较迷惑。下面就分析一下出现该错误的原因。

@objc (TTLesson)
class lesson: NSObject {
}

假设有一个 Swift 类 Lesson,Objective-C 的 TTLessonViewController 中使用了该类,因此需要引入 MixedProj-Swift.h 头文件。

// TTLessonViewController.h
#import "MixedProj-Swift.h"

@interface TTLessonViewController: UIViewController
@property (nonatomic, strong) TTLesson *lesson;
@end

如果 TTLessonViewController 又要在 Swift 中使用,需要将 TTLessonViewController.h 加入到 MixedProj-Bridging-Header.h 中

// MixedProj-Bridging-Header.h
#import "TTLessonViewController.h"

此时编译就会出现 MixedProj-Swift.h file not found 的错误。重新回顾一下混编时的 Pipeline:

  • 首先编译 Swift,需要先处理 MixedProj-Bridging-Header.h。
  • 在处理 MixedProj-Bridging-Header.h 时,里面的 TTLessonViewController.h 中引用了 MixedProj-Swift.h 头文件。
  • 此时由于 Swift 还没编译,因此 MixedProj-Swift.h 头文件并没有生成,所以出现 MixedProj-Swift.h 找不到的错误。

解决办法就是不显式的 import 头文件,而是使用前置声明(Forward Declaration),打破 Circle Reference。

// TTLessonViewController.h
@class TTLesson;

@interface TTLessonViewController: UIViewController
@property (nonatomic, strong) TTLesson *lesson;
@end

语言混编的一些思考

笔者在了解 Objective-C 与 Swift 混编时一直在思考语音为什么能混编,语言到底是如何混编的?笔者了解的也不多,这里只是谈谈自己的一些思考,抛砖引玉。

对于语言混编,不妨尝试着回归到程序本质的角度进行看待:

程序 = 数据结构 + 算法 / 数据 + 逻辑

在机器码层面,所有编程语言都转化成了机器指令:

  • 数据:内存访问,一个基地址 Base + 偏移 Offset,读取 Size 长度的内存。
  • 逻辑:用符号标记的一段机器码,跳转到符号标记的地址。

那语言想要混编,就需要在数据和逻辑之间建立连接

  • 数据:要么内存布局相同,可以直接用;要么互相了解转换规则能够进行转换;要么使用通用格式进行通信。
  • 逻辑:符号能够匹配上或者互相识别,方法调用的 Call Convenience 是相同的,能够互相跳转。

最后发散一下,既然是建立连接,那么通过 TCP/HTTP/IPC 等方式建立的通信可以看成更广义上的混编。例如客户端以 REST API 的方式向服务器端请求数据也可以看成客户端语言 Objective-C/Swift/Kotlin/Java 与服务器语言 Java/Go/Python 的混编,只是两端之间使用 URL(逻辑)+ JSON(数据)这种通用格式/协议的方式建立起了连接。

Clang Module

Module 是 WWDC 2013 就引进的技术,在 Xcode 的 Build Setting 中能看到 Enable Modules (C and Objective-C) 的设置项,在 OTHER_CFLAGS 中也看到了 -fmodule-map-file=“xxx.modulemap”,但是一直对其不太了解,一项技术的出现总是为了解决某些痛点,那 Module 是为了处理哪些问题呢?

Module 解决什么问题?

以前在 C/C++/Objective-C 中,源文件中引入的头文件在编译时需要进行展开,预处理宏等相关操作。这样的方式存在以下几种问题:

Header Fragile

由于头文件是一起展开后再统一处理宏,当宏重名时,头文件引入的顺序就会导致不一样的结果,并且宏只是简单粗暴的文本替换,也存在宏污染的可能。例如,AppDelegate.m 中定义了一个 readonly 的宏,与 Objective-C 中属性的关键字冲突了。

// iAd/ADBannerView.h
@interface ADBannerView : UIView
@property (nonatomic, readonly) ADAdtype type;
@end

// AppDelegate.m
#define readonly 0x01
#import <iAd/iAd.h>

@implementation AppDelegate
// ....
@end

由于是文本替换,预处理完之后,ADBannerView 的 type 属性中,readonly 被替换成了 0x01,导致编译错误,并且很难定位到问题根源。

// AppDelegate.m
#define readonly 0x01
@interface ADBannerView : UIView
@property (nonatomic, 0x01) ADAdtype type;
@end

@implementation AppDelegate
// ....
@end

Compile Time

由于每个源文件中的头文件都需要展开、预处理,假设有 M 个源文件,N 个头文件,需要 M * N 的编译时间。

为了优化这个问题,Objective-C 引入了预编译头文件(Pre-Compiled Header),将公用的头文件放入预编译头文件中预先进行编译,然后在真正编译工程时再将预先编译好的产物加入到所有待编译的 Source 中去,来加快编译速度。

但是 PCH 文件会导致里面的头文件是全局可见的,相当于变成了全局依赖,可能会导致 Namespace Pollution。并且当 PCH 文件变成庞大时,还是会导致预编译头文件的时间变长。

Link & Use

在使用框架时,需要 import 正确的头文件,不然可能导致编译问题。另外,仅仅 import 头文件是不够的,需要在设置中链接对应的库,不是很方便。

Module 是什么?

framework module UIKit {
	umbrella header "UIKit.h"
	module * {export *}
	link framework "UIKit"
}

Modules 相当于将框架进行了封装,看一下上面 UIKit 的 modulemap 文件,Module 在 modulemap 中定义了框架的三大核心要素:

  • umbrella header 描述主要头文件:UIKit.h
  • module 描述需要导出的子 Module:全部导出
  • link 描述需要链接的 Framework:UIKit

在实际编译时,加入到一个用来存放已编译添加过的 Modules 列表中。如果在编译的文件引用到某个 Modules 的话,将首先在这个列表内查找,找到的话说明已经被加载过则直接使用已有的,如果没有找到,则把引用的头文件编译后加入到这个表中。

所谓 umbrella header,就是 includes all of the headers in its directory,一个包含框架内所有需要开放头文件的 Wrapper 头文件。为什么叫”雨伞“呢?笔者猜测是“雨伞”能比较形象的描述这种头文件的作用。可以想象一下雨伞☂️的形状和作用,最顶部的那个尖就像把所有东西集中起来,雨伞也将伞下的细节给遮盖起来了。

@import ModuleName;
@import ModuleName.SubmoduleName;

通过 @import 使用 Module,并且 Module 支持 Submodule。如果在 Build Settings 中将 Enable Modules (C and Objective-C) 打开,不需要改变代码,即使使用的是旧的 #import 方式,编译器也会在编译的时候自动地把可能的地方换成 Modules 的写法去编译的。

总之,Module 有如下特点:

  • 单独编译处理,宏不会互相影响
  • 编译时间从 M * N 变成了 M + N
  • 自动链接相关库

下一篇,将讲一下如何将一个 Objective-C 与 Swift 混编的代码,从依赖混乱不完备的 Git Submodule,一步步的抽成 Development Pod,最后变成二进制的过程与遇到的坑。

Article by Joe Shang

参考: