JHHK

欢迎来到我的个人网站
行者常至 为者常成

Swift混编3

-参考文章1:从预编译的角度理解Swift与Objective-C及混编机制

目录

简图

在同一个target内的互相调用

我们知道,每个文件的编译是独立的。能够互相调用,就是在编译阶段能够找到对方声明的接口。

一、在 App target 内的互相调用

我们创建一个Object-C的工程:InvocationDemo

1、OC调用OC
通过头文件来知道对方的声明接口

2、OC调用Swift
在 App target 中如果有OC和swift的代码混编,编译时会生成一个ProjectName-swift.h的文件
提示:要了解ProjectName-swift.h文件是如何生成的,在参考文章1中有讲述

// swift中被@objc修饰的类和方法,会以OC声明的方式放在这个文件
InvocationDemo-Swift.h

// oc调用swift时只需要引用这个头文件就可以
#import "InvocationDemo-Swift.h"

这个头文件是编译器过程中自动生成的不需要手动生成,如果需要手动添加,该文件的位置如下:

// xx.noindex是存放中间代码的  
/Users/YourUsername/Library/Developer/Xcode/DerivedData/YourProjectName-*/Build/Intermediates.noindex
/YourLibraryName.build/Debug-iphoneos/YourLibraryName.build/Objects-normal/arm64/YourLibraryName-Swift.h

InvocationDemo-Swift.h就是声明的接口文件

3、Swift调用OC
在 InvocatoionDemo 这个 App target 中创建一个SwiftViewController.swift文件,系统会提示创建一个桥接头文件
提示:如果工程是swift工程,那么在创建Object-C文件时同样会提示创建桥接头文件

// swift调用oc需要用到这个文件
InvocationDemo-Bridging-Header.h

// 将OC代码的头文件放到这个文件内,swift代码就可以调用oc代码了
#import "OCPerson.h"

InvocationDemo-Bridging-Header.h 就是声明的接口文件

4、Swift 调用 Swift

(1)在参考文章1的 第一步 - 如何寻找 Target 内部的 Swift 方法声明 章节有说明

Swift没有头文件,意味着,编译器会进行额外的操作来查找接口定义并需要持续关注接口的变化!

Swiftc 编译的时候,会将相同 Target 里的其他 Swift 文件进行一次解析,用来检查其中与被编译文件关联的接口部分是否符合预期。

每编译一个文件,就需要将当前 Target 里的其余文件当做接口

(2)添加了swift文件后,在编译产物中会看到多出了一个InvocationDemo.swiftmodule文件

// Swift没有头文件,这个文件的作用是提供 Swift 模块的接口描述,用于swift模块之间(不是模块内部)的互相调用  
InvocationDemo.swiftmodule

InvocationDemo-Swift.h文件是在InvocationDemo.swiftmodule文件的基础上生成的。
也就是说先有swiftmodule文件再有-swift.h文件

xy:在target内部,swift之间的相互调用不是通过InvocationDemo.swiftmodule文件找到对方的。swiftmodule文件是在编译完每个swift文件之后生成的,这在参考文章1中有相关的说明。下面的编译顺序也能说明这个问题

在 app target 内总结
(1)Swift调用OC:InvocationDemo-Bridging-Header.h 添加swift文件时会提示创建
(2)OC调用Swift:InvocationDemo-Swift.h 该文件是编译过程中自动生成的不需要手动创建
(3)OC调用OC:头文件
(4)Swift调用Swift:被编译swift文件将其它swift文件当做接口文件
当我们创建一个Swift工程时,在工程内进行混编也是一样的道理。

二、在 library 内的互相调用

library 内允许创建 OCLibrary-Bridging-Header.h 桥接头文件,所以 library 内 OC 和 Swift 代码的调用,跟在 App target 内并无区别

(1)Swift调用OC:InvocationDemo-Bridging-Header.h 添加swift文件时会提示创建
(2)OC调用Swift:InvocationDemo-Swift.h 该文件是编译过程中自动生成的不需要手动创建
(3)OC调用OC:头文件
(4)Swift调用Swift:被编译swift文件将其它swift文件当做接口文件

