改进翻译

Kotlin/Native 开发 Apple Framework

作者 Eugene Petrenko,乔禹昂(翻译)
最近更新 2019-04-15
编译 Kotlin/Native 代码并在 Objective-C 与 Swift 中使用它

Kotlin/Native 提供与 Objective-C/Swift 的双向互操作性。 Objective-C framework 与库可以在 Kotlin 代码中使用。 Kotlin 模块同样可以在 Swift/Objective-C 代码中使用。 除此之外,Kotlin/Native 也拥有 C 互操作性。 这篇 Kotlin/Native 开发动态库教程包含了更多信息。

在本教程中,我们将看到如何在 Objective-C 与 Swift 编写的 macOS 与 iOS 应用程序中使用 Kotlin/Native 代码。 我们将使用 Kotlin 代码构建一个 framework。

在本教程中我们将:

创建一个 Kotlin 库

Kotlin/Native 编译器可以使 Kotlin 代码为 macOS 与 iOS 生产一个 framework 的输出。生成的 framework 包含在 Objective-C 与 Swift 中所有使用所需的声明与二进制文件。 理解这项技术的最佳方式是自己进行一下尝试。 我们首先来创建一个小型的 Kotlin 库,并在 Objective-C 程序中使用它。

我们创建该 hello.kt 文件,并在其中编写库的内容:

package example

object Object {
  val field = "A"
}

interface Interface {
  fun iMember() {}
}

class Clazz : Interface {
  fun member(p: Int): ULong? = 42UL
}

fun forIntegers(b: Byte, s: UShort, i: Int, l: ULong?) { }
fun forFloats(f: Float, d: Double?) { }

fun strings(str: String?) : String {
  return "That is '$str' from C"
}

fun acceptFun(f: (String) -> String?) = f("Kotlin/Native rocks!")
fun supplyFun() : (String) -> String? = { "$it is cool!" }

虽然可以使用命令行或直接通过将它与脚本文件(即 sh 或 bat 文件)相结合,但我们应该注意到, 对于拥有数百个文件以及库的大型项目来说,这不能很好地扩展。 所以最好使用附带构建系统的 Kotlin/Native 编译器,它可以帮助下载与缓存 Kotlin/Native 编译器二进制文件与库传递依赖,以及运行该编译器并测试。 Kotlin/Native 可以通过 kotlin 多平台插件来使用 Gradle 构建系统。

基本 Kotlin/Native 应用程序这篇教程涵盖了使用 Gradle 创建 IDE 兼容工程的基础知识。如果你正在寻找关于第一步的更多细节以及如何开始一个新的 Kotlin/Native 项目并在 IntelliJ IDEA 中打开它的说明,则请你阅读这篇教程,我们将看到关于在 Kotlin/Native 中进行高级的 C 互操作的相关用法以及使用 multiplatform(Kotlin 多平台插件)及 Gradle 进行构建。

首先,让我们创建一个工程目录。在本教程中的所有路径都是相对于这个目录的。有时在添加任何新文件之前,都必须创建缺少的目录。

我们将使用下面的 build.gradle build.gradle.kts Gradle 构建文件并添加以下内容:

plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}

repositories {
    mavenCentral()
}

kotlin {
  macosX64("native") {
    binaries {
      framework {
        baseName = "Demo"
      }
    }
  }
}

wrapper {
  gradleVersion = "5.3.1"
  distributionType = "ALL"
}
plugins {
    kotlin("multiplatform") version "1.3.21"
}

repositories {
    mavenCentral()
}

kotlin {
  macosX64("native") {
    binaries {
      framework {
        baseName = "Demo"
      }
    }
  }
}

tasks.wrapper {
  gradleVersion = "5.3.1"
  distributionType = Wrapper.DistributionType.ALL
}

已经准备好的工程源代码可以在这里直接下载: GitHub GitHub

我们将源文件移动到工程下的 src/nativeMain/kotlin 文件夹。当使用 kotlin-多平台插件的时候这是定位文件的默认路径。 使用插件。我们使用以下代码块来指示配置项目为我们生成动态或共享库:

binaries {
  framework {
    baseName = "Demo"
  }  
}

macOS X64 是单独的,Kotlin/Native 支持 iOS arm32arm64X64 目标平台。我们可以使用各自的函数替代 macosX64, 如表所示:

