Kotlin/Native 编译目标默认会被编译输出为 *.klib
库文件, 这种库文件可以被 Kotlin/Native 用作依赖项, 但它不能执行, 也不能被用作一个原生的库.
如果要编译为最终的原生二进制文件, 比如可执行文件, 或共享库, 可以使用原生编译目标的 binaries
属性. 这个属性值是原生二进制文件的列表, 表示除默认的 *.klib
库文件之外, 这个编译目标还需要编译为哪些类型, 这个属性还提供了一组方法, 用来声明和配置这些原生二进制文件.
Kotlin/Native 编译器生成的二进制文件可能包含第三方代码, 数据, 或衍生作品. 也就是说, 如果你发布 Kotlin/Native 编译的最终二进制文件, 那么你始终需要在你的二进制分发版中包含必要的 许可证文件.
声明二进制文件
请使用以下工厂方法来声明 binaries
列表中的元素.
工厂方法 | 二进制文件类型 | 可用于 |
---|
executable
| 产品版的可执行文件 | 所有的原生编译目标 |
test
| 测试程序的可执行文件 | 所有的原生编译目标 |
sharedLib
| Shared 原生库 | 所有的原生编译目标, WebAssembly 除外 |
staticLib
| Static 原生库 | 所有的原生编译目标, WebAssembly 除外 |
framework
| Objective-C 框架 | 仅限于 macOS, iOS, watchOS, 和 tvOS 编译目标 |
最简单的版本不需要任何额外参数, 并对每一个构建类型创建一个二进制文件. 现在有 2 种构建类型:
DEBUG
– 产生一个未经优化的, 带调试信息的二进制文件
RELEASE
– 产生优化过的, 无调试信息的二进制文件
下面的代码会创建 2 个可执行的二进制文件, debug 和 release:
kotlin {
linuxX64 { // 这里请改为你的编译目标.
binaries {
executable {
// 这里指定二进制文件的配置信息.
}
}
}
}
如果不需要额外的配置, 那么可以省略这个 Lambda 表达式:
binaries {
executable()
}
还可以指定对哪些构建类型创建二进制文件. 下面的示例只创建 debug
版的二进制文件:
binaries {
executable(listOf(DEBUG)) {
// 这里指定二进制文件的配置信息.
}
}
binaries {
executable([DEBUG]) {
// 这里指定二进制文件的配置信息.
}
}
还可以使用自定义的名称来声明二进制文件:
binaries {
executable("foo", listOf(DEBUG)) {
// 这里指定二进制文件的配置信息.
}
// 可以省略构建类型
// (这时会使用所有可用的构建类型).
executable("bar") {
// 这里指定二进制文件的配置信息.
}
}
binaries {
executable('foo', [DEBUG]) {
// 这里指定二进制文件的配置信息.
}
// 可以省略构建类型
// (这时会使用所有可用的构建类型).
executable('bar') {
// 这里指定二进制文件的配置信息.
}
}
这个示例中的第一个参数指定一个名称前缀, 它会是二进制文件的默认名称. 比如, 在 Windows 平台, 这个示例会输出 foo.exe
和 bar.exe
. 还可以使用这个名称前缀 在构建脚本中访问二进制文件.
访问二进制文件
可以访问二进制文件来 对其进行配置, 或者得到它们的属性 (比如, 得到输出文件的路径).
可以通过二进制文件的唯一名称来得到它. 这个名称由名称前缀(如果有指定), 构建类型, 以及二进制文件类型组成, 使用以下命名方式: <optional-name-prefix><build-type><binary-kind>
, 比如, releaseFramework
或 testDebugExecutable
.
// 如果二进制文件不存在, 这个函数会失败.
binaries["fooDebugExecutable"]
binaries.getByName("fooDebugExecutable")
// 如果二进制文件不存在, 这个函数会返回 null.
binaries.findByName("fooDebugExecutable")
// 如果二进制文件不存在, 这个函数会失败.
binaries['fooDebugExecutable']
binaries.fooDebugExecutable
binaries.getByName('fooDebugExecutable')
// 如果二进制文件不存在, 这个函数会返回 null.
binaries.findByName('fooDebugExecutable')
另一种方法是, 可以使用名称前缀和构建类型, 通过有类型的 get 方法访问二进制文件.
// 如果二进制文件不存在, 这个函数会失败.
binaries.getExecutable("foo", DEBUG)
binaries.getExecutable(DEBUG) // 如果没有设置名称前缀, 可以省略第一个参数.
binaries.getExecutable("bar", "DEBUG") // 对于构建类型, 也可以使用字符串.
// 对其他二进制文件类型, 可以使用类似的 get 方法:
// getFramework, getStaticLib 以及 getSharedLib.
// 如果二进制文件不存在, 这个函数会返回 null.
binaries.findExecutable("foo", DEBUG)
// 对其他二进制文件类型, 可以使用类似的 get 方法:
// findFramework, findStaticLib 以及 findSharedLib.
// 如果二进制文件不存在, 这个函数会失败.
binaries.getExecutable('foo', DEBUG)
binaries.getExecutable(DEBUG) // 如果没有设置名称前缀, 可以省略第一个参数.
binaries.getExecutable('bar', 'DEBUG') // 对于构建类型, 也可以使用字符串.
// 对其他二进制文件类型, 可以使用类似的 get 方法:
// getFramework, getStaticLib 以及 getSharedLib.
// 如果二进制文件不存在, 这个函数会返回 null.
binaries.findExecutable('foo', DEBUG)
// 对其他二进制文件类型, 可以使用类似的 get 方法:
// findFramework, findStaticLib 以及 findSharedLib.
将依赖项目导出到二进制文件
编译 Objective-C 框架, 或原生库(共享库或静态库)时, 经常会出现一种需要, 不仅要打包当前项目的类文件, 同时还要打包它的依赖项的类. 我们可以用 export
方法, 指定需要导出哪些依赖项到二进制文件中.
kotlin {
sourceSets {
macosMain.dependencies {
// 这些依赖项会被导出.
api(project(":dependency"))
api("org.example:exported-library:1.0")
// 这个依赖项不会被导出.
api("org.example:not-exported-library:1.0")
}
}
macosX64("macos").binaries {
framework {
export(project(":dependency"))
export("org.example:exported-library:1.0")
}
sharedLib {
// 可以对不同的二进制文件导出不同的依赖项目.
export(project(':dependency'))
}
}
}
kotlin {
sourceSets {
macosMain.dependencies {
// 这些依赖项会被导出.
api project(':dependency')
api 'org.example:exported-library:1.0'
// 这个依赖项不会被导出.
api 'org.example:not-exported-library:1.0'
}
}
macosX64("macos").binaries {
framework {
export project(':dependency')
export 'org.example:exported-library:1.0'
}
sharedLib {
// 可以对不同的二进制文件导出不同的依赖项目.
export project(':dependency')
}
}
}
比如, 你用 Kotlin 实现了几个模块, 并且想要在 Swift 中访问这些模块. 在一个 Swift 应用程序中无法使用多个 Kotlin/Native 框架, 但你可以创建一个 umbrella 框架, 把所有这些模块都导出到这个框架.
当你导出一个依赖项, 它的所有 API 到会包含框架 API 中. 编译器会向框架添加这个依赖项的代码, 即使你只使用了它的一小部分. 这就使得对导出的依赖项 (以及某种程度上对它的依赖项) 死代码消除功能不再有效.
默认情况下, 导出是非传递性的(non-transitively). 也就是说, 如果你导出的库 foo
依赖于库 bar
, 只有 foo
中的方法会被添加到输出的框架中.
这种行为可以通过 transitiveExport
选项来修改. 如果设置为 true
, 库 bar
中的声明也会被导出.
binaries {
framework {
export(project(":dependency"))
// 传递性导出.
transitiveExport = true
}
}
binaries {
framework {
export project(':dependency')
// 传递性导出.
transitiveExport = true
}
}
构建通用框架(Universal Framework)
默认情况下, Kotlin/Native 编译产生的 Objective-C 框架只支持单个平台. 但是, 使用 lipo
工具程序, 可以将多个框架合并为单个通用的(fat) 二进制文件. 对 32 位和 64 位 iOS 框架来说, 这种操作尤其合理. 这种情况下, 最终产生的通用框架可以同时运行在 32 位和 64 位设备上.
import org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask
kotlin {
// 创建并配置编译目标.
val watchos32 = watchosArm32("watchos32")
val watchos64 = watchosArm64("watchos64")
configure(listOf(watchos32, watchos64)) {
binaries.framework {
baseName = "my_framework"
}
}
// 创建 fat 框架的构建任务.
tasks.register<FatFrameworkTask>("debugFatFramework") {
// fat 框架必须使用与原框架相同的基本名称(base name).
baseName = "my_framework"
// 默认的输出目录是 "<build directory>/fat-framework".
destinationDir = buildDir.resolve("fat-framework/debug")
// 指定需要合并的框架.
from(
watchos32.binaries.getFramework("DEBUG"),
watchos64.binaries.getFramework("DEBUG")
)
}
}
import org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask
kotlin {
// 创建并配置编译目标.
targets {
watchosArm32("watchos32")
watchosArm64("watchos64")
configure([watchos32, watchos64]) {
binaries.framework {
baseName = "my_framework"
}
}
}
// 创建 fat 框架的构建任务.
tasks.register("debugFatFramework", FatFrameworkTask) {
// fat 框架必须使用与原框架相同的基本名称(base name).
baseName = "my_framework"
// 默认的输出目录是 "<build directory>/fat-framework".
destinationDir = file("$buildDir/fat-framework/debug")
// 指定需要合并的框架.
from(
targets.watchos32.binaries.getFramework("DEBUG"),
targets.watchos64.binaries.getFramework("DEBUG")
)
}
}
构建 XCFramework
所有的 Kotlin 跨平台项目都可以使用 XCFramework 作为输出, 将用于所有目标平台和架构的逻辑收集在单个 bundle 之内. 与 单个通用的(fat)框架 不同, 在将应用程序发布到 App Store 之前, 你不需要删除所有不必要的架构.
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
plugins {
kotlin("multiplatform")
}
kotlin {
val xcf = XCFramework()
val iosTargets = listOf(iosX64(), iosArm64(), iosSimulatorArm64())
iosTargets.forEach {
it.binaries.framework {
baseName = "shared"
xcf.add(this)
}
}
}
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig
plugins {
id 'org.jetbrains.kotlin.multiplatform'
}
kotlin {
def xcf = new XCFrameworkConfig(project)
def iosTargets = [iosX64(), iosArm64(), iosSimulatorArm64()]
iosTargets.forEach {
it.binaries.framework {
baseName = 'shared'
xcf.add(it)
}
}
}
在你声明 XCFramework 时, Kotlin Gradle plugin 会注册 3 个 Gradle task:
如果在你的项目中使用 CocoaPods 集成, 那么可以使用 Kotlin CocoaPods Gradle plugin 构建 XCFramework. 它包含以下 task, 使用所有已注册的编译目标构建 XCFramework, 并生成 podspec 文件:
podPublishReleaseXCFramework
, 生成 release 版 XCFramework 以及一个 podspec 文件.
podPublishDebugXCFramework
, 生成 debug 版 XCFramework 以及一个 podspec 文件.
podPublishXCFramework
, 生成 debug 版和 release 版 XCFramework 以及一个 podspec 文件.
通过这些 task, 可以帮助你将你的项目的共用部分从移动应用程序中分离出来, 单独通过 CocoaPod 发布. 你也可以使用 XCFramework 来发布到私有的或公共的 podspec 仓库.
定制 Info.plist 文件
输出框架时, Kotlin/Native 编译器会生成信息属性列表文件, Info.plist
. 你可以使用相应的二进制选项来定制其中的属性:
属性 | 二进制 |
---|
CFBundleIdentifier
| bundleId
|
CFBundleShortVersionString
| bundleShortVersionString
|
CFBundleVersion
| bundleVersion
|
要启用这个功能, 请对指定的框架使用 -Xbinary=$option=$value
编译器选项, 或通过 Gradle DSL 设置 binaryOption("option", "value")
:
binaries {
framework {
binaryOption("bundleId", "com.example.app")
binaryOption("bundleVersion", "2")
}
}
最终更新: 2024/12/17