Edit Page

与 Swift/Objective-C 代码交互

最终更新: 2024/03/21

Objective-C 库的导入是 实验性功能. cinterop 工具从 Objective-C 库生成的所有 Kotlin 声明都应该标注 @ExperimentalForeignApi 注解.

Kotlin/Native 自带的原生平台库 (例如 Foundation, UIKit, 和 POSIX),

本章介绍 Kotlin/Native 与 Swift/Objective-C 的交互能力的一些细节.

关于 iOS 和 Kotlin 之间的内存管理, 详情请参见 与 iOS 集成.

使用方法

Kotlin/Native 提供了与 Objective-C 的双向交互能力. Objective-C 框架和库可以在 Kotlin 代码中使用, 只需要正确地导入到编译环境中 (系统框架已经默认导入了). 参见 编译配置. Swift 库也可以在 Kotlin 代码中使用, 只需要将它的 API 用 @objc 导出为 Objective-C. 纯 Swift 模块目前还不支持.

Kotlin 模块可以在 Swift/Objective-C 代码中使用, 只需要编译成一个框架 (参见 如何声明二进制文件). 我们提供了一个例子, 请参见 Kotlin Multiplatform Mobile 示例程序.

隐藏 Kotlin 声明

如果你不希望将 Kotlin 声明导出到 Objective-C 和 Swift, 请使用专门的注解:

  • @HiddenFromObjC 注解对 Objective-C 和 Swift 隐藏 Kotlin 声明. 这个注解会禁止一个函数或属性导出到 Objective-C, 让你的 Kotlin 代码对 Objective-C/Swift 更加友好.
  • @ShouldRefineInSwift 可以将一个 Kotlin 声明替换为 Swift 编写的一个封装(Wrapper). 这个注解会在生成的 Objective-C API 中, 将一个函数或属性标记为 swift_private. 这样的声明会带有 __ 前缀, 使得它们在 Swift 中不可见.

    你仍然可以在 Swift 代码中使用这些声明, 来创建 Swift 友好的 API, 但在 Xcode 的代码自动完成功能中, 不会显示这些声明.

    关于如何在 Swift 中润色(Refine) Objective-C 声明, 详情请参见 Apple 官方文档.

使用这些注解需要 使用者同意(Opt-in).

映射

下表展示了 Kotlin 中的各种概念与 Swift/Objective-C 的对应关系.

"->" 和 "<-" 代表单方向的对应关系.

Kotlin Swift Objective-C 注意事项
class class @interface 名称翻译
interface protocol @protocol  
constructor/create 初始化器(Initializer) 初始化器(Initializer) 初始化器
属性 属性 属性 顶层函数和属性 设值方法(Setter)
方法 方法 方法 顶层函数和属性 方法名称翻译
enum class class @interface 枚举类
suspend -> completionHandler:/ async completionHandler: 错误与异常 挂起函数
@Throws fun throws error:(NSError**)error 错误与异常
Extension Extension Category 成员 扩展与 Category 成员
companion 成员 <- Class 方法或属性 Class 方法或属性  
null nil nil  
Singleton sharedcompanion 属性 sharedcompanion 属性 Kotlin 单子(singleton)
基本类型 基本类型 / NSNumber   NSNumber
Unit 类型返回值 Void void  
String String NSString  
String NSMutableString NSMutableString NSMutableString
List Array NSArray  
MutableList NSMutableArray NSMutableArray  
Set Set NSSet  
MutableSet NSMutableSet NSMutableSet 集合
Map Dictionary NSDictionary  
MutableMap NSMutableDictionary NSMutableDictionary 集合
Function 类型 Function 类型 Block pointer 类型 Function 类型
内联类(Inline class) 不支持 不支持 不支持的特性

名称翻译

Objective-C 类导入 Kotlin 时使用它们原来的名称. Protocol 导入 Kotlin 后会变成接口, 并使用 Protocol 作为名称后缀, 也就是说 @protocol Foo 会被导入为 interface FooProtocol. 这些类和接口会放在一个 在编译配置中指定 的包之内 (预定义的系统框架导入到 platform.* 包内).

Kotlin 类和接口导入 Objective-C 时会加上名称前缀. 前缀由框架名称决定.

Objective-C 不支持框架内的包. 如果 Kotlin 编译器发现同一个框架内的不同包下存在同名的 Kotlin 类, Kotlin 编译器会对类重命名. 这个算法还未稳定, 在不同的 Kotlin 发布版中可能发生变化. 要绕过这个问题, 你可以将框架内发生名称冲突的 Kotlin 类重命名.