同样在编译产物里也能看到OCLibrary.swiftmodule 文件

三、在 framework 内的互相调用

1、如何互相调用
(1)Swift调用OC:不同于上面两种情形
(2)OC调用Swift:InvocationDemo-Swift.h 该文件是编译过程中自动生成的不需要手动创建
(3)OC调用OC:头文件
(4)Swift调用Swift:被编译swift文件将其它swift文件当做接口文件

不管是动态还是静态framework,我们在添加swift文件时不会再提示创建 Projectname-Bridging-Header.h文件
framework内不允许使用桥接头文件

2、那么我们Swift代码如何调用OC代码呢?

framework在构建时会生成modulemap文件,我们将OC的头文件放在modulemap里,就可以被swift引用到
提示:在参考文章1中 第二步 - 如何找到 Objective-C 组件里的方法声明 章节里有说明:Swift 编译器将 Clang 的大部分功能包含在其自身的代码中,这就使得我们能够以 Module 的形式,直接引用 Objective-C 的代码

我们查看产物里的modulemap发现SwiftFramework.h文件是modulemap文件的伞文件

framework module SwiftFramework {
  umbrella header "SwiftFramework.h"
  export *

  module * { export * }
}

module SwiftFramework.Swift {
  header "SwiftFramework-Swift.h"
  requires objc
}

所以我们将oc的头文件放在伞文件下就可以了

#import <Foundation/Foundation.h>

//! Project version number for SwiftFramework.
FOUNDATION_EXPORT double SwiftFrameworkVersionNumber;

//! Project version string for SwiftFramework.
FOUNDATION_EXPORT const unsigned char SwiftFrameworkVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <SwiftFramework/PublicHeader.h>


// 需要用这种方式
#import <SwiftFramework/SwiftFrameOCPerson.h>


3、不要暴露Swift代码给外部模块,但又能让内部的oc代码调用该如何做?
在Framework内要想swif代码在ProjectName-Swift.h文件内生成声明,需要objc和public两个条件修饰
这样swift代码就会暴露给外部,如果我只是在Framework内部的OC代码调用Swift而不暴露该怎么操作?
我们先写上@objc和public,让编译器自动生成声明代码

// 指定具体的tag:SwiftFramePerson,以方便关联
@objc(SwiftFramePerson)
public class SwiftFramePerson: NSObject {
    @objc
    public func test(){
        print("this is SwiftFramePerson test function")
    }
}

将生成的编译代码拷贝出来

SWIFT_CLASS_NAMED("SwiftFramePerson")
@interface SwiftFramePerson : NSObject
- (void)test;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

这个时候我们就可以去掉public了或者改为internal,在模块外部就不会被访问到。
将拷贝出来的声明代码,放到我们需要调用Swift代码的xxx.m文件内即可。

#import "SwiftFrameOCViewController.h"

// 这个文件很重要,SWIFT_CLASS_NAMED宏定义就在这个文件内
#import <SwiftFramework/SwiftFramework-Swift.h>

SWIFT_CLASS_NAMED("SwiftFramePerson")
@interface SwiftFramePerson : NSObject
- (void)test;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end


@interface SwiftFrameOCViewController ()

@end

@implementation SwiftFrameOCViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    SwiftFramePerson *person = [[SwiftFramePerson alloc] init];
    [person test];
}

协议也是类似的方式:

@objc(AnimalProtocol)
public protocol Animal {
    init()
    func walk(withStep: Int)
}

拷贝出来后,去掉public或者改为internal

SWIFT_PROTOCOL_NAMED("Animal")
@protocol AnimalProtocol
- (nonnull instancetype)init;
- (void)walkWithStep:(NSInteger)withStep;
@end

4、不要暴露OC代码给外部模块,但又能让内部的Swift代码调用该如何做?

在内部swift代码要想调用oc代码,那么oc代码需要放到伞文件内,这样也就暴露给了外部,该如何避免呢?
我们可以通过协议的方式

(1)我们先定义一个协议和创建一个swift类

// 协议作为swift和oc沟通的桥梁  
@objc(PersonProtocol)
public protocol Person {
    init()
    func test()->Void
}


