Kotlin 與 ArkTS 交互性能與效率優(yōu)化實踐

背景
ByteKMP 是字節(jié)內部基于 KMP(Kotlin Multiplatform) 建設的客戶端跨平臺方案,希望通過 KMP 技術實現(xiàn) Android、鴻蒙、iOS 三端的代碼復用,以此降低開發(fā)成本、提高邏輯與 UI 的多端一致性。
由于抖音鴻蒙版已經(jīng)基于 ArkTS 完成了大部分基礎能力和部分業(yè)務開發(fā),接入 KMP 后需要在充分復用現(xiàn)有 ArkTS 能力的同時,支持業(yè)務側在 ArkTS 場景下調用 KMP 代碼。因此,我們需要建設 Kotlin 與 ArkTS 之間的跨語言交互能力,為開發(fā)者提供便捷、高效的跨語言交互體驗,助力 ByteKMP 在業(yè)務順利落地。
名詞解釋
- KN: Kotlin/Native,ByteKMP 在鴻蒙上采用 Kotlin/Native 技術執(zhí)行 Kotlin 代碼。
 - ArkTS:鴻蒙官方開發(fā)語言。
 - 主模塊:KN 在鴻蒙中以 so 形式集成,因此在 KN 項目中需要一個處于頂層的模塊將依賴的 KMP 代碼打包為目標平臺二進制產(chǎn)物,這個模塊稱為主模塊。
 
Kotlin 調用 ArkTS
在鴻蒙開發(fā)中,系統(tǒng)提供了 NAPI 實現(xiàn) ArkTS 與C/C++ 模塊之間的交互。而 Kotlin/Native 本身就具備與 C/C++ 互操作的能力(基于 cinterop),因此理論上 Kotlin/Native 也能夠通過 NAPI 實現(xiàn)與 ArkTS 互操作。
如何基于 NAPI 調用 ArkTS 代碼
ArkTS 對象在 native 側均以 napi_value 類型表示,包括ArkTS 模塊、類、實例以及方法等。NAPI 提供了一系列方法用于操作 napi_value 對象,比如獲取模塊、獲取模塊導出類、調用 ArkTS 方法等,同時也支持在 native 與 ArkTS 之間進行基礎類型數(shù)據(jù)轉換。
下面以一個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 類型的參數(shù) tag 和 msg
 - 通過napi_call_function調用 d 方法并傳遞參數(shù)
 
// 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. 構造參數(shù) 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. 構造參數(shù)數(shù)組
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 側統(tǒng)一表示為 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 操作
    }
}除了實例對象外還有模塊、類、方法、數(shù)組、基礎類型等對象,由于所有對象都需要napi_value,我們可以定義一個基類 ArkObject 來持有napi_value,其他對象均繼承自 ArkObject 并提供特定的能力。