如果要避免对 Kotlin 声明的重新命名, 请使用 @ObjCName 注解. 这个注解会指示 Kotlin 编译器对类, 接口, 以及其他 Kotlin 元素使用自定义的 Objective-C 和 Swift 名称:

@ObjCName(swiftName = "MySwiftArray")
class MyKotlinArray {
    @ObjCName("index")
    fun indexOf(@ObjCName("of") element: String): Int = TODO()
}

// ObjCName 注解的使用示例
let array = MySwiftArray()
let index = array.index(of: "element")

使用这个注解需要 使用者同意(Opt-in).

初始化器(Initializer)

Swift/Objective-C 初始化器导入 Kotlin 时会成为构造器. 对于 Objective-C category 中声明的初始化器, 或声明为 Swift extension 的初始化器, 导入 Kotlin 时会成为名为 create 的工厂方法, 因为 Kotlin 没有扩展构造器的概念.

Kotlin 构造器导入 Swift/Objective-C 时会成为初始化器.

设值方法(Setter)

Objective-C 中可写的属性如果覆盖超类中的只读属性, 对于 foo 属性会表示为 setFoo() 方法. 对于一个协议(protocol)的只读属性, 如果实现为可变的属性, 那么也是同样的规则.

顶层函数和属性

Kotlin 的顶层函数和属性, 可以通过某个特殊类的成员来访问. 每个 Kotlin 源代码文件都会被翻译为一个这样的类. 比如:

// MyLibraryUtils.kt
package my.library

fun foo() {}

在 Swift 中可以这样调用:

MyLibraryUtilsKt.foo()

方法名称翻译

通常来说, Swift 的参数标签和 Objective-C 的 selector 会被映射为 Kotlin 的参数名称. 但这两种概念还是存在一些语义上的区别, 因此有时 Swift/Objective-C 方法导入时可能导致 Kotlin 中的签名冲突. 这时, 发生冲突的方法可以在 Kotlin 使用命名参数来调用, 比如:

[player moveTo:LEFT byMeters:17]
[player moveTo:UP byInches:42]

在 Kotlin 中, 应该这样调用:

player.moveTo(LEFT, byMeters = 17)
player.moveTo(UP, byInches = 42)

kotlin.Any 的方法 (equals(), hashCode()toString()), 在 Objective-C 中被映射为方法 isEquals:, hashdescription, 在 Swift 被映射为方法 isEquals(_:) 和属性 hash, description.

你可以在 Swift 或 Objective-C 中指定一个更加符合使用习惯的名称, 而不是对 Kotlin 声明自动重命名. 请使用 @ObjCName 注解, 指示 Kotlin 编译器对方法或参数使用自定义的 Objective-C 和 Swift 名称.

使用这个注解需要 使用者同意(Opt-in).

错误与异常

Kotlin 中不存在受控异常(Checked Exception)的概念, 所有的 Kotlin 异常都是不受控的. Swift 则只有受控错误. 因此如果 Swift 或 Objective-C 的代码调用一个 Kotlin 方法, 这个方法抛出一个需要被处理的异常, 那么 Kotlin 方法应该使用 @Throws 注解, 指明一组 "期待的" 异常类.

编译为 Objective-C/Swift 框架时, 非-suspend 的函数如果拥有或继承了 @Throws 注解, 在 Objective-C 中会被表示为产生 NSError* 的方法, 在 Swift 中会被表示为 throws 方法. suspend 函数的表达中, 在它的 completion handler 中一定会有 NSError*/Error 参数.

如果从 Swift/Objective-C 代码调用的 Kotlin 函数中抛出异常, 而且这个异常是 @Throws 注解指定的异常类(或其子类)的实例, 那么这个异常会被转换为 NSError. 其他 Kotlin 异常到达 Swift/Objective-C 代码后, 会被认为是未处理的错误, 并导致程序终止.

没有 @Throws 注解的 suspend 函数, 只会把 CancellationException 异常变换为 NSError. 没有 @Throws 注解的非-suspend 函数, 则完全不会传播 Kotlin 的异常.

注意, 反过来的翻译目前还未实现: Swift/Objective-C 中抛出 error 的方法, 导入 Kotlin 时不会成为抛出异常的方法.

枚举类