目标 平台/设备 Gradle 函数
macOS x86_64 macosX64()
iOS ARM 32 iosArm32()
iOS ARM 64 iosArm64()
iOS Simulator (x86_64) iosX64()

我们运行 linkNative Gradle 任务以在 IDE 中构建该库, 或者使用如下的控制台命令:

./gradlew linkNative
./gradlew linkNative
gradlew.bat linkNative

依据配置的不同,构建生成的 framework 位于 build/bin/native/debugFrameworkbuild/bin/native/releaseFramework 文件夹。 我们来看看里面是什么

生成 Framework 头文件

每个创建的 framework 头文件都包含在 <Framework>/Headers/Demo.h 中。 这个头文件不依赖目标平台(至少需要 Kotlin/Native v.0.9.2)。 它包含我们的 Kotlin 代码的定义与一些 Kotlin 级的声明。

注意,Kotlin/Native 导出符号的方式如有变更,恕不另行通知。

Kotlin/Native 运行时声明

我们首先来看看 Kotlin 的运行时声明:

NS_ASSUME_NONNULL_BEGIN

@interface KotlinBase : NSObject
- (instancetype)init __attribute__((unavailable));
+ (instancetype)new __attribute__((unavailable));
+ (void)initialize __attribute__((objc_requires_super));
@end;

@interface KotlinBase (KotlinBaseCopying) <NSCopying>
@end;

__attribute__((objc_runtime_name("KotlinMutableSet")))
__attribute__((swift_name("KotlinMutableSet")))
@interface DemoMutableSet<ObjectType> : NSMutableSet<ObjectType>
@end;

__attribute__((objc_runtime_name("KotlinMutableDictionary")))
__attribute__((swift_name("KotlinMutableDictionary")))
@interface DemoMutableDictionary<KeyType, ObjectType> : NSMutableDictionary<KeyType, ObjectType>
@end;

@interface NSError (NSErrorKotlinException)
@property (readonly) id _Nullable kotlinException;
@end;

Kotlin 类在 Objective-C 中拥有一个 KotlinBase 基类,该类在这里继承自 NSObject 类。我们同样也有集合与异常的包装器。 大多数的集合类型都从另一边映射到了相似的集合类型:

Kotlin Swift Objective-C
List Array NSArray
MutableList NSMutableArray NSMutableArray
Set Set NSSet
Map Dictionary NSDictionary
MutableMap NSMutableDictionary NSMutableDictionary

Kotlin Numbers 与 NSNumber

下一步,<Framework>/Headers/Demo.h 包含了 Kotlin/Native 数字类型与 NSNumber 之间的映射。我们在 Objective-C 中拥有一个基类名为 DemoNumber, 而在 Swift 中是 KotlinNumber。它继承自 NSNumber。 这里有每个作为子类的 Kotlin 数字类型:

Kotlin Swift Objective-C Simple type
- KotlinNumber <Package>Number -
Byte KotlinByte <Package>Byte char
UByte KotlinUByte <Package>UByte unsigned char
Short KotlinShort <Package>Short short
UShort KotlinUShort <Package>UShort unsigned short
Int KotlinInt <Package>Int int
UInt KotlinUInt <Package>UInt unsigned int
Long KotlinLong <Package>Long long long
ULong KotlinULong <Package>ULong unsigned long long
Float KotlinFloat <Package>Float float
Double KotlinDouble <Package>Double double
Boolean KotlinBoolean <Package>Boolean BOOL/Bool

每个数字类型都有一个类方法,用于从相关的简单类型创建新实例。此外,还有一个实例方法用于提取一个简单的值。原理上,声明看起来像这样:

__attribute__((objc_runtime_name("Kotlin__TYPE__")))
__attribute__((swift_name("Kotlin__TYPE__")))
@interface Demo__TYPE__ : DemoNumber
- (instancetype)initWith__TYPE__:(__CTYPE__)value;
+ (instancetype)numberWith__TYPE__:(__CTYPE__)value;
@end;

其中 __TYPE__ 是简单类型的名称之一,而 __CTYPE__ 是相关的 Objective-C 类型,例如 initWithChar(char)

这些类型用于将装箱的 Kotlin 数字类型映射到 Objective-C 与 Swift。 在 Swift 中,我们可以简单的调用构造函数来创建一个示例,例如 KotlinLong(value: 42)

Kotlin 中的类与对象