@objc
public class SwiftFrameViewController: UIViewController {
    
    // 注册一个实现了协议的类对象
    private static var PersonType:Person.Type?
    @objc
    public static func registerPersonType(type:Person.Type) {
        PersonType = type
    }

    // 用注册的类对象生成实例对象,这样就不用显示的去引用oc的头文件
    private static func createPerson()->Person? {
        return PersonType?.init()
    }
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        let p: Person? = SwiftFrameViewController.createPerson()
        p?.test()
    }
}

(2)在OC的Person类加载的时候进行注册

#import "Person.h"
//协议和SwiftFrameViewController的声明都在这个文件内
#import <SwiftFramework/SwiftFramework-Swift.h>

@interface Person ()<PersonProtocol>

@end

@implementation Person

+(void)load {
    //将Person的类对象传递过去
    [SwiftFrameViewController registerPersonTypeWithType:[self class]];
}

-(void)test{
    NSLog(@"Person test");
}

(3)升级改造
虽然上述方法不会暴露Person给外部模块了,但会将协议和注册的方法暴露给外部模块,这也是我们不愿意看到的。
提示:我们是否可以按照上一章节提到的方式,将协议和注册的方法限制在模块内呢?答案是不行,因为SwiftFrameViewController是public修饰,如果协议不用public修饰就会报错。另外SwiftFrameViewController是公开的,我们也没办法单独的将注册方法搞成只在模块内访问。

我们抽出一个Swift中间类,作为swift和OC的桥梁

这个中间类,1需要能帮助SwiftFrameViewController创建遵守协议的实例,2需要让oc将自己的类对象传递过来。 3自己不能暴露给外部模块

中间类PersonManager.swift代码如下

import UIKit

@objc(PersonProtocol)
internal protocol Person {
    init()
    func test()->Void
}


@objc(PersonManager)
internal class PersonManager: NSObject {
    private static var PersonType:Person.Type?
    
    @objc
    static func registerPersonType(type:Person.Type) {
        PersonType = type
    }

    static func createPerson()->Person? {
        return PersonType?.init()
    }
}

改造后的Person.m内代码如下

#import "Person.h"
//协议和SwiftFrameViewController的声明都在这个文件内
#import <SwiftFramework/SwiftFramework-Swift.h>

SWIFT_PROTOCOL_NAMED("Person")
@protocol PersonProtocol
- (nonnull instancetype)init;
- (void)test;
@end


SWIFT_CLASS_NAMED("PersonManager")
@interface PersonManager : NSObject
+ (void)registerPersonTypeWithType:(Class <PersonProtocol> _Nonnull)type;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end


@interface Person ()<PersonProtocol>

@end

@implementation Person
+(void)load {
    [PersonManager registerPersonTypeWithType:[self class]];
}

-(void)test{
    NSLog(@"Person test");
}

@end

改造后的SwiftFrameViewController.swift代码如下

@objc
public class SwiftFrameViewController: UIViewController {
    public override func viewDidLoad() {
        super.viewDidLoad()
        let p: Person? = PersonManager.createPerson()
        p?.test()
    }
}

注意:在创建demo演示过程中,Framework中的Person的load方法不调用,始终找不到原因。后来才知道Framework内没有被直接使用的类不会被加载进二进制文件内,需要给链接器配置-ObjC才可以,参考OC的Other Linker Flags这篇文章 Other Linker Flags

在不同的target内的互相调用

一、App target 使用 framework

1、app target 中的 swift 代码 调用 framework中的 swift 代码

能调用的关键是framework库中的SwiftFramework.swiftmodule文件

在 app target -> build phases -> link binary with libraries 中,正确添加framework
注意:如果是动态framework,需要embed,否则在启动的时候无法加载动态库

在 app target 的 swift 代码中添加自定义模块的引用
说明:swift中 import 的作用是引入其它模块

import SwiftFramework

接下来就可以调用framework内的swift代码了
注意: SwiftFrameViewController 类需要是 public 否则不会暴露给其它模块

    @objc
    func jumpHandle(button:UIButton){
        let vc = SwiftFrameViewController()
        self.navigationController?.pushViewController(vc, animated: true)
    }