Kotlin 枚举类会被导入为 Objective-C 中的 @interface, 以及 Swift 中的 class. 这些数据结构拥有与各个枚举值相对应的属性. 对于下面的 Kotlin 代码:

// Kotlin
enum class Colors {
    RED, GREEN, BLUE
}

在 Swift 中, 你可以这样访问这个枚举类的属性:

// Swift
Colors.red
Colors.green
Colors.blue

要在 Swift 的 switch 语句中使用 Kotlin 枚举类型的变量, 需要提供一个 default 语句, 以避免发生编译错误:

switch color {
    case .red: print("It's red")
    case .green: print("It's green")
    case .blue: print("It's blue")
    default: fatalError("No such color")
}

挂起函数

从 Swift 代码中 以 async 方式调用 suspend函数是 实验性功能. 它随时有可能变更或被删除. 请注意, 只为评估和试验目的来使用这个功能. 希望你能通过我们的 问题追踪系统 提供你的反馈意见.

Kotlin 的 挂起函数 (suspend) 在生成的 Objective-C 头文件中表达为带有回调的函数, 或用 Swift/Objective-C 术语称为 completion handlers.

从 Swift 5.5 开始, Kotlin 的 suspend 函数也可以从 Swift 代码中以 async 函数的方式调用, 而不需要使用 completion handler. 目前, 这个功能还处于非常初始的实验阶段, 存在很多限制. 详情请参见 这个 YouTrack issue.

更多详情请参见 关于 async/await 机制的 Swift 文档.

扩展与 Category 成员

Objective-C Category 的成员, 以及 Swift extension 的成员, 导入 Kotlin 时通常会变成扩展函数. 因此这些声明在 Kotlin 中不能被覆盖. 另外, extension 初始化器 在 Kotlin 中不会成为类的构造器.

目前有两种例外情况. 从 Kotlin 1.8.20 开始, 在 NSView 类 (来自 AppKit 框架) 或 UIView 类 (来自 UIKit 框架) 的相同的头文件中声明的 Category 的成员, 会被导入为这些类的成员. 因此你可以覆盖从 NSView 或 UIView 继承的子类的方法.

对 "通常的" Kotlin 类的 Kotlin 扩展, 导入 Swift 和 Objective-C 后, 分别会成为扩展和 category 成员. 对其他类型的 Kotlin 扩展, 会被当作 顶层声明 处理, 带有额外的接受者参数. 这些类型包括:

  • Kotlin String 类型
  • Kotlin 集合类型, 及其子类型
  • Kotlin interface 类型
  • Kotlin 基本类型(primitive type)
  • Kotlin inline
  • Kotlin Any 类型
  • Kotlin 函数类型, 及其子类型
  • Objective-C 类和协议(protocol)

Kotlin 单子(singleton)

Kotlin 单子(singleton) (通过 object 声明产生, 包括 companion object) 导入 Swift/Objective-C 会成为一个类, 但它只有唯一一个实例.

这个实例可以通过 sharedcompanion 属性来访问.

对于下面的 Kotlin 代码:

object MyObject {
    val x = "Some value"
}

class MyClass {
    companion object {
        val x = "Some value"
    }
}

可以通过以下方式访问这些对象:

MyObject.shared
MyObject.shared.x
MyClass.companion
MyClass.Companion.shared

通过 Objective-C 的 [MySingleton mySingleton] 和 Swift 的 MySingleton() 访问对象, 这个功能已被废弃.

NSNumber

Kotlin 基本类型的装箱类会被映射为 Swift/Objective-C 中的特殊类. 比如, kotlin.Int 装箱类在 Swift 中会被表达为 KotlinInt 类的实例 (或 Objective-C 中的 ${prefix}Int 类的实例, 其中 prefix 是框架名称前缀). 这些类都继承自 NSNumber, 因此它们的实例都是 NSNumber, 也支持 NSNumber 上的所有的操作.

NSNumber 类型用做 Swift/Objective-C 的参数类型或返回值类型时, 不会自动翻译为 Kotlin 的基本类型. 原因是, NSNumber 类型没有提供足够的信息, 指明它内部包装的基本值类型是什么, 也就是说, 通过 NSNumber 我们无法知道它究竟是 Byte, Boolean, 还是 Double. 因此 Kotlin 基本类型与 NSNumber 类型的相互转换必须手工进行 (详情请参见 下文).

NSMutableString

Objective-C 的 NSMutableString 类在 Kotlin 中无法使用. NSMutableString 所有实例在传递给 Konlin 之前都会被复制一次.