我们来看看如何将 classobject 映射到 Objective-C 与 Swift。 生成的 <Framework>/Headers/Demo.h 文件包含 ClassInterfaceObject 的确切定义:

NS_ASSUME_NONNULL_BEGIN

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Object")))
@interface DemoObject : KotlinBase
+ (instancetype)alloc __attribute__((unavailable));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
+ (instancetype)object __attribute__((swift_name("init()")));
@property (readonly) NSString *field;
@end;

__attribute__((swift_name("Interface")))
@protocol DemoInterface
@required
- (void)iMember __attribute__((swift_name("iMember()")));
@end;

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Clazz")))
@interface DemoClazz : KotlinBase <DemoInterface>
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (DemoLong * _Nullable)memberP:(int32_t)p __attribute__((swift_name("member(p:)")));
@end;

这段代码有各种 Objective-C attribute,旨在提供在 Objective-C 与 Swift 语言中使用该 framework 的帮助。 DemoClazzDemoInterfaceDemoObject 被分别创建为 ClazzInterfaceObjectInterface 被转换为 @protocol,同样 classobject 都以 @interface 表示。 Demo 前缀来自于 kotlinc-native 编译器的 -output 参数与 framework 的名称。 我们看到这里的可空的返回值类型 ULong? 被转换到 Objective-C 中的 DemoLong*

Kotlin 中的全局声明

Demo 作为 framework 名称的地方且为 kotlinc-native 设置了 -output 参数时, 所有 Kotlin 中的全局声明都被转化为 Objective-C 中的 DemoLibKt 以及 Swift 中的 LibKt

NS_ASSUME_NONNULL_BEGIN

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("LibKt")))
@interface DemoLibKt : KotlinBase
+ (void)forIntegersB:(int8_t)b s:(int16_t)s i:(int32_t)i l:(DemoLong * _Nullable)l __attribute__((swift_name("forIntegers(b:s:i:l:)")));
+ (void)forFloatsF:(float)f d:(DemoDouble * _Nullable)d __attribute__((swift_name("forFloats(f:d:)")));
+ (NSString *)stringsStr:(NSString * _Nullable)str __attribute__((swift_name("strings(str:)")));
+ (NSString * _Nullable)acceptFunF:(NSString * _Nullable (^)(NSString *))f __attribute__((swift_name("acceptFun(f:)")));
+ (NSString * _Nullable (^)(NSString *))supplyFun __attribute__((swift_name("supplyFun()")));
@end;

我们看到 Kotlin String 与 Objective-C NSString * 是透明映射的。 类似地,Kotlin 的 Unit 类型被映射到 void。我们看到原始类型直接映射。不可空的原始类型透明地映射。 可空的原始类型被映射到 Kotlin<TYPE>* 类型,如表所示。 包括高阶函数 acceptFunFsupplyFun, 都接收一个 Objective-C 块。

更多的关于所有其它类型的映射细节可以在这篇 Objective-C 互操作文档中找到。

垃圾回收与引用计数

Objective-C 与 Swift 使用引用计数。Kotlin/Native 也同样拥有自己的垃圾回收。 Kotlin/Native 的垃圾回收与 Objective-C/Swift 引用计数相集成。我们不需要在 Swift 或 Objective-C 中使用任何其它特别的方式去控制 Kotlin/Native 实例的生命周期。

在 Objective-C 中使用代码

让我们在 Objective-C 中调用代码。为此我们使用下面的内容创建 main.m 文件:

#import <Foundation/Foundation.h>
#import <Demo/Demo.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[DemoObject object] field];
        
        DemoClazz* clazz = [[ DemoClazz alloc] init];
        [clazz memberP:42];
        
        [DemoLibKt forIntegersB:1 s:1 i:3 l:[DemoULong numberWithUnsignedLongLong:4]];
        [DemoLibKt forIntegersB:1 s:1 i:3 l:nil];
        
        [DemoLibKt forFloatsF:2.71 d:[DemoDouble numberWithDouble:2.71]];
        [DemoLibKt forFloatsF:2.71 d:nil];
        
        NSString* ret = [DemoLibKt acceptFunF:^NSString * _Nullable(NSString * it) {
            return [it stringByAppendingString:@" Kotlin is fun"];
        }];
        
        NSLog(@"%@", ret);
        return 0;
    }
}

