Kotlin 与 ArkTS 交互性能与效率优化实践

本文编写:@吴霖鹏

合作伙伴:@王明哲、@刘潇

背景

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.ets  
export class ArkTSLogger {  
  d(tag: string, msg: string) {  
    console.log(`[${tag}] ${msg}`)  
  }  
}  
  
export const logger = new ArkTSLogger()  
  
// Index.ets  
export { 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 string  
val 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] = tagVar  
argsValue[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 并提供特定的能力。

picture.image

不过需要注意的是,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") // 获取导出对象 log  
val dFunc = logger.getFunction("d") // 获取方法 d  
dFunc.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;  
}  
  
// 注册 bridgeMethod  
EXTERN\_C\_START  
static 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.ts  
export const testLog: (value: number) => void  

这样在 ArkTS 模块中引用并调用testLog 即可触发 C++ 侧的testLog 方法执行。

基于 KSP 封装模板代码

与 C++ 类似,在 Kotlin 中每个需要导出到 ArkTS 的代码(类、方法、属性)都必须经过以下步骤:

  • 定义桥接方法,在方法内处理对象获取、参数解析、调用 native 实现、数据返回及类型转换

  • 将桥接方法注册到 exports

  • index.d.ts 中添加对应声明

这一整套流程同样既繁琐又充斥着大量模板代码,为降低开发成本,我们考虑通过代码生成来简化该流程,其整体设计如下

picture.image

桥接代码代码生成

桥接代码生成基于 KSP + 注解 的方案,使用方通过注解标注需要导出的属性、方法和类,KSP 插件在编译期自动生成对应的桥接代码。以一个简单的方法 test 为例,使用方只需要在方法上标注 @ArkTsExportFunction 注解

  
@ArkTsExportFunction  
fun 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)  
}  

最终我们会在主模块收集项目中全部模块生成的注册代码,生成一个整体的注册代码 ,代码示例如下

  
fun init(env: napi\_env, exports: napi\_value, bundle: String, soName: String) {  
    defineExports(env, exports)  
}  
  
private fun defineExports(env: napi\_env, exports: napi\_value) {  
    // 项目中 ksp 生成的全部桥接代码  
    com.bytedance.kmp.demo.js\_bind\_function.defineFunctionFor\_test1(env, exports)  
    com.bytedance.kmp.demo.js\_bind\_function.defineFunctionFor\_test2(env, exports)  
    com.bytedance.kmp.demo.js\_bind\_function.defineFunctionFor\_test3(env, exports)  
    com.bytedance.kmp.demo.js\_bind\_function.defineFunctionFor\_test4(env, exports)  
    // ...  
}  

然而,KSP 并不具备跨模块访问能力,这意味着主模块在处理时仅能看到本模块的代码,从而导致子模块生成的桥接代码无法被识别。为了解决这一问题,我们采用了 MetaInfo 机制:在每个子模块中生成一份固定包名的 MetaInfo 代码,并将桥接信息以字符串形式保存在注解中。主模块通过getDeclarationsFromPackage 获取指定包下的所有声明,再解析注解提取子模块的桥接信息,从而生成完整的注册代码。

完整的处理流程如下图所示

picture.image

导出 Kotlin Class

导出Kotlin 顶层方法、属性 只需要像上面的代码一样提供桥接方法即可,而导出类则会复杂一些,因为需要支持非基础类型(Class)在 Kotlin 与 ArkTS 之间的相互转换。

核心思路是: 利用 NAPI 提供的 napi_wrap 将 ArkTS 对象与 Kotlin 对象进行绑定;在 ArkTS 调用 Kotlin 时,通过 napi_unwrap 获取当前 ArkTS 对象绑定的 Kotlin 实例,并将调用转发至该对象。大致流程为:

  1. 实现构造函数桥接方法: 该方法会在 ArkTS 侧尝试创建导出类时调用

  2. 创建 Kotlin 对象

  3. 通过napi_wrap 将 Kotlin 对象与 ArkTS 对象绑定

  4. 实现类方法的桥接方法

  5. 通过napi_unwrap 获取当前 ArkTS 对象绑定的 Kotlin 实例

  6. 调用 Kotlin 对象的对应方法

  7. 将结果返回给 ArkTS,并处理必要的类型转换。

  8. exports 中注册导出类,并绑定上面实现的桥接方法

以 KotlinClass 为例,生成的桥接代码如下所示

  
class KotlinClass {  
    fun test() {  
    }  
}  
  