集合

Kotlin 集合会被转换为 Swift/Objective-C 的集合类型, 对应关系请参见上表. Swift/Objective-C 的集合也会以同样的方式映射为 Kotlin 的集合类型, 但 NSMutableSetNSMutableDictionary 除外. NSMutableSet 不会转换为 Kotlin 的 MutableSet. 要创建一个 Kotlin MutableSet 类型的对象, 你可以明确地创建这个 Kotlin 集合类型的实例, 要么在 Kotlin 中创建, 比如使用 mutableSetOf() 方法, 或者在 Swift 中使用 KotlinMutableSet 类创建 (或者在 Objective-C 中使用 ${prefix}MutableSet 类, 其中 prefix 是框架名称前缀). 对于 MutableMap 类型也是如此.

Function 类型

Kotlin 的函数类型对象 (比如 Lambda 表达式) 会被转换为 Swift 函数 或 Objective-C 代码段(block). 但是, 在翻译函数和函数类型时, 对于参数类型和返回值类型的映射方法存在区别. 对于函数类型, 基本类型映射为它们的装箱类. Kotlin 的 Unit 返回值类型在 Swift/Objective-C 中会被表达为对应的 Unit 单子. 这个单子的值可以象其他任何 Kotlin object 一样, 通过相同的方式得到(参见上表中的单子). 综合起来的结果就是:

fun foo(block: (Int) -> Unit) { ... }

在 Swift 中会成为:

func foo(block: (KotlinInt) -> KotlinUnit)

调用方法是:

foo {
    bar($0 as! Int32)
    return KotlinUnit()
}

泛型

Objective-C 支持类上定义的 "轻量的泛型", 支持的功能相对有限. Swift 可以导入类上定义的泛型, 向编译器提供额外的类型信息.

Objective-C 和 Swift 对泛型功能的支持与 Kotlin 不同, 因此翻译过程不可避免的将会丢失部分信息, 但支持的那部分功能还能保留有意义的信息.

功能限制

Objective-C 泛型不支持 Kotlin 或 Swift 的全部特性, 因此在翻译过程中会有一些信息丢失.

泛型只能定义在类上, 而不能用于接口 (也就是 Objective-C 和 Swift 中的协议(protocol)), 也不能用于函数.

可空性(Nullability)

Kotlin 和 Swift 都把可空性(Nullability)的定义作为类型信息的一部分, 而 Objective-C 则在一个类型的方法或属性上定义可空性. 因此, 下面的代码:

class Sample<T>() {
  fun myVal(): T
}

(逻辑上)将会变成这样:

class Sample<T>() {
  fun myVal(): T?
}

为了支持可以为 null 的类型, Objective-C 头文件需要将 myVal 的返回值定义为可为 null.

为了减轻这个问题, 定义你的泛型类时, 如果泛型类型 绝对不会 为 null, 需要提供一个非-null 的类型约束(type constraint):

class Sample<T : Any>() {
  fun myVal(): T
}

这样将会强制要求 Objective-C 头文件将 myVal 标记为非-null.

类型变异(Variance)

Objective-C 允许泛型声明为协变(covariant), 或反向类型变异(contravariant). Swift 不支持类型变异(Variance). 如果需要, 对来自 Objective-C 的泛型类, 可以进行强制类型转换.

data class SomeData(val num: Int = 42) : BaseData()
class GenVarOut<out T : Any>(val arg: T)
let variOut = GenVarOut<SomeData>(arg: sd)
let variOutAny : GenVarOut<BaseData> = variOut as! GenVarOut<BaseData>

类型约束

在 Kotlin 中, 你可以对泛型类型指定上界(Upper Bound). Objective-C 也支持这种功能, 但不能用于更复杂的情况, 而且在 Kotlin - Objective-C 交互中, 目前也不支持. 例外是, 上界(Upper Bound)指定为非-null, 会使得 Objective-C 方法/属性变为非-null.

关闭泛型功能

如要想要框架头文件不使用泛型, 需要在编译器配置中添加以下参数:

binaries.framework {
    freeCompilerArgs += "-Xno-objc-generics"
}

在映射的类型之间进行变换

编写 Kotlin 代码时, 对象可能需要从 Kotlin 类型转换为等价的 Swift/Objective-C 类型 (或者反过来). 这种情况下, 可以直接使用传统的 Kotlin 类型转换, 比如:

val nsArray = listOf(1, 2, 3) as NSArray
val string = nsString as String
val nsNumber = 42 as NSNumber