2、app target 中的 oc 代码 调用 framework中的 swift 代码

(1)oc代码调用swift代码,需要SwiftFramework-Swift.h文件
(2)SwiftFramework不会自动生成SwiftFramework-Swift.h文件,需要在 Build Settings -> Install Generated Header 的开关打开,打开后在产物里才会有这个文件
(3)被oc调用的swift代码需要添加@objc标识,只有添加了@objc标识的代码的声明才会出现在SwiftFramework-Swift.h里

import UIKit

@objc
public class SwiftFrameViewController: UIViewController {
    public override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "SwiftFrameViewController"
    }
}

在 app target 的 oc 代码中调用

#import <SwiftFramework/SwiftFramework-Swift.h>

...

-(void)jumpHandle:(UIButton*)jumpBtn {
    UIViewController * vc = [[SwiftFrameViewController alloc] init];
    [self.navigationController pushViewController:vc animated:YES];
}

3、app target 中的 swift 代码 调用 framework中的 oc 代码
为了能在外部访问,我们将 SwiftFramework 中的 SwiftFrameOCViewController.h 头文件 拖入到 Build Phases -> Headers -> public
这时候编译会报错:
xy:我们在 app target 中使用 framework时,会优先通过modulemap来寻找头文件的信息,所以暴露给外部的头文件需要添加到伞文件,信息才会被modulemap管理。从这一方面看苹果也是在主推modulemap

我们知道framework的伞文件就是 SwiftFramework.h, 所以将SwiftFrameOCViewController.h文件添加到该文件中

#import <Foundation/Foundation.h>

//! Project version number for SwiftFramework.
FOUNDATION_EXPORT double SwiftFrameworkVersionNumber;

//! Project version string for SwiftFramework.
FOUNDATION_EXPORT const unsigned char SwiftFrameworkVersionString[];

#import <SwiftFramework/SwiftFrameOCViewController.h>

在 app target 中的 swift 代码中调用

import SwiftFramework

...

@objc
func jumpHandle(button:UIButton){
    let vc = SwiftFrameOCViewController()
    self.navigationController?.pushViewController(vc, animated: true)
}

xy:我们手动将SwiftFramework中的module.modulemap文件移走,app target照样能正常运行,所以起作用的还是SwiftFramework.swiftmodule.猜测SwiftFramework在编译过程中将SwiftFrameOCViewController.h头文件信息以某种方式打包进了SwiftFramework.swiftmodule文件内

根据前面的相关讲解,我们其实还有一种方式让 app target 中的 swift 代码 调用到 framework中的 oc 代码

将#import <SwiftFramework/SwiftFrameOCViewController.h>放在 app target 内的 桥接头文件内

// app target 的 桥接头文件
InvocationDemo-Bridging-Header.h

内容如下:
<SwiftFramework/SwiftFrameOCViewController.h>

这样app target 中的 swift 代码 不需要import模块就可以直接调用

@objc
func jumpHandle(button:UIButton){
    let vc = SwiftFrameOCViewController()
    self.navigationController?.pushViewController(vc, animated: true)
}

4、app target 中的 oc 代码 调用 framework中的 oc 代码

(1)通过传统方式引入

#import <SwiftFramework/SwiftFrameOCViewController.h>

...

-(void)jumpHandle:(UIButton*)jumpBtn {
    SwiftFrameOCViewController * vc = [[SwiftFrameOCViewController alloc] init];
    [self.navigationController pushViewController:vc animated:YES];
}

(2)通过module方式引入

// 引入整个模块
//@import SwiftFramework;

// 只引入需要的子模块
@import SwiftFramework.SwiftFrameOCViewController;


...

-(void)jumpHandle:(UIButton*)jumpBtn {
    SwiftFrameOCViewController * vc = [[SwiftFrameOCViewController alloc] init];
    [self.navigationController pushViewController:vc animated:YES];
}

(3)做个小实验
我们手动将SwiftFramework中的module.modulemap文件移走,会发现 @import SwiftFramework; 的这种方式会报错:
Module ‘SwiftFramework’ not found
所以模块引入是依赖module.modulemap的