// 定义导出类的构造桥接方法  
fun constructor(env: napi\_env?, info: napi\_callback\_info?): napi\_value? {  
    // 获取当前对象的 this  
    val thisArg = info!!.thisArg()  
    // 创建 KN 对象  
    val kmpClassInstance = KotlinClass()  
    val stableRef = StableRef.create(kmpClassInstance).asCPointer()  
    // 将 Kotlin 对象与 ArkTS 对象绑定  
    napi\_wrap(env, thisArg, stableRef, null, null, null)  
    // 返回构造参数  
    return thisArg  
}  
  
// 获取 ArkTS 对象绑定的 KotlinClass 对象  
fun napi\_value.getKotlinClass(): KotlinClass? {  
    // 获取 Kotlin 对象  
    val instance = unwrap()?.asStableRef<KotlinClass>()?.get()  
    return instance  
}  
  
// test 方法的桥接代码  
private fun bridgeMethodFortest(env: napi\_env?, info: napi\_callback\_info?): napi\_value? {  
    // 通过 this 获取当前绑定的 Kotlin 对象  
    val obj = info!!.thisArg().getKotlinClass()  
    // 调用 Kotlin 对象的 test 方法  
    obj?.test()  
    return null  
}  
  
// 注册代码  
fun defineClassForKotlinClass(env: napi\_env, exports: napi\_value) {  
    val descArray = nativeHeap.allocArray<napi\_property\_descriptor>(1)  
  
    // 定义 Function  
    descArray[0].name = createString("test")  
    descArray[0].method = staticCFunction { env: napi\_env?, info: napi\_callback\_info? ->  
        bridgeMethodFortest(env, info)  
    }  
    descArray[0].attributes = napi\_default  
  
    // 定义 Class  
    val result = nativeHeap.alloc<napi\_valueVar>()  
    napi\_define\_class(env, "KotlinClass", strlen("KotlinClass"), staticCFunction { env: napi\_env?, info: napi\_callback\_info? ->  
        constructor(env, info)  
    }, null, 1u, descArray, result.ptr)  
    napi\_set\_named\_property(env, exports, "KotlinClass", result.value)  
}  

导出 Kotlin Interface

Kotlin 导出代码供 ArkTS 调用的场景中,经常会涉及 回调ArkTS 能力注入 。以接口回调为例,Kotlin 侧可能会设计出如下代码

  
interface Callback {  
    fun onSuccess(result: String)  
}  
  
fun requestNetwork(callback: Callback) {  
    // 忽略请求代码  
    callback.onSuccess("success")  
}  

requestNetwork 方法导出至 ArkTS ,callback 由 ArkTS 实现并在调用时传入,这样在后续请求结束时 Kotlin 侧可以将结果回调至 ArkTS。

这种场景下接口回调本质上是一次 Kotlin 调用 ArkTS 的过程,我们可以通过前面设计的能力来实现,代码如下

  
fun requestNetwork(callback: napi\_value) {  
    // 忽略请求代码  
    val result = arkString("success")  
    ArkInstance(callback).getFunction("onSuccess")?.call(arrayOf(result))  
}  

不过这种实现方式存在一个明显的问题:缺少强制约束。 ArkTS 与 Kotlin 之间的通信完全依赖双方的“约定”,一旦任意一方修改了接口定义或编写代码时出现错误,往往难以及时发现问题,排查成本也会大幅提升。为了支持这种**「Kotlin 侧定义接口,由 ArkTS 实现」** 的场景,我们需要实现 Kotlin 接口的导出能力。

核心实现思路是:

  • 基于 KSP 为接口自动生成一个实现类,在该类中持有napi_value

  • 根据方法签名信息,将 Kotlin 方法的调用转发至 napi_value 对应的 ArkTS 方法上

  • 将接口定义导出到 ArkTS,确保导出的代码具有明确定义和约束

根据这个思路,编译期将为 Callback 接口生成如下实现

  
// 接口实现类,为 ArkTS 侧实现的 Callback 对象做一层代理  
class JsImportInterfaceBinding\_Callback(val instance: ArkInstance) : Callback {  
    override fun onSuccess(result:String): Unit {  
        // 将 onSuccess 方法转发到 napi\_value 的 onSuccess 方法上  
        val function = instance.getFunction("onSuccess")  
        val params = arrayOf(createString(result))  
        function.getNapiValue().call(instance.getNapiValue(), params)  
    }  
}  
  
// 将 ArkTS 对象转换为 Callback  
fun napi\_value.getCallback(): Callback? {  
    val instance = ArkInstance(this)  
    return JsImportInterfaceBinding\_Callback(instance)  
}  

