动态库中中使用modulemap使OC与Swift互相调用

动态库中使用modulemap使OC与Swift互相调用

# 动态库中OC如何与Swift互相调用
在RFramework使用过程中,遇到了一个难以复现的问题,再所以需要对SDK添加Log进行监控,经过调研,决定引入`CocoaLumberjack`这个日志库,但因为SDK本身有一些Swift的代码,现在加入这个日志库后,需要Swift中的代码调用这个日志库了,所以

### 1. **ObjC 调用 Swift**

在Framework中实现 ObjC 调用 Swift,和平时在项目中那样,过设置`Swift Compiler -> Objective-C Generated Interface Header Name`就可以了
- 在 Build Setting 中,搜索SWIFT_OBJC_INTERFACE_HEADER_NAME,配置正确的名字,比如默认的`$(SWIFT_MODULE_NAME)-Swift.h`
- 然后在 ObjC 中引入该模块的 Swift 头文件,然后使用#import <XXX/XXX-Swift.h>倒入生成的头文件
- 若在 ObjC的 .h 中引入,则可以通过向前声明的方式,@class XXX

```ObjC
// RLog.m
#import <RFramework/RFramework-Swift.h>

-(void)log:(NSString *)message {
  DDDebug(message);
}

-(void)useSwiftTest {
    RPreviewController *preview = [[RPreviewController alloc] init];
    [preview show];
}

2. Swift 调用 ObjC

但是在Framework中实现 Swift 调用 ObjC,通过设置 Bridging-Header 的方式是无法解决的。如果尝试在Swift Compiler -> Objective-C Bridging Header填入OC的头文件,然后编译,最终将会报错: Using bridging headers with framework targets is unsupported

此时可以使用 modulemap 的方式访问,创建一个RFramework.modulemap文件

/* RtFramework.modulemap */
framework module RFramework {
    umbrella "Headers"
    export *
}

然后在在 buildSettings → packaging → Module Map File 写入这个 modulemap文件的路径,然后把想要在Swift中使用的OC文件,比如Pikachu.h放在Build Phases → Hearders → Public

// RPreviewController.swift
@objcMembers public class RPreviewController: UIViewController {
    ...

    func show() {

      ....

      webView.loadRequest(request)

      RLog.log("RPreviewController.show")
    }

}

此时就可以在动态库内部,使用Swift调用OC了.

那这个 modulemap 是个啥意思呢,它是用来描述头文件与module之间映射的关系,比如前边写的那个文件: framework module 代表这个是一个Framework的module, Framework的名称叫做RFramework

umbrella "Headers",表示该模块包含 "Headers"目录下的所有头文件.其实从名字看就也能大概理解,伞状头文件,伞下边的所有东西(伞兵啊什么的…),都属于伞的一部分

OK,那么Headers文件夹是哪里来的呢?当framework编译完后,编译产物会自动生成一个Headers文件夹,并且把所有 Public 的头文件都放在这个文件夹里面

在外部项目中使用:

// ViewController.m
#import <RFramework/RFramework.h>
//或者使用module语法 `@import RFramework;`

- (void)viewDidLoad {
    [super viewDidLoad];
    
    RPreviewController *m = [[RPreviewController alloc] init];
    RLog *l = [[RLog alloc] init];
}

当然在看别人第三方库的时候,会发现还有另外一种用法:

framework module RFramework {
    umbrella header "RFramework.h"
    export *
}

这种写法的意思是,只有在"RFramework.h" 中引用的头文件,才能作为该模块的一部分给外部使用,比如我们在 RFramework.h 中导入下边的文件

// RFramework.h
#import "RPreviewController.h"

此时,你会发现 RPreviewController 可以正常使用, RLog报错了,使用未声明的文件,即使你强行导入这个文件 #import <RFramework/RLog.h> 也还是报错

#import <RFramework/RFramework.h>

- (void)viewDidLoad {
    [super viewDidLoad];
    
    RPreviewController *m = [[RPreviewController alloc] init];
    RLog *r = [[RLog alloc] init]; // Use of undeclared identifier 'RLog'
}

子module的语法,把开始的例子的modulemap文件做些修改,在module中再声明两module: PreviewLog

framework module RFramework {
    module Preview {
        header "RPreviewController.h"
    }
    module Log {
        header "RLog.h"
    }
}

使用的时候只导入 #import <RFramework/RPreviewController.h> 或者使用module语法 @import RFramework.Preview;就可以了

- (void)viewDidLoad {
    [super viewDidLoad];

    RPreviewController *p = [[RPreviewController alloc] init];
	
}

export * 首先说 export,也可以叫做re_export

使用上边的那个modulemap文件,然后只导入@import RFramework.Preview;,这时编译就会报错,要想使用RLog的话,需要导入模块 Log

- (void)viewDidLoad {
    [super viewDidLoad];

    RPreviewController *p = [[RPreviewController alloc] init];
		// Declaration of 'RLog' must be imported from module 
    //'RFramework.Log' before it is required
    RLog *l = [[RLog alloc] init];
}

当然,直接导入Log模块就不会报错了,但是如果我既不想导入,还想正常使用该怎么办呢?

module Preview {
    header "RPreviewController.h"
    export Log
}

对modulemap文件稍做修改,把LogPreview 的模块中重新导出去,使用就不会报错,所以 上边的 export * 的意思就很明显了,导出当前 头文件/头文件集合 的所有的符号

当然,subModule其实在swift中,只有象征意义了,尽管只倒入

参考1: 京东App Swift 混编及组件化落地 - 知乎 (zhihu.com)

参考2: Swift与Objective-C互相调用(总结) - 掘金 (juejin.cn)


本博客文章采用 CC 4.0 协议,转载需注明出处和作者。