7.3.1.2 把 Kotlin/Native 作为 Apple framework – 教程

原文链接: https://kotlinlang.org/docs/apple-framework.html

7.3.1.2 把 Kotlin/Native 作为 Apple framework – 教程

注意: Objective-C 库导入处于 Beta 阶段。由 cinterop 工具从 Objective-C 库生成的所有 Kotlin 声明都应带有 @ExperimentalForeignApi 注解。随 Kotlin/Native 提供的原生平台库(例如 Foundation、UIKit 和 POSIX)只对部分 API 要求选择启用。

Kotlin/Native 提供与 Swift/Objective-C 的双向互操作。你既可以在 Kotlin 代码中使用 Objective-C 框架和库,也可以在 Swift/Objective-C 代码中使用 Kotlin 模块。

Kotlin/Native 附带了一组预先导入的系统框架;也可以导入现有的框架并在 Kotlin 中使用它。在本教程中,你将学习如何创建自己的框架,并在 macOS 和 iOS 上的 Swift/Objective-C 应用中使用 Kotlin/Native 代码。

在本教程中,你将:

你可以使用命令行直接生成 Kotlin framework,也可以通过脚本文件(例如 .sh 或 .bat 文件)来完成。不过,对于包含数百个文件和库的较大项目,这种方式扩展性不好。使用构建系统可以下载并缓存 Kotlin/Native 编译器二进制文件及其传递依赖的库,并负责运行编译器和测试,从而简化整个过程。Kotlin/Native 可以通过 Kotlin Multiplatform 插件使用 Gradle 构建系统。

注意: 如果你使用 Mac,并且想为 iOS 或其他 Apple 目标创建并运行应用,还需要先安装 Xcode Command Line Tools、启动它并接受许可条款。

创建 Kotlin 库

提示: 关于详细的第一步以及如何创建新的 Kotlin/Native 项目并在 IntelliJ IDEA 中打开它,请参阅 Kotlin/Native 入门教程。

Kotlin/Native 编译器可以从 Kotlin 代码为 macOS 和 iOS 生成 framework。创建出的 framework 包含在 Swift/Objective-C 中使用它所需的全部声明和二进制文件。

我们先创建一个 Kotlin 库:

  1. 在 src/nativeMain/kotlin 目录中,创建包含该库内容的 lib.kt 文件:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
   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!" }
  1. 用以下内容更新你的 build.gradle(.kts) Gradle 构建文件:

Kotlin

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

    plugins {
        kotlin("multiplatform") version "2.4.20"
    }

    repositories {
        mavenCentral()
    }

    kotlin {
        iosArm64()
        // macosArm64()
        // iosSimulatorArm64()

        targets.withType<KotlinNativeTarget>().configureEach {
            binaries {
                framework {
                    baseName = "Demo"
                }
            }
        }
    }

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

Groovy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

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

    repositories {
        mavenCentral()
    }

    kotlin {
        iosArm64()
        // macosArm64()
        // iosSimulatorArm64()

        targets.withType(KotlinNativeTarget).configureEach {
            binaries {
                framework {
                    baseName = "Demo"
                }
            }
        }
    }

    wrapper {
        gradleVersion = "9.7.0"
        distributionType = "ALL"
    }

binaries {} 块把项目配置为生成动态库(共享库)。

Kotlin/Native 支持 iOS 的 iosArm64 和 iosSimulatorArm64 目标,以及 macOS 的 macosArm64 目标。因此,你可以把 iosArm64() 替换为目标平台对应的 Gradle 函数:

| 目标/设备 | Gradle 函数 |

| macOS ARM64 | macosArm64() | | iOS ARM64 | iosArm64() | | iOS Simulator (ARM64) | iosSimulatorArm64() |

关于其他受支持的 Apple 目标的信息,请参阅 Kotlin/Native 目标支持。

  1. 要构建该 framework,请在你的 IDE 中运行 linkDebugFramework<YourTargetName> Gradle 任务,或在终端中使用控制台命令,例如:
1
   ./gradlew linkDebugFrameworkIosArm64

构建会把 framework 生成到 build/bin/<yourTargetName>/debugFramework 目录中。

提示: 你也可以使用通用的 link<YourTargetName> Gradle 任务同时生成 framework 的 debug 和 release 两种变体。

生成的 framework 头文件

每个 framework 变体都包含一个头文件。这些头文件不依赖于目标平台。头文件包含你的 Kotlin 代码的定义,以及少量 Kotlin 全局声明。我们来看看其中有什么。

Kotlin/Native 运行时声明

在 build/bin/<yourTargetName>/debugFramework/Demo.framework/Headers 目录中,打开 Demo.h 头文件。看一看 Kotlin 运行时声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
NS_ASSUME_NONNULL_BEGIN
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunknown-warning-option"
#pragma clang diagnostic ignored "-Wincompatible-property-type"
#pragma clang diagnostic ignored "-Wnullability"

#pragma push_macro("_Nullable_result")
#if !__has_feature(nullability_nullable_result)
#undef _Nullable_result
#define _Nullable_result _Nullable
#endif

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

@interface DemoBase (DemoBaseCopying) <NSCopying>
@end

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

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

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

Kotlin 类在 Swift/Objective-C 中有一个 KotlinBase 基类,它在那里继承自 NSObject 类。集合和异常也有对应的包装类型。大多数集合类型会映射到 Swift/Objective-C 中相似的集合类型:

| Kotlin | Swift | Objective-C |