并在requestNetwork 的桥接方法中将 ArkTS 传入的对象转换为JsImportInterfaceBinding_Callback

  
private fun methodBridge(env: napi\_env?, info: napi\_callback\_info?): napi\_value? {  
    val params = info!!.params(1)  
    // 将 ArkTS 对象转换为 Callback  
    val arg0 = params[0]!!.getCallback()!!  
    // 调用 requestNetwork  
    requestNetwork(arg0)  
}  

最终导出到 ArkTS 的接口定义如下

  
// Callback.d.ts  
export interface Callback {  
    onSuccess: (result: string) => void;  
}  
  
// index.d.ts  
import { Callback } from './Callback'  
export { Callback }  
export const requestNetwork: (callback: Callback) => void;

接入使用

导出代码

导出顶层属性

使用**@ArkTsExportProperty** 注解标注对应的属性来导出到 ArkTS,支持val/var ,代码示例如下所示

  
// Kotlin  
@ArkTsExportProperty  
val name: String = "123"  
@ArkTsExportProperty  
var age: String = "123"  

  
// 自动生成 ArkTs 侧代码声明 index.d.ts  
export const name: string  
export var age: string  

导出顶层方法

使用**@ArkTsExportFunction** 注解标注对应的方法来导出到 ArkTS,对于suspend 类型的方法在 ArkTS 侧会生成对应的Promise 方法,代码示例如下所示

  
// Kotlin  
@ArkTsExportFunction  
fun test(): String {  
    ...  
}  
  
@ArkTsExportFunction  
suspend fun testSuspend(): String {  
    // ...  
}
  
// 自动生成 ArkTs 侧代码声明 index.d.ts  
export const test: () => string  
export const testSuspend: () => Promise<string>  

导出类

使用**@ArkTsExportClass** 注解标注对应的类来导出到 ArkTS,默认情况下框架会使用无参数构造函数来构造 KN 对象,如果想指定有参构造函数可以搭配**@ArkTsExportClassGenerator** 来使用

  
// Kotlin  
@ArkTsExportClass  
class A {  
  
}  
@ArkTsExportClass  
class B {  
    @ArkTsExportClassGenerator  
    constructor(name: String)  
}  

  
// 自动生成 ArkTs 侧代码声明 index.d.ts  
export class A {}  
  
export class B {  
    constructor(name: string)  
}  

默认情况下不会导出类中的属性和方法,如果需要导出需要在对应的属性和方法上标注**@ArkTsExport**

  
// Kotlin  
@ArkTsExportClass  
class A {  
    val name: String = ""  
    fun test() {}  
}  
@ArkTsExportClass  
class B {  
    @ArkTsExport  
    val name: String = ""  
    @ArkTsExport  
    fun test() {}  
}  

  
// 自动生成 ArkTs 侧代码声明 index.d.ts  
export class A {}  
  
export class B {  
    readonly name: string  
    test: () => void  
}  

导出枚举

使用**@ArkTsExportEnum** 注解标注对应的枚举类来导出到 ArkTS

  
// Kotlin  
@ArkTsExportEnum  
enum class UserType {  
    A, B, C  
}  

  
// 自动生成 ArkTs 侧代码声明 index.d.ts  
export enum UserType {  
    A, B, C  
}  

导出接口

通过**@ArkTsExportInterface** 导出需要 ArkTS 实现的接口,不支持导出接口属性,且默认导出所有方法不需要使用**@ArkTsExport** 标注

  
// Kotlin  
@ArkTsExportInterface  
interface ArkTsService {  
    fun getUserName(): String  
}  
  
@ArkTsExportFunction  
fun injectArkTsService(service: ArkTsService)  

  
// 自动生成 ArkTs 侧代码声明 index.d.ts  
export interface ArkTsService {  
    getUserName: () => string  
}  
  
export injectArkTsService: (service: ArkTsService) => void  
  
// ArkTs侧实现接口并传递给 Kotlin  
injectArkTsService({  
    getUserName: () => {  
        return ...  
    }  
})  

线程安全

由于在 KN 侧调用**@ArkTsExportInterface** 方法涉及到 NAPI 操作, 而 NAPI 调用需要保证在主线程执行,否则会发生崩溃。为了避免业务侧过多的关注线程切换逻辑,框架提供了以下两种方式实现自动线程切换以保证线程安全

1. @ArkTsThreadSafe

对于非 suspend 方法,如果业务想在调用时不去关注线程切换问题,可以为该方法标注**@ArkTsThreadSafe** ,框架会在方法实现内使用runBlocking 切换到主线程调用并阻塞当前线程直到结果返回

  
// Kotlin  
@ArkTsExportInterface  
interface ArkTsService {  
    @ArkTsThreadSafe  
    fun getUserName(): String = ""  
}  

2. safeSuspend()

