7.3.1.1 与 Swift/Objective-C 互操作
14 分钟阅读
7.3.1.1 与 Swift/Objective-C 互操作
注意: Objective-C 库导入处于 Beta 阶段。由 cinterop 工具从 Objective-C 库生成的所有 Kotlin 声明都应带有
@ExperimentalForeignApi注解。随 Kotlin/Native 提供的原生平台库(例如 Foundation、UIKit 和 POSIX)只对部分 API 要求选择启用。
Kotlin/Native 通过 Objective-C 提供与 Swift 的间接互操作。本文介绍如何在 Swift/Objective-C 代码中使用 Kotlin 声明,以及如何在 Kotlin 代码中使用 Objective-C 声明。
你可能还会觉得以下资源有用:
- Kotlin-Swift interopedia,一个关于如何在 Swift 代码中使用 Kotlin 声明的示例集合。
- 与 Swift/Objective-C ARC 集成部分,介绍 Kotlin 追踪式 GC 与 Objective-C ARC 之间集成的细节。
把 Swift/Objective-C 库导入 Kotlin
Objective-C 框架和库如果被正确导入到构建中(系统框架默认导入),就可以在 Kotlin 代码中使用。更多细节请参阅:
如果 Swift 库的 API 通过 @objc 导出到 Objective-C,就可以在 Kotlin 代码中使用。纯 Swift 模块目前尚不受支持。
在 Swift/Objective-C 中使用 Kotlin
如果 Kotlin 模块被编译为 framework,就可以在 Swift/Objective-C 代码中使用:
- 请参阅构建最终原生二进制文件了解如何声明二进制文件。
- 查看 Kotlin Multiplatform 示例项目作为示例。
对 Objective-C 和 Swift 隐藏 Kotlin 声明
实验性
为了让你的 Kotlin 代码对 Swift/Objective-C 更友好,请使用 @HiddenFromObjC 注解把某个 Kotlin 声明对 Objective-C 和 Swift 隐藏。它会禁用该函数或属性向 Objective-C 的导出。
或者,你可以用 internal 修饰符标记 Kotlin 声明,以限制它们在编译模块中的可见性。如果你希望把该 Kotlin 声明对 Objective-C 和 Swift 隐藏,同时仍对其他 Kotlin 模块可见,请使用 @HiddenFromObjC。
在 Kotlin-Swift interopedia 中查看示例。
在 Swift 中使用 refinement
实验性
@ShouldRefineInSwift 有助于用 Swift 编写的包装器替换 Kotlin 声明。该注解会把函数或属性在生成的 Objective-C API 中标记为 swift_private。这类声明会带上 __ 前缀,从而对 Swift 不可见。
你仍然可以在 Swift 代码中使用这些声明来创建对 Swift 友好的 API,但它们不会出现在 Xcode 的自动补全建议中。
- 关于在 Swift 中 refinement Objective-C 声明的更多信息,请参阅 Apple 官方文档。
- 关于如何使用
@ShouldRefineInSwift注解的示例,请参阅 Kotlin-Swift interopedia。
修改声明名称
实验性
要避免重命名 Kotlin 声明,请使用 @ObjCName 注解。它指示 Kotlin 编译器为带注解的类、接口或其他 Kotlin 实体使用自定义的 Objective-C 和 Swift 名称:
| |
在 Kotlin-Swift interopedia 中查看另一个示例。
使用 KDoc 注释提供文档
文档对于理解任何 API 都至关重要。为共享的 Kotlin API 提供文档,可以让你就用法、注意事项等与使用者沟通。
生成 Objective-C 头文件时,Kotlin 代码中的 KDoc 注释会被转换为相应的 Objective-C 注释。例如,以下带 KDoc 的 Kotlin 代码:
| |
会生成带相应注释的 Objective-C 头文件:
| |
KDoc 注释会被嵌入 klib,并在生成 Apple framework 时从 klib 中提取出来。因此,类和方法的注释会出现在自动补全中,例如在 Xcode 中。如果你跳到 .h 文件中函数的定义处,会看到关于 @param、@return 及类似标签的注释。
已知限制:
- 除非使用
-Xexport-kdoc选项编译,否则依赖项的文档不会被导出。使用该编译器选项编译的库可能与其他编译器版本不兼容。 - KDoc 注释大多按原样导出,但许多 KDoc 块标签(例如
@property)不受支持。
如有必要,你可以在 Gradle 构建文件的 binaries {} 块中禁止把 KDoc 注释从 klib 导出到生成的 Apple framework:
| |
映射
下表展示了 Kotlin 概念如何映射到 Swift/Objective-C,以及反向的映射。
“->” 和 “<-” 表示映射只朝一个方向进行。
| Kotlin | Swift | Objective-C | 说明 |
| class | class | @interface | 说明 |
| interface | protocol | @protocol | |
| constructor/create | Initializer | Initializer | 说明 |
| 属性 | 属性 | 属性 | 说明 1、说明 2 |
| 方法 | 方法 | 方法 | 说明 1、说明 2 |
| enum class | class | @interface | 说明 |
| suspend -> | completionHandler:/ async | completionHandler: | 说明 1、说明 2 |
| @Throws fun | throws | error:(NSError**)error | 说明 |
| 扩展 | 扩展 | Category 成员 | 说明 |
| companion 成员 <- | 类方法或属性 | 类方法或属性 | |
| null | nil | nil | |
| Singleton | shared 或 companion 属性 | shared 或 companion 属性 | 说明 |
| 原始类型 | 原始类型 / NSNumber | | 说明 |
| Unit 返回类型 | Void | void | |
| String | String | NSString | 说明 |
| String | NSMutableString | NSMutableString | 说明 |
| List | Array | NSArray | |
| MutableList | NSMutableArray | NSMutableArray | |
| Set | Set | NSSet | |
| MutableSet | NSMutableSet | NSMutableSet | 说明 |
| Map | Dictionary | NSDictionary | |
| MutableMap | NSMutableDictionary | NSMutableDictionary | 说明 |
| 函数类型 | 函数类型 | Block 指针类型 | 说明 |
| 内联类 | 不支持 | 不支持 | 说明 |
类
名称转换
Objective-C 类以原始名称导入 Kotlin。协议会以带 Protocol 名称后缀的接口形式导入,例如 @protocol Foo -> interface FooProtocol。这些类和接口会被放入构建配置中指定的包中(对于预配置的系统框架则是 platform.* 包)。
Kotlin 类和接口的名称在导入 Objective-C 时会加上前缀。该前缀来自 framework 名称。
Objective-C 在 framework 中不支持包。如果 Kotlin 编译器在同一个 framework 中发现名称相同但包不同的 Kotlin 类,会对它们重命名。该算法目前还不稳定,可能在不同 Kotlin 版本之间发生变化。为绕过这个问题,你可以重命名 framework 中冲突的 Kotlin 类。
强链接
只要你在 Kotlin 源码中使用某个 Objective-C 类,它就会被标记为强链接符号。生成的构建产物会把这些相关符号列为强外部引用。
这意味着应用在启动时会尝试动态链接这些符号,如果它们不可用,应用就会崩溃。即使这些符号从未被使用,崩溃也会发生。符号可能在特定设备或 OS 版本上不可用。
要绕过这个问题并避免 “Symbol not found” 错误,请使用检查该类是否真正可用的 Swift 或 Objective-C 包装器。查看 Compose Multiplatform framework 中是如何实现这一变通方案的。
初始化器
Swift/Objective-C 的初始化器会作为构造器或名为 create 的工厂方法导入 Kotlin。后者适用于在 Objective-C category 或 Swift 扩展中声明的初始化器,因为 Kotlin 没有扩展构造器的概念。
提示: 在把 Swift 初始化器导入 Kotlin 之前,别忘了用
@objc注解标记它们。
Kotlin 构造器会作为初始化器导入 Swift/Objective-C。
Setter
覆盖父类只读属性的可写 Objective-C 属性,会表示为属性 foo 的 setFoo() 方法。对于以可变方式实现的协议只读属性,情况也是如此。
顶层函数和属性
顶层 Kotlin 函数和属性可以作为特殊类的成员访问。每个 Kotlin 文件都会转换为这样一个类,例如:
| |
然后你可以像这样从 Swift 调用 foo() 函数:
| |
在 Kotlin-Swift interopedia 中查看一组关于访问顶层 Kotlin 声明的示例:
方法名转换
一般来说,Swift 参数标签和 Objective-C 选择器片段会映射为 Kotlin 参数名。这两个概念语义不同,因此有时 Swift/Objective-C 方法导入后会与 Kotlin 签名发生冲突。在这种情况下,可以从 Kotlin 用命名实参调用这些冲突的方法,例如:
| |
在 Kotlin 中是这样的:
| |
kotlin.Any 的函数映射到 Swift/Objective-C 的方式如下:
| Kotlin | Swift | Objective-C |
| equals() | isEquals(_:) | isEquals: |
| hashCode() | hash | hash |
| toString() | description | description |
在 Kotlin-Swift interopedia 中查看数据类的示例。
你可以为 Swift 或 Objective-C 指定更地道的名称,而不必用 @ObjCName 注解重命名 Kotlin 声明。
错误与异常
所有 Kotlin 异常都是非受检的,即错误在运行时被捕获。而 Swift 只有编译期处理的受检错误。因此,如果 Swift 或 Objective-C 代码调用了会抛出异常的 Kotlin 方法,该 Kotlin 方法应使用 @Throws 注解标记,并指定一组“预期的”异常类。
编译为 Swift/Objective-C framework 时,带有或继承 @Throws 注解的非 suspend 函数在 Objective-C 中表示为产生 NSError* 的方法,在 Swift 中表示为 throws 方法。suspend 函数的表示在完成处理器中总是带有一个 NSError*/Error 参数。
当从 Swift/Objective-C 代码调用的 Kotlin 函数抛出某个由 @Throws 指定的类或其子类的异常实例时,该异常会作为 NSError 传播。其他到达 Swift/Objective-C 的 Kotlin 异常会被视为未处理,并导致程序终止。
不带 @Throws 的 suspend 函数只会传播 CancellationException(以 NSError 形式)。不带 @Throws 的非 suspend 函数完全不传播 Kotlin 异常。
请注意,反向的转换尚未实现:Swift/Objective-C 中会抛错的方法不会作为会抛异常的方法导入 Kotlin。
在 Kotlin-Swift interopedia 中查看示例。
枚举
Kotlin 枚举在 Objective-C 中作为 @interface 导入,在 Swift 中作为 class 导入。这些数据结构的属性与每个枚举值相对应。考虑以下 Kotlin 代码:
| |
你可以像这样从 Swift 访问这个枚举类的属性:
| |
要在 Swift 的 switch 语句中使用 Kotlin 枚举变量,请提供 default 分支以避免编译错误:
| |
在 Kotlin-Swift interopedia 中查看另一个示例。
挂起函数
实验性
Kotlin 的挂起函数(suspend)在生成的 Objective-C 头文件中表现为带回调的函数,用 Swift/Objective-C 的术语说就是完成处理器。
从 Swift 5.5 开始,Kotlin 的 suspend 函数也可以不使用完成处理器、而作为 async 函数从 Swift 调用。目前该功能仍高度实验性,并且有一些限制。细节请参阅这个 YouTrack issue。
- 在 Swift 文档中进一步了解
async/await机制。 - 在 Kotlin-Swift interopedia中查看示例以及关于实现相同功能的第三方库的建议。
扩展与 category 成员
Objective-C category 和 Swift 扩展的成员通常作为扩展导入 Kotlin。正因如此,这些声明在 Kotlin 中不能被覆盖,扩展初始化器也不能作为 Kotlin 构造器使用。
注意: 目前有两个例外。从 Kotlin 1.8.20 开始,与 NSView 类(来自 AppKit framework)或 UIView 类(来自 UIKit framework)声明在相同头文件中的 category 成员,会作为这些类的成员导入。这意味着你可以覆盖继承自 NSView 或 UIView 的方法。
对“普通”Kotlin 类的 Kotlin 扩展会分别作为扩展和 category 成员导入 Swift 和 Objective-C。对其他类型的 Kotlin 扩展会被视为带额外接收者参数的顶层声明。这些类型包括:
- Kotlin
String类型 - Kotlin 集合类型及其子类型
- Kotlin
interface类型 - Kotlin 原始类型
- Kotlin
inline类 - Kotlin
Any类型 - Kotlin 函数类型及其子类型
- Objective-C 类和协议
在 Kotlin-Swift interopedia 中查看一组示例。
Kotlin 单例
Kotlin 单例(用 object 声明创建,包括 companion object)会作为只有一个实例的类导入 Swift/Objective-C。
该实例可以通过 shared 和 companion 属性获取。
对于以下 Kotlin 代码:
| |
请按如下方式访问这些对象:
| |
注意: 在 Objective-C 中通过
[MySingleton mySingleton]访问对象、在 Swift 中通过MySingleton()访问对象的做法已被弃用。
在 Kotlin-Swift interopedia 中查看更多示例:
原始类型
Kotlin 原始类型的装箱值会映射到特殊的 Swift/Objective-C 类。例如,kotlin.Int 装箱值在 Swift 中表示为 KotlinInt 类实例(在 Objective-C 中表示为 ${prefix}Int 实例,其中 prefix 是 framework 的名称前缀)。这些类派生自 NSNumber,因此这些实例是真正的 NSNumber,支持所有相应操作。
当作为 Swift/Objective-C 参数类型或返回值使用时,NSNumber 类型不会被自动转换为 Kotlin 原始类型。原因是 NSNumber 类型没有提供关于所包装原始值类型的足够信息,例如从静态类型上并不知道 NSNumber 是 Byte、Boolean 还是 Double。因此 Kotlin 原始值应当手动与 NSNumber 相互转换。
字符串
当把 Kotlin String 传给 Swift 时,它会先导出为 Objective-C 对象,然后 Swift 编译器为了 Swift 转换再复制一次。这会带来额外的运行时开销。
为避免这一点,请在 Swift 中把 Kotlin 字符串直接作为 Objective-C 的 NSString 访问。查看转换示例。
NSMutableString
Objective-C 的 NSMutableString 类在 Kotlin 中不可用。所有 NSMutableString 实例在传给 Kotlin 时都会被复制。
集合
Kotlin -> Objective-C -> Swift
当 Kotlin 集合传给 Swift 时,它会先转换为 Objective-C 的对应类型,然后 Swift 编译器会复制整个集合并把它转换为 Swift 原生集合,如映射表中所述。
最后这次转换会带来性能开销。为避免这种开销,在 Swift 中使用 Kotlin 集合时,请把它们显式转换为对应的 Objective-C 类型:NSDictionary、NSArray 或 NSSet。
查看转换示例
例如,以下 Kotlin 声明:
| |
在 Swift 中它看起来像这样:
| |
这里 map 被隐式转换为 Swift 的 Dictionary,其字符串值被映射为 Swift 的 String。这会带来性能开销。
为避免这种转换,请把 map 显式转换为 Objective-C 的 NSDictionary,并改为以 NSString 访问值:
| |
这样就能确保 Swift 编译器不执行额外的转换步骤。
Swift -> Objective-C -> Kotlin
Swift/Objective-C 集合会如映射表中所述映射到 Kotlin,但 NSMutableSet 和 NSMutableDictionary 除外。
NSMutableSet 不会被转换为 Kotlin 的 MutableSet。要把对象传给 Kotlin 的 MutableSet,请显式创建这种 Kotlin 集合。为此,你可以使用 Kotlin 中的 mutableSetOf() 函数,或 Swift 中的 KotlinMutableSet 类以及 Objective-C 中的 ${prefix}MutableSet(prefix 是 framework 的名称前缀)。MutableMap 也是如此。
在 Kotlin-Swift interopedia 中查看示例。
函数类型
Kotlin 函数类型的对象(例如 lambda)在 Swift 中转换为闭包,在 Objective-C 中转换为 block。在 Kotlin-Swift interopedia 中查看带 lambda 的 Kotlin 函数示例。
不过,在转换函数和函数类型时,参数类型和返回值的映射方式有所不同。在后一种情况下,原始类型会映射为其装箱表示。Kotlin 的 Unit 返回值在 Swift/Objective-C 中表示为相应的 Unit 单例。该单例的值可以像访问任何其他 Kotlin object 那样获取。请参阅上表中的单例部分。
考虑以下 Kotlin 函数:
| |
它在 Swift 中表示如下:
| |
你可以像这样调用它:
| |
Objective-C block 类型中的显式参数名
实验性
你可以为导出的 Objective-C 头文件给 Kotlin 的函数类型添加显式参数名。这样,Xcode 的自动补全在 Objective-C block 中调用 Objective-C 函数时就会建议这些名称。这有助于避免生成的 block 中出现 Clang 警告。
要启用显式参数名,请把以下二进制选项添加到你的 gradle.properties 文件中:
kotlin.native.binary.objcExportBlockExplicitParameterNames=true
例如,对于以下 Kotlin 代码:
| |
Kotlin 会把 Kotlin 函数类型中的参数名转发到 Objective-C block 类型,从而让 Xcode 可以在建议中使用它们:
| |
注意: 该选项只影响 Objective-C 互操作。它适用于在 Xcode 中从 Objective-C 调用生成的 Objective-C 代码,通常不影响从 Swift 的调用。
泛型
Objective-C 支持类中定义的“轻量级泛型”,但功能相对有限。Swift 可以导入类上定义的泛型,以帮助向编译器提供额外的类型信息。
Objective-C 和 Swift 对泛型的支持与 Kotlin 不同,因此转换不可避免地会丢失一些信息,但受支持的特性仍会保留有意义的信息。
关于如何在 Swift 中使用 Kotlin 泛型的具体示例,请参阅 Kotlin-Swift interopedia。
限制
Objective-C 泛型并不支持 Kotlin 或 Swift 的所有特性,因此转换中会丢失一些信息。
泛型只能定义在类上,不能定义在接口(Objective-C 和 Swift 中的协议)或函数上。
可空性
Kotlin 和 Swift 都把可空性定义为类型规格的一部分,而 Objective-C 把可空性定义在类型的方法和属性上。因此,以下 Kotlin 代码:
| |
在 Swift 中看起来像这样:
| |
为了支持可能为空的类型,Objective-C 头文件需要把 myVal 定义为可空返回值。
为缓解这一点,在定义泛型类时,如果该泛型类型_绝不_应为空,请提供不可为空的类型约束:
| |
这会强制 Objective-C 头文件把 myVal 标记为不可为空。
型变
Objective-C 允许把泛型声明为协变或逆变。Swift 不支持型变。来自 Objective-C 的泛型类可以按需强制转换。
| |
| |
约束
在 Kotlin 中,你可以为泛型类型提供上界。Objective-C 也支持这一点,但在更复杂的情况下这种支持不可用,目前在 Kotlin 与 Objective-C 的互操作中也不受支持。唯一的例外是不可为空的上界会让 Objective-C 方法/属性变为不可为空。
禁用
要让 framework 头文件在生成时不包含泛型,请在构建文件中添加以下编译器选项:
| |
前向声明
要导入前向声明,请使用 objcnames.classes 和 objcnames.protocols 包。例如,要导入在包为 library.package 的 Objective-C 库中声明的 objcprotocolName 前向声明,请使用特殊的前向声明包:import objcnames.protocols.objcprotocolName。
考虑两个 objcinterop 库:一个使用 objcnames.protocols.ForwardDeclaredProtocolProtocol,另一个在另一个包中包含实际实现:
| |
| |
要在两个库之间传递对象,请在 Kotlin 代码中使用显式的 as 转换:
| |
注意: 你只能从对应的真实类转换到
objcnames.protocols.ForwardDeclaredProtocolProtocol。否则会报错。
映射类型之间的转换
在编写 Kotlin 代码时,可能需要把对象从 Kotlin 类型转换为等价的 Swift/Objective-C 类型,或者反向转换。在这种情况下,你可以使用 as 转换,例如:
| |
IDE 可能会错误地发出 “This cast can never succeed” 警告。在这种情况下,请使用 @Suppress("CAST_NEVER_SUCCEEDS") 注解。
子类化
在 Swift/Objective-C 中子类化 Kotlin 类和接口
Kotlin 类和接口可以由 Swift/Objective-C 的类和协议子类化。
在 Kotlin 中子类化 Swift/Objective-C 类和协议
Swift/Objective-C 的类和协议可以用 Kotlin 的 final 类子类化。继承 Swift/Objective-C 类型的非 final Kotlin 类尚不受支持,因此无法声明继承 Swift/Objective-C 类型的复杂类层次结构。
普通方法可以用 Kotlin 的 override 关键字覆盖。在这种情况下,覆盖方法必须与被覆盖方法具有相同的参数名。
有时需要覆盖初始化器,例如在子类化 UIViewController 时。以 Kotlin 构造器形式导入的初始化器,可以由带 @OverrideInit 注解的 Kotlin 构造器覆盖:
| |
覆盖构造器必须与被覆盖构造器具有相同的参数名和类型。
要覆盖 Kotlin 签名冲突的不同方法,你可以给类添加 @ObjCSignatureOverride 注解。当从 Objective-C 类继承了若干实参类型相同但实参名不同的函数时,该注解指示 Kotlin 编译器忽略这些冲突的重载。
默认情况下,Kotlin/Native 编译器不允许把非指定的 Objective-C 初始化器作为 super() 构造器调用。如果 Objective-C 库中没有正确标记指定初始化器,这种行为可能会带来不便。要禁用这些编译器检查,请把 disableDesignatedInitializerChecks = true 添加到该库的 .def 文件中。
C 特性
关于库使用普通 C 特性(例如不安全指针、结构体等)的示例,请参阅与 C 互操作。
不支持的特性
Kotlin 编程语言的某些特性尚未映射到 Objective-C 或 Swift 的相应特性。目前,以下特性在生成的 framework 头文件中没有得到适当的暴露:
- 内联类(实参被映射为底层原始类型或
id) - 实现标准 Kotlin 集合接口(
List、Map、Set)的自定义类及其他特殊类 - Objective-C 类的 Kotlin 子类