本文编写:@吴霖鹏
合作伙伴:@王明哲、@刘潇
背景
ByteKMP 是字节内部基于 KMP(Kotlin Multiplatform) 建设的客户端跨平台方案,希望通过 KMP 技术实现 Android、鸿蒙、iOS 三端的代码复用,以此降低开发成本、提高逻辑与 UI 的多端一致性。
由于抖音鸿蒙版已经基于 ArkTS 完成了大部分基础能力和部分业务开发,接入 KMP 后需要在充分复用现有 ArkTS 能力的同时,支持业务侧在 ArkTS 场景下调用 KMP 代码。因此,我们需要建设 Kotlin 与 ArkTS 之间的跨语言交互能力,为开发者提供便捷、高效的跨语言交互体验,助力 ByteKMP 在业务顺利落地。
名词解释
KN: Kotlin/Native,ByteKMP 在鸿蒙上采用 Kotlin/Native 技术执行 Kotlin 代码
ArkTS:鸿蒙官方开发语言
主模块:KN 在鸿蒙中以 so 形式集成,因此在 KN 项目中需要一个处于顶层的模块将依赖的 KMP 代码打包为目标平台二进制产物,这个模块称为主模块
Kotlin 调用 ArkTS
在鸿蒙开发中,系统提供了 NAPI 实现 ArkTS 与C/C++ 模块之间的交互。而 Kotlin/Native 本身就具备与 C/C++ 互操作的能力(基于 cinterop),因此理论上 Kotlin/Native 也能够通过 NAPI 实现与 ArkTS 互操作。
如何基于 NAPI 调用 ArkTS 代码
ArkTS 对象在 native 侧均以 napi_value 类型表示,包括ArkTS 模块、类、实例以及方法等。NAPI 提供了一系列方法用于操作 napi_value 对象,比如获取模块、获取模块导出类、调用 ArkTS 方法等,同时也支持在 native 与 ArkTS 之间进行基础类型数据转换。
下面以一个ArkTS 模块 @douyin/logger 导出的 logger 对象为例,演示 KN 如何基于 NAPI 调用 logger 来打印日志,ArkTS 代码如下
// ArkTSLogger.etsexport class ArkTSLogger { d(tag: string, msg: string) { console.log(`[${tag}] ${msg}`) }}export const logger = new ArkTSLogger()// Index.etsexport { logger } form './src/main/ets/ArkTSLogger'
在 KN 侧主要通过以下流程调用 logger 的 log 方法
通过 napi_load_module_with_info 获取模块@douyin/logger
通过napi_get_named_property获取模块导出的 logger 对象以及方法 d
通过napi_create_string_utf8构造 string 类型的参数 tag 和 msg
通过napi_call_function调用 d 方法并传递参数
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
封装 NAPI
直接调用 NAPI 的方式既繁琐,又要求使用者具备一定的 NAPI 知识,同时还伴随着大量模板化代码。为降低使用成本,我们有必要在 NAPI 之上进行一层封装。
考虑到 ArkTS 对象在 native 侧统一表示为 napi_value,且所有操作都必须基于 napi_value 展开,我们可以将 ArkTS 对象抽象为一个 Kotlin 对象:该对象内部持有 napi_value,并通过封装相应的方法,对外提供更友好的操作接口。
以 ArkTs 实例为例,我们可以进行如下封装
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
除了实例对象外还有模块、类、方法、数组、基础类型等对象,由于所有对象都需要napi_value,我们可以定义一个基类 ArkObject 来持有napi_value,其他对象均继承自 ArkObject 并提供特定的能力。
不过需要注意的是,napi_value 只在一次主线程方法执行期间有效,当本次调用结束后就会失效。因此需要通过 napi_ref 来延长它的生命周期,并且在 Kotlin 对象被回收后主动释放引用避免内存泄漏。ArkObject代码实现如下
open class ArkObject(var value: napi_value) { // 创建 napi_value 引用 private var napiRef = value.createRef() // 通过 ref 获取 napi_value var napiValue: napi_value = value get() = napiRef.getRefValue() // 当前对象回收后主动解除 napiRef 的绑定 @Suppress("unused") private val cleaner = createCleaner(napiRef) { GlobalScope.launch(Dispatchers.Main) { it.deleteRef() } }}
下面展示了基于封装后的 logger 调用实现。与原始的 NAPI 调用方式相比,这种写法不仅更加简洁,也显著提升了代码的可读性。
val ezLogModule = ArkModule("@douyin/logger") // 获取模块val logger = ezLogModule.getExportInstance("logger") // 获取导出对象 logval dFunc = logger.getFunction("d") // 获取方法 ddFunc.call(arrayOf(arkString("KmpLogger"), arkString("kmp msg"))) // 调用 d
Kotlin 代码导出至 ArkTS
如何基于 NAPI 导出 C++ 代码
NAPI 同样支持将 native 代码导出为 TS 声明(.d.ts) 供 ArkTS 使用。在鸿蒙中,系统会在 native 模块初始化时注入一个 exports 对象,我们可以通过 NAPI 将属性、方法或类信息注册到该对象中,并在 ArkTS 侧提供对应的 .d.ts 声明。当 ArkTS 调用这些代码时,NAPI 会将调用请求转发至 native 侧的桥接代码,从而实现 ArkTS 对 native 能力的访问。
由于 exports 对象是在鸿蒙 C++ 模块的 Init 方法中传入,我们就用 C++ 演示如何基于 NAPI 导出代码到 ArkTS。首先在 C++ 代码中添加一个包含日志打印逻辑的方法testLog
static void testLog(int value) { OH_LOG_Print(LOG_APP, LOG_INFO, 0, "KmpLogger", "log from c++: %{public}d", value);}
根据 NAPI 规范,我们需要实现一个桥接方法,用于将 ArkTS 的调用请求转发至 native 侧的具体实现。同时,还需在 exports 对象中将 testLog 方法注册到对应的桥接方法上
// 桥接方法,napi 固定签名static napi_value bridgeMethod(napi_env env, napi_callback_info info) { // 获取 ArkTS 侧传递的参数 size_t argc = 1; napi_value args[1]; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); int value; // 将参数转换为 int napi_get_value_int32(env, args[0], &value); // 调用 testlog testLog(value);return nullptr;}// 注册 bridgeMethodEXTERN_C_STARTstatic napi_value Init(napi_env env, napi_value exports){ napi_property_descriptor desc[] = { { "testLog", nullptr, bridgeMethod, nullptr, nullptr, nullptr, napi_default, nullptr } }; // 向 exports 中注册桥接方法 napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);return exports;}EXTERN_C_END
最后在 index.d.ts 中定义 testLog 方法的签名即可
// Index.d.tsexport const testLog: (value: number) => void
这样在 ArkTS 模块中引用并调用 testLog 即可触发 C++ 侧的 testLog 方法执行。
基于 KSP 封装模板代码
与 C++ 类似,在 Kotlin 中每个需要导出到 ArkTS 的代码(类、方法、属性)都必须经过以下步骤:
定义桥接方法,在方法内处理对象获取、参数解析、调用 native 实现、数据返回及类型转换
将桥接方法注册到 exports
在 index.d.ts 中添加对应声明
这一整套流程同样既繁琐又充斥着大量模板代码,为降低开发成本,我们考虑通过代码生成来简化该流程,其整体设计如下
桥接代码代码生成
桥接代码生成基于 KSP + 注解 的方案,使用方通过注解标注需要导出的属性、方法和类,KSP 插件在编译期自动生成对应的桥接代码。以一个简单的方法 test 为例,使用方只需要在方法上标注 @ArkTsExportFunction 注解
@ArkTsExportFunctionfun test(): Int {return 1}
KSP 插件将会生成如下桥接代码
// 桥接代码private fun methodBridge(env: napi_env?, info: napi_callback_info?): napi_value? { // 调用 Kotlin 代码 val result = test() // 处理类型转换并返回结果return createInt(result).value}// 注册代码fun defineFunctionFor_test(env: napi_env, exports: napi_value) { val descArray = nativeHeap.allocArray<napi_property_descriptor>(1) descArray[0].name = arkString("test").napiValue.value descArray[0].method = staticCFunction(::methodBridge) descArray[0].attributes = napi_default napi_define_properties(env, exports, 1u, descArray)}
最终我们会在主模块收集项目中全部模块生成的注册代码,生成一个整体的注册代码,代码示例如下
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
0
然而,KSP 并不具备跨模块访问能力,这意味着主模块在处理时仅能看到本模块的代码,从而导致子模块生成的桥接代码无法被识别。为了解决这一问题,我们采用了 MetaInfo 机制:在每个子模块中生成一份固定包名的 MetaInfo 代码,并将桥接信息以字符串形式保存在注解中。主模块通过 getDeclarationsFromPackage 获取指定包下的所有声明,再解析注解提取子模块的桥接信息,从而生成完整的注册代码。
完整的处理流程如下图所示
导出 Kotlin Class
导出 Kotlin 顶层方法、属性只需要像上面的代码一样提供桥接方法即可,而导出类则会复杂一些,因为需要支持非基础类型(Class)在 Kotlin 与 ArkTS 之间的相互转换。
核心思路是:利用 NAPI 提供的 napi_wrap 将 ArkTS 对象与 Kotlin 对象进行绑定;在 ArkTS 调用 Kotlin 时,通过 napi_unwrap 获取当前 ArkTS 对象绑定的 Kotlin 实例,并将调用转发至该对象。大致流程为:
实现构造函数桥接方法:该方法会在 ArkTS 侧尝试创建导出类时调用
创建 Kotlin 对象
通过 napi_wrap 将 Kotlin 对象与 ArkTS 对象绑定
实现类方法的桥接方法
通过napi_unwrap获取当前 ArkTS 对象绑定的 Kotlin 实例
调用 Kotlin 对象的对应方法
将结果返回给 ArkTS,并处理必要的类型转换。
在 exports 中注册导出类,并绑定上面实现的桥接方法
以 KotlinClass 为例,生成的桥接代码如下所示
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
1
导出 Kotlin Interface
Kotlin 导出代码供 ArkTS 调用的场景中,经常会涉及 回调 或 ArkTS 能力注入。以接口回调为例,Kotlin 侧可能会设计出如下代码
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
2
requestNetwork方法导出至 ArkTS ,callback 由 ArkTS 实现并在调用时传入,这样在后续请求结束时 Kotlin 侧可以将结果回调至 ArkTS。
这种场景下接口回调本质上是一次 Kotlin 调用 ArkTS 的过程,我们可以通过前面设计的能力来实现,代码如下
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
3
不过这种实现方式存在一个明显的问题:缺少强制约束。ArkTS 与 Kotlin 之间的通信完全依赖双方的“约定”,一旦任意一方修改了接口定义或编写代码时出现错误,往往难以及时发现问题,排查成本也会大幅提升。为了支持这种「Kotlin 侧定义接口,由 ArkTS 实现」的场景,我们需要实现 Kotlin 接口的导出能力。
核心实现思路是:
基于 KSP 为接口自动生成一个实现类,在该类中持有 napi_value
根据方法签名信息,将 Kotlin 方法的调用转发至 napi_value 对应的 ArkTS 方法上
将接口定义导出到 ArkTS,确保导出的代码具有明确定义和约束
根据这个思路,编译期将为 Callback 接口生成如下实现
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
4
并在 requestNetwork 的桥接方法中将 ArkTS 传入的对象转换为JsImportInterfaceBinding_Callback
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
5
最终导出到 ArkTS 的接口定义如下
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
6
接入使用
导出代码
导出顶层属性
使用@ArkTsExportProperty注解标注对应的属性来导出到 ArkTS,支持val/var,代码示例如下所示
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
7
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
8
导出顶层方法
使用@ArkTsExportFunction注解标注对应的方法来导出到 ArkTS,对于 suspend 类型的方法在 ArkTS 侧会生成对应的 Promise 方法,代码示例如下所示
// 1. 获取 @douyin/logger 模块val module = nativeHeap.alloc<napi_valueVar>()napi_load_module_with_info(globalEnv, "@douyin/logger", bundleName, module.ptr)// 2. 获取 @douyin/logger 模块导出的 logger 对象val log = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, module.value, "logger", log.ptr)// 3. 获取 logger 对象的 d 方法val dFunc = nativeHeap.alloc<napi_valueVar>()napi_get_named_property(globalEnv, log.value, "d", dFunc.ptr)// 4. 构造参数 tag、msg,将 Kotlin String 转换为 ArkTS stringval tag = "KmpTag"val msg = "KmpMsg"val tagVar = nativeHeap.alloc<napi_valueVar>()val msgVar = nativeHeap.alloc<napi_valueVar>()napi_create_string_utf8(globalEnv, value, strlen(tag), tagVar.ptr)napi_create_string_utf8(globalEnv, value, strlen(msg), msgVar.ptr)// 5. 构造参数数组val argsValue = nativeHeap.allocArray<napi_valueVar>(2)argsValue[0] = tagVarargsValue[1] = msgVar// 6. 调用 d 方法val result = nativeHeap.alloc<napi_valueVar>()napi_call_function(globalEnv, log.value, dFunc.value, 2, argsValue, result.ptr)
9
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
0
导出类
使用@ArkTsExportClass注解标注对应的类来导出到 ArkTS,默认情况下框架会使用无参数构造函数来构造 KN 对象,如果想指定有参构造函数可以搭配@ArkTsExportClassGenerator来使用
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
1
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
2
默认情况下不会导出类中的属性和方法,如果需要导出需要在对应的属性和方法上标注@ArkTsExport
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
3
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
4
导出枚举
使用@ArkTsExportEnum注解标注对应的枚举类来导出到 ArkTS
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
5
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
6
导出接口
通过@ArkTsExportInterface 导出需要 ArkTS 实现的接口,不支持导出接口属性,且默认导出所有方法不需要使用@ArkTsExport标注
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
7
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
8
线程安全
由于在 KN 侧调用@ArkTsExportInterface方法涉及到 NAPI 操作, 而 NAPI 调用需要保证在主线程执行,否则会发生崩溃。为了避免业务侧过多的关注线程切换逻辑,框架提供了以下两种方式实现自动线程切换以保证线程安全
1. @ArkTsThreadSafe
对于非 suspend 方法,如果业务想在调用时不去关注线程切换问题,可以为该方法标注@ArkTsThreadSafe,框架会在方法实现内使用 runBlocking 切换到主线程调用并阻塞当前线程直到结果返回
class ArkInstance(private val napiValue: napi_valueVar) { fun getProperty(name: String): ArkInstance { val propertyNapiValue = ... // 省略 napi 操作 retun ArkInstance(propertyNapiValue) } fun getFunction(name: String): ArkFunction { // 省略 napi 操作 }}class ArkFunction(private val receiver: napi_valueVar, private val napiValue: napi_valueVar) { fun call(args: Array<Any>) { // 省略 napi 操作 }}
9
2. safeSuspend()
同时框架也提供了接口对象的扩展方法safeSuspend(),用于获取该接口的 suspend 包装类,该类提供了当前接口所有方法的 suspend 版本,业务可以在协程中安全使用
open class ArkObject(var value: napi_value) { // 创建 napi_value 引用 private var napiRef = value.createRef() // 通过 ref 获取 napi_value var napiValue: napi_value = value get() = napiRef.getRefValue() // 当前对象回收后主动解除 napiRef 的绑定 @Suppress("unused") private val cleaner = createCleaner(napiRef) { GlobalScope.launch(Dispatchers.Main) { it.deleteRef() } }}
0
支持类型
框架对导出代码的类型有一定限制,包括属性类型、方法参数类型和方法返回值类型,具体支持的类型如下表所示
类型 | Kotlin 类型 | ArkTS 类型 |
基础类型 | Int | number |
Long | number | |
Double | number | |
Float | number | |
String | string | |
容器 | Map<String, [基础类型] | [自定义类型]> | Map<string, [基础类型] | [自定义类型]> |
Array<[基础类型] | [自定义类型]> | Array<[基础类型] | [自定义类型]> | |
List<[基础类型] | [自定义类型]> | List<[基础类型] | [自定义类型]> | |
ByteArray | ArrayBuffer | |
自定义类型 | @ArkTsExportClass | class |
@ArkTsExportInterface | Interface | |
导出枚举类型 | @ArkTsExportEnum | enum |
ArkTS对象 | napi_value | EsObject |
协程 | suspend function | Promise |
性能优化
Kotlin Native 基于 LLVM 编译器基础设施构建,它将 K2 中间表示(IR)转换为 LLVM IR,其语言后端与 C/C++ 保持一致。这种设计使得 Kotlin Native 的理论性能有潜力接近 C 语言的水平。然而,为了维护 Kotlin 语言的内存安全特性和垃圾回收(GC)机制,Kotlin Native 引入了一套相对厚重的运行时系统。这套运行时在提供便利性的同时,也带来了不可忽视的开销。
当 Kotlin 通过 cinterop 调用 NAPI 时,这种双重内存管理模型的开销会进一步放大,在字符串转换场景尤为明显。一个 C 语言版本的实现可以直接在栈上或原生堆上分配内存,过程直接高效。
open class ArkObject(var value: napi_value) { // 创建 napi_value 引用 private var napiRef = value.createRef() // 通过 ref 获取 napi_value var napiValue: napi_value = value get() = napiRef.getRefValue() // 当前对象回收后主动解除 napiRef 的绑定 @Suppress("unused") private val cleaner = createCleaner(napiRef) { GlobalScope.launch(Dispatchers.Main) { it.deleteRef() } }}
1
相比之下,Kotlin 版本的实现则面临双重内存管理的开销
open class ArkObject(var value: napi_value) { // 创建 napi_value 引用 private var napiRef = value.createRef() // 通过 ref 获取 napi_value var napiValue: napi_value = value get() = napiRef.getRefValue() // 当前对象回收后主动解除 napiRef 的绑定 @Suppress("unused") private val cleaner = createCleaner(napiRef) { GlobalScope.launch(Dispatchers.Main) { it.deleteRef() } }}
2
在一次简单的字符串转换过程中,Kotlin 与 C/C++ 层之间存在 2 次状态切换、 2 次字符串拷贝以及2 次编码转换,字符串跨语言传输已成为业务的性能瓶颈。
针对这种问题我们参考Kotiln内部实现引入了@GCUnsafeCall,它的作用是向编译器声明:与此函数链接的 C++ 实现是“可信的”,它完全理解并遵循 Kotlin 的 GC 规则和调用约定。因此,编译器无需为其生成常规的、带有性能开销的包装代码。
借助@GCUnsafeCall,我们针对字符串场景做了优化,实现机制如下:
1. 声明内存安全接口:暂时性地、非安全地获取内部内存的写权限,完成数据填充后,再将其作为一个合法的、不可变的对象交还给 Kotlin 。
open class ArkObject(var value: napi_value) { // 创建 napi_value 引用 private var napiRef = value.createRef() // 通过 ref 获取 napi_value var napiValue: napi_value = value get() = napiRef.getRefValue() // 当前对象回收后主动解除 napiRef 的绑定 @Suppress("unused") private val cleaner = createCleaner(napiRef) { GlobalScope.launch(Dispatchers.Main) { it.deleteRef() } }}
3
2. 消除中间抽象:直接在 C++ 侧处理 `napi_value` 等 Native 句柄和裸指针,彻底抛弃为指针、缓冲区等创建的 Kotlin 包装器,免除 2 次状态切换与 1 次拷贝。
open class ArkObject(var value: napi_value) { // 创建 napi_value 引用 private var napiRef = value.createRef() // 通过 ref 获取 napi_value var napiValue: napi_value = value get() = napiRef.getRefValue() // 当前对象回收后主动解除 napiRef 的绑定 @Suppress("unused") private val cleaner = createCleaner(napiRef) { GlobalScope.launch(Dispatchers.Main) { it.deleteRef() } }}
4
优化前后的对比如下,最终长字符串转换耗时能够降低 90%
对比维度 | 优化前 | 优化后 |
指针/句柄处理 | 创建 Kotlin 对象封装 | C++侧直接使用原始值 |
内存分配 | 2+ 次 (包装器, C缓冲, Kotlin对象) | 1 次 (仅最终 Kotlin 对象) |
数据拷贝 | 2 次 | 1 次 |
编码转换 | 2 次 (UTF-16 -> UTF-8 -> UTF-16) | 0 次 |
长字符串转换耗时 | 25.4 ms | 2.4 ms (-90.5%) |
未来规划
实现 ArkTS 与 KMP 之间的字符串共享,避免拷贝操作,彻底解决字符串传输性能问题
解决多线程限制问题,抹平平台(Android、Harmony)差异,提供一致的调用体验
加入我们
我们是「抖音客户端架构」团队,以「打造极致的研发效能,助力业务高效发展」为使命,「成为行业卓越的架构师团队」为愿景,为抖音客户端高效高质量交付保驾护航。
目前团队急需KMP、大模型应用、架构师方向工程师,诚邀有志之士与我们一起构建字节跨端方案,服务亿级用户。请扫描下方二维码或点击阅读原文投递简历
客户端跨端技术专家
抖音客户端架构师
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...