同时框架也提供了接口对象的扩展方法safeSuspend() ,用于获取该接口的 suspend 包装类,该类提供了当前接口所有方法的suspend 版本,业务可以在协程中安全使用

  
val service: ArkTsService = xxx  
GlobalScope.launch {  
    // 调用生成的 suspend 方法  
    val userName = service.safeSuspend().getUserName()  
}  

支持类型

框架对导出代码的类型有一定限制,包括属性类型、方法参数类型和方法返回值类型,具体支持的类型如下表所示

类型

|

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 语言版本的实现可以直接在栈上或原生堆上分配内存,过程直接高效。

  
static napi\_value c\_string\_params(napi\_env env, napi\_callback\_info info) {  
    size\_t argc = 1;  
    napi\_value args[1];  
    napi\_get\_cb\_info(env, info, &argc, args, nullptr, nullptr);  
  
    size\_t length;  
    napi\_get\_value\_string\_utf8(env, args[0], nullptr, 0, &length);  
    char* buf = new char[length + 1]; // 在原生堆上分配  
    napi\_get\_value\_string\_utf8(env, args[0], buf, length + 1, nullptr);  
  
    // ... 使用 buf ...  
    delete[] buf;  
    return nullptr;  
}

相比之下,Kotlin 版本的实现则面临双重内存管理的开销

  
fun napi\_value.asString(): String {  
    // 在原生堆上为长度变量分配空间,状态切换  
    val length = nativeHeap.alloc<ULongVar>()   
    napi\_get\_value\_string\_utf8(OhosFFIManager.globalEnv, this, null, 0.toULong(), length.ptr)  
  
    // 在原生堆上为字符串内容分配空间,UTF16转为UTF8,字符串拷贝赋值,状态切换  
    val buffer = nativeHeap.allocArray<ByteVar>(length.value.toInt() + 1)  
    napi\_get\_value\_string\_utf8(OhosFFIManager.globalEnv, this, buffer, (length.value.toInt() + 1).toULong(), null)  
  
    // 在 Kotlin 托管堆上的第三次分配,UTF8转为UTF16,字符串拷贝赋值  
    return buffer.toKString()  
}  

在一次简单的字符串转换过程中,Kotlin 与 C/C++ 层之间存在 2 次状态切换、 2 次字符串拷贝以及2 次编码转换,字符串跨语言传输已成为业务的性能瓶颈。

针对这种问题我们参考Kotiln内部实现引入了**@GCUnsafeCall** ,它的作用是向编译器声明:与此函数链接的 C++ 实现是“可信的”,它完全理解并遵循 Kotlin 的 GC 规则和调用约定。因此,编译器无需为其生成常规的、带有性能开销的包装代码。

借助**@GCUnsafeCall** ,我们针对字符串场景做了优化,实现机制如下:

  1. 声明内存安全接口:暂时性地、非安全地获取内部内存的写权限,完成数据填充后,再将其作为一个合法的、不可变的对象交还给 Kotlin 。

  
@GCUnsafeCall("Kotlin\_napi\_get\_kotlin\_string\_utf16")  
@Escapes.Nothing  
public external fun napi\_get\_kotlin\_string\_utf16(env: NativePtr, value: NativePtr): String  

  1. 消除中间抽象:直接在 C++ 侧处理 napi\_value 等 Native 句柄和裸指针,彻底抛弃为指针、缓冲区等创建的 Kotlin 包装器,免除 2 次状态切换与 1 次拷贝。

  
OBJ\_GETTER(Kotlin\_napi\_get\_kotlin\_string\_utf16, KNativePtr env, KNativePtr value) {  
    // 获取字符串长度  
    size\_t str\_size;  
    napi\_get\_value\_string\_utf16((napi\_env)env, (napi\_value)value, NULL, 0, &str\_size);  
  
    // 复制字符串内容  
    size\_t str\_size\_read;  
    ArrayHeader* result = AllocArrayInstance(theStringTypeInfo, (int32\_t)str\_size, OBJ\_RESULT)->array();  
    napi\_get\_value\_string\_utf16((napi\_env)env, (napi\_value)value, (char16\_t *)CharArrayAddressOfElementAt(result, 0), str\_size, &str\_size\_read);  
  
    RETURN\_OBJ(result->obj());  
}

优化前后的对比如下,最终长字符串转换耗时能够降低 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、大模型应用、架构师方向工程师,诚邀有志之士与我们一起构建字节跨端方案,服务亿级用户。请扫描下方二维码或点击阅读原文投递简历

picture.image

picture.image

客户端跨端技术专家

抖音客户端架构师

0
0
0
0
评论
未登录
暂无评论