不過需要注意的是,napi_value 只在一次主線程方法執(zhí)行期間有效,當本次調用結束后就會失效。因此需要通過 napi_ref 來延長它的生命周期,并且在 Kotlin 對象被回收后主動釋放引用避免內存泄漏。ArkObject代碼實現(xiàn)如下:
open class ArkObject(var value: napi_value) {
    // 創(chuàng)建 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 調用實現(xiàn)。與原始的 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"))) // 調用 dKotlin 代碼導出至 ArkTS
如何基于 NAPI 導出 C++ 代碼
NAPI 同樣支持將 native 代碼導出為 TS 聲明(.d.ts) 供 ArkTS 使用。在鴻蒙中,系統(tǒng)會在 native 模塊初始化時注入一個 exports 對象,我們可以通過 NAPI 將屬性、方法或類信息注冊到該對象中,并在 ArkTS 側提供對應的 .d.ts 聲明。當 ArkTS 調用這些代碼時,NAPI 會將調用請求轉發(fā)至 native 側的橋接代碼,從而實現(xiàn) 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);
}根據(jù) NAPI 規(guī)范,我們需要實現(xiàn)一個橋接方法,用于將 ArkTS 的調用請求轉發(fā)至 native 側的具體實現(xiàn)。同時,還需在 exports 對象中將 testLog 方法注冊到對應的橋接方法上。
// 橋接方法,napi 固定簽名
static napi_value bridgeMethod(napi_env env, napi_callback_info info) {
    // 獲取 ArkTS 側傳遞的參數(shù)
    size_t argc = 1;
    napi_value args[1];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
    int value;
    // 將參數(shù)轉換為 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 即可觸發(fā) C++ 側的 testLog 方法執(zhí)行。
基于 KSP 封裝模板代碼
與 C++ 類似,在 Kotlin 中每個需要導出到 ArkTS 的代碼(類、方法、屬性)都必須經(jīng)過以下步驟:
- 定義橋接方法,在方法內處理對象獲取、參數(shù)解析、調用 native 實現(xiàn)、數(shù)據(jù)返回及類型轉換
 - 將橋接方法注冊到 exports
 - 在 index.d.ts 中添加對應聲明
 
這一整套流程同樣既繁瑣又充斥著大量模板代碼,為降低開發(fā)成本,我們考慮通過代碼生成來簡化該流程,其整體設計如下:

橋接代碼代碼生成
橋接代碼生成基于 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 獲取指定包下的所有聲明,再解析注解提取子模塊的橋接信息,從而生成完整的注冊代碼。
完整的處理流程如下圖所示:

導出 Kotlin Class
導出 Kotlin 頂層方法、屬性只需要像上面的代碼一樣提供橋接方法即可,而導出類則會復雜一些,因為需要支持非基礎類型(Class)在 Kotlin 與 ArkTS 之間的相互轉換。
核心思路是:利用 NAPI 提供的 napi_wrap 將 ArkTS 對象與 Kotlin 對象進行綁定;在 ArkTS 調用 Kotlin 時,通過 napi_unwrap 獲取當前 ArkTS 對象綁定的 Kotlin 實例,并將調用轉發(fā)至該對象。大致流程為:
- 實現(xiàn)構造函數(shù)橋接方法:該方法會在 ArkTS 側嘗試創(chuàng)建導出類時調用
 
a.創(chuàng)建 Kotlin 對象
b.通過 napi_wrap 將 Kotlin 對象與 ArkTS 對象綁定
- 實現(xiàn)類方法的橋接方法
 
a.通過napi_unwrap獲取當前 ArkTS 對象綁定的 Kotlin 實例
b.調用 Kotlin 對象的對應方法
c.將結果返回給 ArkTS,并處理必要的類型轉換。
- 在 exports 中注冊導出類,并綁定上面實現(xiàn)的橋接方法
 
以 KotlinClass 為例,生成的橋接代碼如下所示:
class KotlinClass {
    fun test() {
    }
}
// 定義導出類的構造橋接方法
fun constructor(env: napi_env?, info: napi_callback_info?): napi_value? {
    // 獲取當前對象的 this
    val thisArg = info!!.thisArg()
    // 創(chuàng)建 KN 對象
    val kmpClassInstance = KotlinClass()
    val stableRef = StableRef.create(kmpClassInstance).asCPointer()
    // 將 Kotlin 對象與 ArkTS 對象綁定
    napi_wrap(env, thisArg, stableRef, null, null, null)
    // 返回構造參數(shù)
    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 調用的場景中,經(jīng)常會涉及 回調 或 ArkTS 能力注入。以接口回調為例,Kotlin 側可能會設計出如下代碼:
interface Callback {
    fun onSuccess(result: String)
}
fun requestNetwork(callback: Callback) {
    // 忽略請求代碼
    callback.onSuccess("success")
}requestNetwork方法導出至 ArkTS ,callback 由 ArkTS 實現(xiàn)并在調用時傳入,這樣在后續(xù)請求結束時 Kotlin 側可以將結果回調至 ArkTS。
這種場景下接口回調本質上是一次 Kotlin 調用 ArkTS 的過程,我們可以通過前面設計的能力來實現(xiàn),代碼如下:
fun requestNetwork(callback: napi_value) {
    // 忽略請求代碼
    val result = arkString("success")
    ArkInstance(callback).getFunction("onSuccess")?.call(arrayOf(result))
}不過這種實現(xiàn)方式存在一個明顯的問題:缺少強制約束。ArkTS 與 Kotlin 之間的通信完全依賴雙方的“約定”,一旦任意一方修改了接口定義或編寫代碼時出現(xiàn)錯誤,往往難以及時發(fā)現(xiàn)問題,排查成本也會大幅提升。為了支持這種「Kotlin 側定義接口,由 ArkTS 實現(xiàn)」的場景,我們需要實現(xiàn) Kotlin 接口的導出能力。
核心實現(xiàn)思路是:
- 基于 KSP 為接口自動生成一個實現(xiàn)類,在該類中持有 napi_value
 - 根據(jù)方法簽名信息,將 Kotlin 方法的調用轉發(fā)至 napi_value 對應的 ArkTS 方法上
 - 將接口定義導出到 ArkTS,確保導出的代碼具有明確定義和約束
 
根據(jù)這個思路,編譯期將為 Callback 接口生成如下實現(xiàn):
// 接口實現(xiàn)類,為 ArkTS 側實現(xiàn)的 Callback 對象做一層代理
class JsImportInterfaceBinding_Callback(val instance: ArkInstance) : Callback {
    override fun onSuccess(result:String): Unit {
        // 將 onSuccess 方法轉發(fā)到 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,默認情況下框架會使用無參數(shù)構造函數(shù)來構造 KN 對象,如果想指定有參構造函數(shù)可以搭配@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 實現(xiàn)的接口,不支持導出接口屬性,且默認導出所有方法不需要使用@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側實現(xiàn)接口并傳遞給 Kotlin
injectArkTsService({
    getUserName: () => {
        return ...
    }
})線程安全
由于在 KN 側調用@ArkTsExportInterface方法涉及到 NAPI 操作, 而 NAPI 調用需要保證在主線程執(zhí)行,否則會發(fā)生崩潰。為了避免業(yè)務側過多的關注線程切換邏輯,框架提供了以下兩種方式實現(xiàn)自動線程切換以保證線程安全。
1. @ArkTsThreadSafe
對于非 suspend 方法,如果業(yè)務想在調用時不去關注線程切換問題,可以為該方法標注@ArkTsThreadSafe,框架會在方法實現(xiàn)內使用 runBlocking 切換到主線程調用并阻塞當前線程直到結果返回。
// Kotlin
@ArkTsExportInterface
interface ArkTsService {
    @ArkTsThreadSafe
    fun getUserName(): String = ""
}2. safeSuspend()
同時框架也提供了接口對象的擴展方法safeSuspend(),用于獲取該接口的 suspend 包裝類,該類提供了當前接口所有方法的 suspend 版本,業(yè)務可以在協(xié)程中安全使用。
val service: ArkTsService = xxx
GlobalScope.launch {
    // 調用生成的 suspend 方法
    val userName = service.safeSuspend().getUserName()
}支持類型
框架對導出代碼的類型有一定限制,包括屬性類型、方法參數(shù)類型和方法返回值類型,具體支持的類型如下表所示:
類型  | 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  | 
協(xié)程  | suspend function  | Promise  | 
性能優(yōu)化
Kotlin Native 基于 LLVM 編譯器基礎設施構建,它將 K2 中間表示(IR)轉換為 LLVM IR,其語言后端與 C/C++ 保持一致。這種設計使得 Kotlin Native 的理論性能有潛力接近 C 語言的水平。然而,為了維護 Kotlin 語言的內存安全特性和垃圾回收(GC)機制,Kotlin Native 引入了一套相對厚重的運行時系統(tǒng)。這套運行時在提供便利性的同時,也帶來了不可忽視的開銷。
當 Kotlin 通過 cinterop 調用 NAPI 時,這種雙重內存管理模型的開銷會進一步放大,在字符串轉換場景尤為明顯。一個 C 語言版本的實現(xiàn)可以直接在棧上或原生堆上分配內存,過程直接高效。
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 版本的實現(xiàn)則面臨雙重內存管理的開銷。
fun napi_value.asString(): String {
    // 在原生堆上為長度變量分配空間,狀態(tài)切換
    val length = nativeHeap.alloc<ULongVar>() 
    napi_get_value_string_utf8(OhosFFIManager.globalEnv, this, null, 0.toULong(), length.ptr)
    // 在原生堆上為字符串內容分配空間,UTF16轉為UTF8,字符串拷貝賦值,狀態(tài)切換
    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 次狀態(tài)切換、 2 次字符串拷貝以及2 次編碼轉換,字符串跨語言傳輸已成為業(yè)務的性能瓶頸。
針對這種問題我們參考Kotiln內部實現(xiàn)引入了@GCUnsafeCall,它的作用是向編譯器聲明:與此函數(shù)鏈接的 C++ 實現(xiàn)是“可信的”,它完全理解并遵循 Kotlin 的 GC 規(guī)則和調用約定。因此,編譯器無需為其生成常規(guī)的、帶有性能開銷的包裝代碼。
借助@GCUnsafeCall,我們針對字符串場景做了優(yōu)化,實現(xiàn)機制如下:
1. 聲明內存安全接口:暫時性地、非安全地獲取內部內存的寫權限,完成數(shù)據(jù)填充后,再將其作為一個合法的、不可變的對象交還給 Kotlin 。
@GCUnsafeCall("Kotlin_napi_get_kotlin_string_utf16")
@Escapes.Nothing
public external fun napi_get_kotlin_string_utf16(env: NativePtr, value: NativePtr): String2. 消除中間抽象:直接在 C++ 側處理 `napi_value` 等 Native 句柄和裸指針,徹底拋棄為指針、緩沖區(qū)等創(chuàng)建的 Kotlin 包裝器,免除 2 次狀態(tài)切換與 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());
}優(yōu)化前后的對比如下,最終長字符串轉換耗時能夠降低 90%。
對比維度  | 優(yōu)化前  | 優(yōu)化后  | 
指針/句柄處理  | 創(chuàng)建 Kotlin 對象封裝  | C++側直接使用原始值  | 
內存分配  | 2+ 次 (包裝器, C緩沖, Kotlin對象)  | 1 次 (僅最終 Kotlin 對象)  | 
數(shù)據(jù)拷貝  | 2 次  | 1 次  | 
編碼轉換  | 2 次 (UTF-16 -> UTF-8 -> UTF-16)  | 0 次  | 
長字符串轉換耗時  | 25.4 ms  | 2.4 ms (-90.5%)  | 
未來規(guī)劃
- 實現(xiàn) ArkTS 與 KMP 之間的字符串共享,避免拷貝操作,徹底解決字符串傳輸性能問題。
 - 解決多線程限制問題,抹平平臺(Android、Harmony)差異,提供一致的調用體驗。
 















 
 
 















 
 
 
 