另外从文章顶部列出的参考文章中我们得知:#import <SwiftFramework/SwiftFrameOCViewController.h> 这种方式编译器会帮我们转为 @import SwiftFramework 的方式
优先从module中寻找头文件信息,只有找不到时才会去搜索头文件

通过这种方式#import <SwiftFramework/SwiftFrameOCViewController.h> 引入,然后预编译下.m文件看看是哪种方式引入的。(modulemap的方式引入)
删除framework内的modulemap文件,再预编译看看是哪种方式引入的。(头文件的方式引入)

二、App target 使用 Library

Library不像framework内部可以包含资源文件,比如headers目录和module相关的文件。它是一个xx.a的纯二进制格式的文件。
不过在产物目录里 OCLibrary target 会将头文件放在一个include文件夹内(注意:只有在Build Phases - Copy Files中添加的文件才会被拷贝到include目录下)

我们在app target 的build setting 内设置好 Header Search Paths 和 Libary Search Path就可以引用库中的OC代码了
提示:头文件的路径设置,设置到include和设置到OCLibary引用时是有区别的
设置到include:import <OCLibrary/OCLibOCPerson.h>这样引用更规范
设置到OCLibary:import “OCLibOCPerson.h”这样引用不能知道是库的头文件

1、OC调用OC:头文件
2、Swift调用OC:将头文件放到 app target 内的 桥接头文件内供swift调用

那么 APP target 中的 OC代码如何调用 OCLibrary target 内的 Swift代码呢?

我们打开 OCLibrary 中 build setting -> install generated header 的开关,仍然没有帮我们生成 OCLibrary-Swift.h 头文件
那么我们去上面提到的中间产物目录中将这个头文件自己考出来放到我们存放头文件的目录即可

3、OC调用Swift:通过 <OCLibrary/OCLibrary-Swift.h>

那么 APP target 中的 Swift代码如何调用 OCLibrary target 内的 Swift代码呢?

我们注意到OCLibrary target 的产物目录里有一个 OCLibrary.swiftmodule文件
我们在 APP target 的 build setting 中 搜索 search path 按照下面配置
将 OCLibrary.swiftmodule 配置给 swift 的编译器

然后我们就可以以模块的形式使用swift代码了

思考
我们看到上面有一个警告
Implicit import of bridging header ‘OCLibrary-Bridging-Header.h’ via module ‘OCLibrary’ is deprecated and will be removed in a later version of Swift
通过模块’OCLibrary’隐式导入桥接头’OCLibrary- bridging - header .h’已被弃用,并将在Swift的后续版本中删除

出现这个警告的原因,是因为我们的OCLibrary中有 OCLibrary-Bridging-Header.h造成,但是我们不用这个头文件,那么在OCLibrary内部swift代码就没法调用OC代码
所以我们能不能参考Framework,创建一个module.modulemap文件,配置给swift编译器,达到swift调用oc的目的,尝试了下将自定义的module.modulemap文件配置到 Swift Compiler-Search Paths - Import Paths没有成功。 可以再探索探索

总结

一、几个重要的文件

1、ProjectName-Swift.h
2、ProjectName-Bridging-Header.h
3、module.modulemap
4、ProjectName.swiftmodule

二、几个重要的编译配置项

1、clang相关
enable modules:开启使用modulemap
module map file:配置自定义的modulemap文件
private module map file:

2、SwiftC相关
install generated Header:打开后会生成ProjectName-Swift.h
import paths:配置project.swiftmodule

三、存在的问题

1、问题一
在Framework或者Library内部swift代码要想被oc调用,权限需要是 public 才会在 projectName-swift.h文件中生成相关的声明。
但这样就会带来一个问题:就是swift代码也会暴露给模块外部使用

2、问题二
在Framework内部,oc代码要想被swift代码调用,需要将oc代码的头文件声明放在伞文件内,但放在伞文件内需将oc的头文件放在 Build Phases - Headers - Public内
但这样就会带来一个问题:oc的头文件就会暴露给外部


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.