类继承

在 Swift/Objective-C 中继承 Kotlin 类和接口

Swift/Objective-C 类和 protocol 可以继承 Kotlin 类和接口.

在 Kotlin 中继承 Swift/Objective-C 类和接口

Kotlin 的 final class 可以继承 Swift/Objective-C 类和 protocol. 目前还不支持非 final 的 Kotlin 类继承 Swift/Objective-C 类型, 因此不可能声明一个复杂的类层级, 同时又继承 Swift/Objective-C 类型.

可以使用 Kotlin 的 override 关键字来覆盖通常的方法. 这种情况下, 子类方法的参数名称, 必须与被覆盖的方法相同.

有时我们会需要覆盖初始化器, 比如, 继承 UIViewController 时. 初始化器会被导入成为 Kotlin 中的构造器, 它可以被 Kotlin 中使用了 @OverrideInit 注解的构造器覆盖:

class ViewController : UIViewController {
    @OverrideInit constructor(coder: NSCoder) : super(coder)

    ...
}

子类构造器的参数名称和类型, 必须与被覆盖的构造器相同.

如果多个方法在 Kotlin 中发生了签名冲突, 要覆盖这些方法, 你可以在类上添加 @Suppress("CONFLICTING_OVERLOADS") 注解.

压制 Kotlin 签名冲突错误, 是一种临时的替代方法. 这样的情况下不能保证稳定性, 因此要小心使用. 我们将会在未来的 Kotlin 发布版本中解决这个问题.

Kotlin/Native 默认不会允许通过 super(...) 构造器来调用 Objective-C 的非指定(non-designated)初始化器. 如果在 Objective-C 库中没有正确地标注出指定的(designated)初始化器, 那么这种限制可能会造成我们的不便. 可以在这个库的 .def 文件中添加一个 disableDesignatedInitializerChecks = true 设定, 来关闭编译器的这个检查.

C 语言功能

请参见 与 C 代码交互, 其中有一些示例程序, 其中的库使用了某些 C 语言功能, 比如, 不安全的指针, 结构(struct), 等等.

将 KDoc 注释导出到生成的 Objective-C 头文件

KDoc 注释导出到生成的 Objective-C 头文件是 实验性功能. 它随时有可能变更或被删除. 需要使用者同意(Opt-in) (详情见下文), 而且你应该只为评估目的来使用这个功能. 希望你能通过我们的 问题追踪系统 提供你的反馈意见.

默认情况下, 在生成 Objective-C 头文件时, KDocs 文档注释不会被翻译为头文件中对应的注释.
例如, 以下带 KDoc 文档的 Kotlin 代码:

/**
 * Prints the sum of the arguments.
 * Properly handles the case when the sum doesn't fit in 32-bit integer.
 */
fun printSum(a: Int, b: Int) = println(a.toLong() + b)

会生成 Objective-C 声明, 没有任何注释:

+ (void)printSumA:(int32_t)a b:(int32_t)b __attribute__((swift_name("printSum(a:b:)")));

要启用 KDoc 注释导出功能, 请在你的 build.gradle(.kts) 添加以下编译器选项:

kotlin {
    targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
        compilations.get("main").compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc")
    }
}
kotlin {
    targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget) {
        compilations.get("main").compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc")
    }
}

这样设置之后, Objective-C 头文件将包含对应的注释:

/**
 * Prints the sum of the arguments.
 * Properly handles the case when the sum doesn't fit in 32-bit integer.
 */
+ (void)printSumA:(int32_t)a b:(int32_t)b __attribute__((swift_name("printSum(a:b:)")));

已知的限制:

  • 依赖项的文档不会导出, 除非它本身也使用 -Xexport-kdoc 选项来编译. 这个功能还是实验性功能, 因此使用这个选项编译的库可能与其他编译器版本不兼容.
  • 绝大多数 KDoc 注释会 "保持原状" 导出. 很多 KDoc 功能(例如, @property)不支持.

不支持的特性

Kotlin 编程语言的一些特性目前还没有映射为 Objective-C 或 Swift 中对应的特性. 目前, 在生成的框架头文件中, 以下特性还不能正确地导出:

  • 内联类(inline class) (参数会被映射为底层的基本类型, 或 id)
  • 实现标准的 Kotlin 集合接口 (List, Map, Set) 的自定义类, 以及其他特殊的类
  • Objective-C 类的 Kotlin 子类