我们直接在 Objective-C 代码中调用 Kotlin 类。一个 Kotlin object 拥有类方法函数 object,这使我们在想获取对象的唯一实例时调用 object 方法就可以了。 广泛使用的用于创建 Clazz 类实例的模式。我们在 Objective-C 上调用 [[ DemoClazz alloc] init]。我们也可以使用没有参数的构造函数 [DemoClazz new]。 Kotlin 源中的全局声明的作用域位于 Objective-C 中的 DemoLibKt 类之下。 所有的方法都被转化为该类的类方法。 strings 函数转化为 Objective-C 中的 DemoLibKt.stringsStr函数,我们可以给它直接传递 NSString。而返回值也同样可以看作 NSString

在 Swift 中使用代码

这个使用 Kotlin/Native 编译的 framework 拥有辅助 attribute 来使它在 Swift 中的使用更为容易。我们来将之前的 Objective-C 示例覆盖为 Swift。其结果是,我们将在 main.swift 中包含下面的代码:

import Foundation
import Demo

let kotlinObject = Object()
assert(kotlinObject === Object(), "Kotlin object has only one instance")

let field = Object().field

let clazz = Clazz()
clazz.member(p: 42)

LibKt.forIntegers(b: 1, s: 2, i: 3, l: 4)
LibKt.forFloats(f: 2.71, d: nil)

let ret = LibKt.acceptFun { "\($0) Kotlin is fun" }
if (ret != nil) {
  print(ret!)
}

这段 Kotlin 代码被转换为看起来非常相似的 Swift 代码。但是它们有一些小差异。在 Kotlin 中任何 object 只拥有一个实例。Kotlin object Object 现在在 Swift 中拥有一个构造函数,我们使用 Object() 语法来访问它唯一的实例。 在 Swift 中该实例总是相同的,所以 Object() === Object() 为 true。 方法与属性名称按原样转换。即 Kotlin String 被转换为 Swift String。 Swift 同样隐藏了 NSNumber* 的装箱。我们传递 Swift 闭包给 Kotlin, 与在 Swift 中调用 Kotlin lambda 函数是相同的。

更多关于类型映射的信息可以在这篇 Objective-C 互操作文档中找到。

Xcode 与 Framework 依赖

我们需要配置一个 Xcode 工程来使用我们的 framework。这个配置依赖于目标平台。

Xcode 与 MacOS 目标平台

首先,我们需要导入该 framework 到 General 选项的 target 配置。选择 Linked Frameworks and Libraries 选项来导入我们的 framework。这将使 Xcode 查看我们的 framework 并同时解决来自 Objective-C 与 Swift 的导入。

第二步是配置 framework 生产的二进制文件的搜索路径。它也被称作 rpath运行时搜索路径。 二进制文件使用路径来查找所需的 framework。我们不推荐, 在不需要的时候在操作系统中去安装其它 framework。我们应该了解未来应用程序的布局,举例来说, 我们可能在应用程序包下有 Frameworks 文件夹,其中包含我们所使用的所有 framework。 @rpath 参数可以被配置到 Xcode。我们需要打开 project 配置项并找到 Runpath Search Paths 选项。在这里我们指定编译 framework 的相对路径。

Xcode 与 iOS 目标平台

首先,我们需要导入编译好的 framework 到 Xcode 工程。为此我们将 framework 添加到 target 配置页的 General 选项的 Embedded Binaries 块。

第二步是将 framework 路径导入到 target 配置页的 Build Settings 选项下的 Framework Search Paths 块。它可以使用 $(PROJECT_DIR) 宏来简化设置。

iOS 模拟器需要一个为 ios_x64 目标平台编译的 framework,它位于我们案例中的 iOS_sim 文件夹。

这个 Stack Overflow 主题包含了一些更多的建议。同样, CocoaPods 包管理器也可能有助于自动化该过程。

接下来

Kotlin/Native 与 Objective-C 以及 Swift 语言之间拥有双向互操作性。 Kotlin 对象集成了 Objective-C/Swift 的引用计数。Kotlin 对象可以被自动释放。 这篇 Objective-C 互操作文档包含了更多关于互操作实现细节的信息。 当然,也可以导入一个外部的 framework 并在 Kotlin 中使用它。Kotlin/Native 附带一套良好的预导入系统 framework。

Kotlin/Native 同样支持 C 互操作。查看这篇 Kotlin/Native 开发动态库教程,或者查看这篇 C 互操作文档