| List | Array | NSArray | | MutableList | NSMutableArray | NSMutableArray | | Set | Set | NSSet | | MutableSet | NSMutableSet | NSMutableSet | | Map | Dictionary | NSDictionary | | MutableMap | NSMutableDictionary | NSMutableDictionary |

Kotlin 数值类型与 NSNumber

Demo.h 文件的下一部分包含 Kotlin/Native 数值类型与 NSNumber 之间的类型映射。基类在 Objective-C 中称为 DemoNumber,在 Swift 中称为 KotlinNumber。它继承自 NSNumber。

每个 Kotlin 数值类型都有一个对应的预定义子类:

| Kotlin | Swift | Objective-C | 简单类型 |

| - | 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 |

每个数值类型都有一个类方法,用于从相应的简单类型创建新实例。此外还有一个实例方法可以把简单值取回。从形式上看,所有这些声明都类似这样:

1
2
3
4
5
__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 数值类型映射到 Swift/Objective-C。在 Swift 中,你可以调用构造器来创建实例,例如 KotlinLong(value: 42)。

来自 Kotlin 的类和对象

我们来看看 class 和 object 如何映射到 Swift/Objective-C。生成的 Demo.h 文件包含 Class、Interface 和 Object 的确切定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__attribute__((swift_name("Interface")))
@protocol DemoInterface
@required
- (void)iMember __attribute__((swift_name("iMember()")));
@end

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

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

这段代码中的 Objective-C 属性有助于从 Swift 和 Objective-C 两种语言使用该 framework。DemoInterface、DemoClazz 和 DemoObject 分别是为 Interface、Clazz 和 Object 创建的。

Interface 被转换为 @protocol,而 class 和 object 都表示为 @interface。Demo 前缀来自 framework 名称。可空返回类型 ULong? 在 Objective-C 中变为 DemoULong。

来自 Kotlin 的全局声明

Kotlin 中的所有全局函数在 Objective-C 中变为 DemoLibKt,在 Swift 中变为 LibKt,其中 Demo 是 kotlinc-native 的 -output 参数所设置的 framework 名称:

1
2
3
4
5
6
7
8
9
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("LibKt")))
@interface DemoLibKt : DemoBase
+ (NSString * _Nullable)acceptFunF:(NSString * _Nullable (^)(NSString *))f __attribute__((swift_name("acceptFun(f:)")));
+ (void)forFloatsF:(float)f d:(DemoDouble * _Nullable)d __attribute__((swift_name("forFloats(f:d:)")));
+ (void)forIntegersB:(int8_t)b s:(uint16_t)s i:(int32_t)i l:(DemoULong * _Nullable)l __attribute__((swift_name("forIntegers(b:s:i:l:)")));
+ (NSString *)stringsStr:(NSString * _Nullable)str __attribute__((swift_name("strings(str:)")));
+ (NSString * _Nullable (^)(NSString *))supplyFun __attribute__((swift_name("supplyFun()")));
@end

Kotlin 的 String 与 Objective-C 的 NSString* 是透明映射的。类似地,Kotlin 的 Unit 类型映射为 void。原始类型是直接映射的。不可为空的原始类型是透明映射的。可为空的原始类型映射为 Kotlin<TYPE>* 类型,如表格所示。高阶函数 acceptFunF 和 supplyFun 都被包含进来,并接受 Objective-C block。

关于类型映射的更多信息,请参阅与 Swift/Objective-C 互操作。

垃圾回收与引用计数

Swift 和 Objective-C 使用自动引用计数(ARC)。Kotlin/Native 有自己的垃圾回收器,它同样与 Swift/Objective-C ARC 集成。

不再使用的 Kotlin 对象会被自动移除。你无需采取额外步骤来从 Swift 或 Objective-C 控制 Kotlin/Native 实例的生命周期。

在 Objective-C 中使用代码

我们来看看从 Objective-C 调用该 framework。在 framework 目录中,创建包含以下代码的 main.m 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import <Foundation/Foundation.h>
#import <Demo/Demo.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [DemoObject.shared 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 name>.shared 类属性,它让你可以获取该对象的唯一实例并在其上调用对象方法。

创建 Clazz 类实例时使用了常见的模式。你在 Objective-C 中调用 [[ DemoClazz alloc] init]。对于没有参数的构造器,也可以使用 [DemoClazz new]。

Kotlin 源码中的全局声明在 Objective-C 中位于 DemoLibKt 类下。所有 Kotlin 函数都被转换为该类的类方法。

strings 函数在 Objective-C 中变为 DemoLibKt.stringsStr 函数,因此你可以直接传 NSString 给它。返回值同样表现为 NSString。

在 Swift 中使用代码

你生成的 framework 带有辅助属性,使其更容易在 Swift 中使用。我们把前面的 Objective-C 示例转换为 Swift。

在 framework 目录中,创建包含以下代码的 main.swift 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import Foundation
import Demo

let kotlinObject = Object.shared

let field = Object.shared.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.shared 语法用于访问这个唯一实例。

Kotlin 的函数和属性名称会按原样转换。Kotlin 的 String 变为 Swift 的 String。Swift 也隐藏了 NSNumber* 装箱。你还可以把 Swift 闭包传给 Kotlin,并从 Swift 调用 Kotlin lambda 函数。

关于类型映射的更多信息,请参阅与 Swift/Objective-C 互操作。

把 framework 连接到你的 iOS 项目

现在你可以把生成的 framework 作为依赖连接到你的 iOS 项目。设置并自动化这一过程有多种方式,请选择最适合你的方法:

选择 iOS 集成方式

接下来做什么