偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

開發(fā)
我們需要建設 Kotlin 與 ArkTS 之間的跨語言交互能力,為開發(fā)者提供便捷、高效的跨語言交互體驗,助力 ByteKMP 在業(yè)務順利落地。

背景

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調用 方法并傳遞參數(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"))) // 調用 d

Kotlin 代碼導出至 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ā)至該對象。大致流程為:

  1. 實現(xiàn)構造函數(shù)橋接方法:該方法會在 ArkTS 側嘗試創(chuàng)建導出類時調用

    a.創(chuàng)建 Kotlin 對象

    b.通過 napi_wrap 將 Kotlin 對象與 ArkTS 對象綁定

  1. 實現(xiàn)類方法的橋接方法

    a.通過napi_unwrap獲取當前 ArkTS 對象綁定的 Kotlin 實例

    b.調用 Kotlin 對象的對應方法

    c.將結果返回給 ArkTS,并處理必要的類型轉換。

  1. 在 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): String

2.  消除中間抽象:直接在 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)差異,提供一致的調用體驗。
責任編輯:龐桂玉 來源: 字節(jié)跳動技術團隊
相關推薦

2022-10-28 13:41:51

字節(jié)SDK監(jiān)控

2023-03-30 16:14:00

數(shù)據(jù)中心IT物聯(lián)網(wǎng)

2018-03-09 15:25:47

IOT語義交叉

2020-03-23 15:15:57

MySQL性能優(yōu)化數(shù)據(jù)庫

2023-10-31 12:50:35

智能優(yōu)化探索

2019-08-12 14:46:56

Web服務器性能

2023-03-22 18:31:10

Android頁面優(yōu)化

2020-10-15 09:10:02

MySQL性能優(yōu)化

2010-11-15 16:33:07

Oracle交互性

2018-02-26 16:41:53

定義IOT語義

2020-07-17 19:55:50

Vue前端性能優(yōu)化

2010-07-06 09:07:09

2025-04-11 03:00:55

2021-09-24 14:02:53

性能優(yōu)化實踐

2019-08-02 11:28:45

HadoopYARN調度系統(tǒng)

2023-09-25 10:26:05

DOMCSS

2025-01-22 08:06:38

C#yield數(shù)據(jù)迭代

2025-06-12 02:22:00

Netflix前端系統(tǒng)

2025-03-27 03:20:00

C#開發(fā)字符串

2024-11-06 08:13:28

點贊
收藏

51CTO技術棧公眾號