本文记录 Featured 接入 OpenHarmony 的完整过程,覆盖特性开关库盘点、鸿蒙 kotlinx 制品线选型、真实引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

Featured 是一套 Kotlin Multiplatform 的「类型安全特性开关 / 配置管理」库:ConfigParam<T> 声明一个开关,ConfigValues 按「本地覆盖 → 远端 Provider → 默认值」的顺序解析,observe 把变化暴露成 Flow,Provider 分为 Local / Remote / InMemory 三层,featured-compose 再把开关接进 Compose,featured-testing 提供测试替身。上游支持 Android、iOS(经 SKIE)和 JVM。本次适配与已审仓库的「根构建替换」路线不同:Featured 的主构建绑着 AGP 9.2.1、Compose 1.10.3、SKIE、kover 与 mavenPublish,把全局 Kotlin 换成鸿蒙发行版会连带整条发布链,因此主构建原样不动,ohos 构建收敛成 example/nativeApp 里的一套独立 Gradle 构建,用 srcDirs 直接编译 core 与 featured-platform 的真实源码,产出双 ABI 的 libohosfeatured.so;ConfigParam、解析顺序、覆盖与重置语义全部是上游原件,ArkTS 只负责发 JSON、收 JSON、刷新页面。

项目地址: AtomGit/oh-tpc/Featured.git

上游仓库: AndroidBroadcast/Featured

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

Featured 要解决的问题是「同一份开关定义,在 Android、iOS、桌面和鸿蒙上行为一致」:业务侧只写一个 ConfigParam<T>(例如 ConfigParam("new_checkout", false)),读取统一走 configValues.getValue(param),返回的 ConfigValue 带 value 与 source(LOCAL / REMOTE / DEFAULT);运行时可以 override 强制某值、resetOverride 回到默认、observe 订阅变化;本地持久化与远端拉取被抽成 LocalConfigValueProvider / RemoteConfigValueProvider 两个接口,Android 用 DataStore、iOS 用 NSUserDefaults、JVM 用 java.util.prefs,各自在平台的 Provider 模块里实现。

这些能力全部落在 commonMain,是平台无关的纯 Kotlin。OpenHarmony 应用同样需要这套能力(灰度开关、A/B、紧急止血都依赖它),但上游构建(Kotlin 2.3.21 / Gradle 9.4.1 / AGP 9.2.1)不会生成 OpenHarmony 变体——ohosArm64() / ohosX64() 这两个 target 只有鸿蒙版 Kotlin 发行版才认识。

如果只把示例页面改成 ArkTS,页面能显示按钮,却无法证明「开关解析逻辑在 OpenHarmony ARM64 设备上真的编译、链接并执行过」。适配必须同时验证解析引擎、Provider 抽象、Kotlin/Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链冲突主构建的 AGP 9.2.1 / Compose 1.10.3 / SKIE / kover / mavenPublish 与鸿蒙 Kotlin 发行版不在同一条版本线上,不能共用一个 Gradle 构建。
依赖矩阵core 依赖 kotlinx-coroutines,鸿蒙持久化依赖 kotlinx-io,两者都必须有 ohos 变体才能解析。
平台 API 差异Kotlin/Native 没有 java.io.File,JVM 那套文件读写照抄不过去;logProviderError 这类 expect 在 ohos 上缺 actual。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
语义跨不了桥getValue / override / resetOverride 都是 suspend,observe 返回 Flow,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此本项目把边界划在:真实解析引擎 + 鸿蒙持久化 Provider 的 ohos 编译、独立 ohos 构建、C ABI/N-API 桥接层。开关语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。

1.2 库提供的能力

演示页把桥接的全部入口摆在一屏里,每个入口都对应上游一个真实 API:

  • 读取(get):configValues.getValue(param),返回 ConfigValue(value, source)——source 直接来自引擎的解析结果(LOCAL / DEFAULT),不是页面自己编的;
  • 覆盖(set):configValues.override(param, value),本地 Provider 写成功后,同一 key 再读就变成 LOCAL;
  • 重置(reset):configValues.resetOverride(param),删掉本地覆盖,再读回落到默认值,source 回到 DEFAULT;
  • 列举(list):桥维护一张 key → ConfigParam<Any> 的注册表,逐个 getValue 后输出 key / type / default / value / source,用来证明多个开关共享同一个 ConfigValues 实例。

响应统一是扁平 JSON:

字段作用
ok操作是否成功;失败时 error 带原文。
valueget 返回当前值;list 返回数组;类型由 type 说明。
sourceLOCAL(有本地覆盖)或 DEFAULT(回落默认)——引擎解析结果。
typebool / int / long / double / string / list。

演示页用的是 InMemoryConfigValueProvider,所以「覆盖」在进程内生效、重启后消失;真实应用换成 defaultLocalProvider() 即落盘(见 1.3 与 8.2)。

1.3 实现适配

维度要求
代码复用core 与 featured-platform 的 commonMain 文件原样编译,不改一行;新增源文件只有 4 个(core 一个 ohos actual、featured-platform 一个 actual 加一个文件型 Provider、nativeApp 一个 Bridge.kt)。
平台目标ohos 构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,产出双 ABI libohosfeatured.so。
构建隔离主构建零改动;ohos 工具链收敛在 example/nativeApp 的独立构建里,两套构建互不感知。
桥接稳定只用 1 个 C ABI 符号 + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律版本主体与官方一致,只换到 ohos 制品线(换线不换版)。
UI 完整单页覆盖全部桥接入口:三个开关、类型自适应编辑器、读取 / 覆盖 / 重置 / 列举、状态行与原始 JSON 回显。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Featured 的渲染能力在 featured-compose 里,本次不参与鸿蒙构建;鸿蒙侧只跑逻辑层,featured-compose / featured-debug-ui / sample 一律留盘。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与 expect/actual、确认 kotlinx ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 独立 ohos 构建、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版
第 3 阶段:桥协议建模     ── get/set/reset/list JSON 契约、ConfigParam 注册表、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 类型自适应编辑器、四按钮、状态行和原始 JSON 回显
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
core/                                 上游解析引擎(进 ohos 构建,纯 commonMain)
core/src/ohosMain/                    新增:logProviderError 的 ohos actual(1 个文件)
featured-platform/                    上游平台层(expect defaultLocalProvider)
featured-platform/src/ohosMain/       新增:ohos actual + 文件型 Provider(2 个文件)
featured-testing/                     测试替身,留盘不进 ohos 构建
featured-compose/ featured-debug-ui/  Compose 渲染层,留盘不进 ohos 构建
providers/                            datastore / sharedpreferences / nsuserdefaults / javaprefs / firebase / configcat,全部留盘
sample/                               上游示例,留盘不进 ohos 构建
example/nativeApp/                    独立 ohos 构建 + Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
doc/                                  本文与效果图
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)

关键决策在目录上就能看出来:仓库根目录的构建文件一行没改。settings.gradle.kts、gradle/libs.versions.toml、build.gradle.kts 全部保持上游状态,example/nativeApp 自带一套 settings.gradle.kts + gradle.properties + build.gradle.kts,从仓库根用 -p example/nativeApp 触发。

1.2 工具链矩阵
项目配置
Kotlin(ohos 构建)HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
Kotlin(主构建)官方 2.3.21(Android / iOS / JVM / Compose,本次未改动)
JDK21(ohos 构建用 org.gradle.java.home 固定,避开主构建的 JDK 17 toolchain)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle9.4.1(沿用上游 wrapper)
kotlinxkotlinx-coroutines-core 1.11.0-0.1.0-07、kotlinx-io-core 0.9.1-0.1.0-07(ohos 制品线)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.oh.tpc.cmp.featured,versionName 1.2.0-ohos.1
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块依赖形态处置
core纯 Kotlin + coroutines,零 UI、零 AGP整体编译(commonMain 原样 + 1 个 ohos actual)
featured-platform纯 Kotlin + coroutines + kotlinx-io(ohos actual 侧)整体编译(commonMain 原样 + 2 个 ohos 文件)
featured-testing纯 Kotlin 测试替身留盘(不进 ohos 构建)
featured-compose / featured-debug-uiCompose Multiplatform 1.10.3留盘(与 Compose 渲染层排除策略一致)
providers/*各平台专有(DataStore / SharedPreferences / NSUserDefaults / java.util.prefs / Firebase / ConfigCat)留盘(鸿蒙侧由新 Provider 承担)
sample/*依赖 Compose 与 Android留盘

全库只有两处 expect:

// core/src/commonMain/.../LogProviderError.kt
internal expect fun logProviderError(e: Throwable)

// featured-platform/src/commonMain/.../DefaultLocalProvider.kt
public expect fun defaultLocalProvider(): LocalConfigValueProvider

上游给了 android / jvm / ios 三套 actual,ohos 补两套。logProviderError 刻意不依赖任何 HarmonyOS NDK 绑定,直接 println——在 NAPI 宿主里 stdout 会被应用日志收集,等于零成本落地:

// core/src/ohosMain/kotlin/dev/androidbroadcast/featured/LogProviderError.kt
internal actual fun logProviderError(e: Throwable) {
    println("Featured: Provider error — ${e.stackTraceToString()}")
}

真正有分量的是第二个 actual。Kotlin/Native 没有 java.io.File,鸿蒙上也没有 DataStore 等价物,所以 featured-platform 的 ohos 侧自己实现了一个文件型 Provider:一行一条 key|type|value,整文件重写,读写都走 kotlinx-io 的 SystemFileSystem。

// featured-platform/src/ohosMain/.../OhosPreferencesConfigValueProvider.kt(节选)
public class OhosPreferencesConfigValueProvider(
    private val filePath: String = "featured_flags.prefs",
) : LocalConfigValueProvider {

    private val path = Path(filePath)

    override suspend fun <T : Any> get(param: ConfigParam<T>): ConfigValue<T>? {
        val entry = readAll()[param.key] ?: return null
        val value = decode(entry.first, entry.second) ?: return null
        return ConfigValue(value as T, ConfigValue.Source.LOCAL)
    }

    override suspend fun <T : Any> set(param: ConfigParam<T>, value: T) {
        val map = readAll().toMutableMap()
        map[param.key] = encode(value)       // type to value.toString()
        writeAll(map)
        changedKeyFlow.tryEmit(param.key)
    }

    override fun <T : Any> observe(param: ConfigParam<T>): Flow<ConfigValue<T>> = flow {
        emit(get(param) ?: ConfigValue(param.defaultValue, ConfigValue.Source.DEFAULT))
        emitAll(changedKeyFlow.filter { it == param.key }.map { get(param) ?: /* 回落默认 */ })
    }
}

配一个 actual 让 defaultLocalProvider() 在鸿蒙上开箱可用,另加一个可传沙箱路径的重载:

// featured-platform/src/ohosMain/.../DefaultLocalProvider.kt
public actual fun defaultLocalProvider(): LocalConfigValueProvider = OhosPreferencesConfigValueProvider()

public fun defaultLocalProvider(filePath: String): LocalConfigValueProvider =
    OhosPreferencesConfigValueProvider(filePath)

真实应用应当传 ArkTS 侧取到的 filesDir,让覆盖落在应用沙箱而不是进程工作目录:

val provider = defaultLocalProvider("/data/storage/el2/base/haps/entry/files/featured_flags.prefs")
val configValues = ConfigValues(localProvider = provider)
1.4 页面边界
标题区          Featured · OpenHarmony / KMP 特性开关库 · Kotlin/Native ohosArm64/ohosX64
Flag 选择       三个 chip:new_checkout / max_retries / theme(当前项高亮)
覆盖值区        按选中 flag 的类型自适应:bool → Toggle,int → Slider + 数值,string → TextInput
操作区          读取 / 覆盖 / 重置 三按钮 + 全宽「列出全部 Flag」
结果列表        每行 key、type·默认值、当前值、source(DEFAULT / LOCAL)
状态行          最近一次操作结果(如"读取成功 value=true source=LOCAL type=bool")
原始 JSON       最近一次 Native 返回的响应原文,用于排查

页面不展示文件路径,也不做持久化演示——Demo 刻意用 InMemoryConfigValueProvider,避免真机路径差异干扰「桥通不通」这一核心验收点。

第 2 阶段:目标与依赖打通

2.1 制品线验证(本适配的成败手)

先确认社区 Maven(https://maven.eazytec-cloud.com)上有 ohos 的 kotlinx 制品,用 .module 元数据逐个验证变体:

org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07
  variants: ohosArm64Api-published / ohosX64Api-published / jvmApi-published / jvmRuntime-published
org.jetbrains.kotlinx:kotlinx-io-core:0.9.1-0.1.0-07
  variants: ohosArm64Api-published / ohosX64Api-published / jvmApi-published / jvmRuntime-published

三个结论:其一,两个依赖的版本主体与官方一致(coroutines 1.11.0、kotlinx-io 0.9.1),只是仓库换线,commonMain 里的 import 一行都不用改;其二,ohos 制品同时发布 jvm 变体,所以 jvm() 交叉验证可以用同一个版本号;其三,Maven Central 上的 kotlinx-io-core:0.9.1 没有 ohos 变体,版本必须带 -0.1.0-07 后缀才解析得到——版本主体写对、后缀写错,解析照样失败。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)

社区早期的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose 一批用的是它),后来还有 2.3.21-1.0.0。本适配最后落到 2.4.20-0.1.0-07,原因不是"越新越好",而是版本必须跟着依赖走:coroutines 1.11.0-0.1.0-07 的 klib 是用 stdlib 2.4.20 那代编译器产出的,用 2.3 线读会直接报元数据不兼容:

Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.4.0, expected version is 2.3.0 or less.

另一个现实约束是制品可达性:本机 konan 缓存与 Gradle 模块缓存里实际存在的是 2.4.20-0.1.0-07 这条线(连 compose-compiler-gradle-plugin 的同线制品都在),2.3.21-1.0.0 在本机不存在——照抄别人的版本号会卡在"下载一个不存在的编译器"。版本号必须对着仓库实际发布核对,不能凭历史结论。

2.3 独立 ohos 构建

这是与 cmp_qrose / KStore 路线最大的不同。那些库没有 AGP、没有 Compose、没有发布链,直接把根构建换成 ohos 构建最省事;Featured 不行——根构建里 com.android.kotlin.multiplatform.library 9.2.1、compose-multiplatform 1.10.3、SKIE、kover、mavenPublish 都绑在官方 Kotlin 2.3.21 上,换线就得重新验证整条矩阵。

所以 ohos 构建被收进 example/nativeApp,自带仓库与根项目名:

// example/nativeApp/settings.gradle.kts
pluginManagement {
    repositories {
        // HarmonyOS Kotlin (ohosArm64 / ohosX64) — 必须置前,否则配置期直接
        // "Unresolved reference: ohosArm64"
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        google()
        mavenCentral()
    }
}

rootProject.name = "featured-native"

构建脚本用 srcDirs 把上游源码"借"进来,一个模块同时承担库编译与动态库产出:

// example/nativeApp/build.gradle.kts
plugins {
    kotlin("multiplatform") version "2.4.20-0.1.0-07"
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()                     // 交叉验证 target
    ohosArm64()
    ohosX64()

    targets.withType<KotlinNativeTarget>().configureEach {
        compilerOptions { optIn.add("kotlin.experimental.ExperimentalNativeApi") }
        binaries.sharedLib { baseName = "ohosfeatured" }
    }

    sourceSets {
        commonMain {
            kotlin.srcDirs(
                "../../core/src/commonMain/kotlin",
                "../../featured-platform/src/commonMain/kotlin",
            )
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07")
                implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.9.1-0.1.0-07")
            }
        }
        getByName("ohosMain") {
            kotlin.srcDirs(
                "../../core/src/ohosMain/kotlin",
                "../../featured-platform/src/ohosMain/kotlin",
                "src/ohosMain/kotlin",
            )
        }
    }
}

gradle.properties 里把 JDK 与 K/N 行为钉死,避免依赖调用方环境:

org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot
kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true

实测:

./gradlew.bat -p example/nativeApp :linkDebugSharedOhosArm64 :linkDebugSharedOhosX64
# BUILD SUCCESSFUL in 1m 30s
# 8 actionable tasks: 7 executed, 1 up-to-date

说明:org.gradle.java.home 是机器相关配置,换机器/换系统时需要改成本机 JDK 21 路径,或删掉该行交给 JAVA_HOME。这是为了让 ohos 构建不继承主构建的 JDK 17 toolchain 而做的显式隔离。

2.4 源集复用而不是复制源码

srcDirs 指向 ../../core/src/commonMain/kotlin,意味着 ohos 构建编译的是仓库里那一份源码,不是复制品。好处有三点:源码只有一份,不存在"改上游忘了改 fork"的漂移;新增的 ohosMain 目录同时被 core、featured-platform 和 nativeApp 三处复用;主构建完全不知道 ohos 的存在,core 里多一个 ohosMain 目录对 Gradle 而言只是一个未被任何 target 引用的普通目录。

第 3 阶段:桥协议建模

3.1 JSON 契约
dev.androidbroadcast.featured/**(上游引擎 + Provider 抽象,原样编译)
        ↓ ConfigValues / ConfigParam<T> / ConfigValue.Source
Bridge.kt(@CName("FeaturedEval"),扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API featuredEval(request) → string
        ↓
ArkUI Index.ets(单页滚动)

请求四种,响应三种:

请求 : {"action":"get",   "key":"new_checkout","type":"bool"}
       {"action":"set",   "key":"new_checkout","type":"bool","value":true}
       {"action":"reset", "key":"new_checkout","type":"bool"}
       {"action":"list"}
响应 : {"ok":true,"type":"bool","source":"LOCAL","value":true}
       {"ok":true,"type":"list","value":[{"key","type","default","value","source"}, ...]}
       {"ok":false,"error":"..."}

一个入口函数覆盖四种动作,action 缺省为 get。这样 N-API 层只需要注册一个方法,ArkTS 侧只需要一个 import。

3.2 为什么用注册表 + ConfigParam<Any>

ConfigValues.getValue 是泛型方法,而 JSON 进来时类型信息只有字符串。桥的做法是维护一张 key → ConfigParam<Any> 的注册表,首次遇到未知 key 时按 type 现场造一个:

private val configValues = ConfigValues(localProvider = InMemoryConfigValueProvider())

private val registry: MutableMap<String, ConfigParam<Any>> = linkedMapOf(
    "new_checkout" to (ConfigParam("new_checkout", true) as ConfigParam<Any>),
    "max_retries" to (ConfigParam("max_retries", 3) as ConfigParam<Any>),
    "theme" to (ConfigParam("theme", "system") as ConfigParam<Any>),
)

private fun paramFor(key: String, type: String): ConfigParam<Any> {
    registry[key]?.let { return it }
    val p = makeParam(key, type, typeDefault(type)) ?: error("unsupported type '$type'")
    registry[key] = p
    return p
}

同一个 key 反复读写必须复用同一个 ConfigParam 实例——注册表就是这层保证;list 直接遍历注册表,所以页面看到的三行正好是三个已注册的开关。

suspend 统一用 runBlocking 收进同步 C ABI:

@CName("FeaturedEval")
fun featuredEval(request: String): String = runBlocking {
    val json = MiniJson.parse(request)
    val action = json.str("action").ifBlank { "get" }
    when (action) {
        "list" -> listAll()
        "get", "set", "reset" -> {
            val key = json.str("key")
            if (key.isBlank()) return@runBlocking fail("missing 'key'")
            val type = json.str("type").ifBlank { "string" }
            val param = paramFor(key, type)
            when (action) {
                "set" -> {
                    val value = coerce(type, json.str("value"))
                        ?: return@runBlocking fail("cannot coerce to $type")
                    configValues.override(param, value)
                }
                "reset" -> configValues.resetOverride(param)
            }
            readOne(param, type)
        }
        else -> fail("unknown action '$action'")
    }
}

鸿蒙线的 Dispatchers.Main 跨线程派发有已知问题(社区适配时记录过),桥全程不碰 Main;runBlocking 只包裹 suspend 调用,引擎本身的解析逻辑是纯计算,不涉及调度器。

3.3 为什么自带 MiniJson

请求报文是扁平的(没有嵌套对象、没有数组),字段值只有布尔/数字/字符串三种。为此引一个序列化运行时进 .so 不划算,所以桥里带了一个约 50 行的 MiniJson:按 , 切分(跳过字符串内的逗号)、按第一个 : 切 key/value、处理 \" 与 \\ 转义。响应侧由 jsonValue() / jsonString() 手工拼装,规则是"数字和布尔裸输出、字符串加引号并转义"。

代价是桥只接受扁平请求——这正好是这份协议的全部需求;收益是 .so 里没有任何 JSON 运行时,体积和依赖都最小。

3.4 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

} catch (t: Throwable) {
    fail(t.message ?: t.toString())     // {"ok":false,"error":"..."}
}

非法 JSON、缺 key、type 不认识、值无法 coerce、Provider 抛错,全部走同一个 ok:false 通道,ArkTS 侧统一渲染成一行红字。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libfeatured_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosfeatured.so)
  │ FeaturedEval
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制开关语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("FeaturedEval")
fun featuredEval(request: String): String

只有一个符号:

C ABI 符号ArkTS 方法覆盖内核
FeaturedEvalfeaturedEval(req)getValue / override / resetOverride / 注册表列举

@CName 来自 kotlin.native.CName——这一点很容易踩:写成 kotlin.experimental.CName 编译期就报未解析,ExperimentalNativeApi 的 opt-in 也需要在编译参数里加上。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"featuredEval", nullptr, NapiFeaturedEval, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};

回调三步走:napi_typeof 校验入参必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并把返回串包成 napi_value。KN 返回的字符串必须经符号表归还:

libohosfeatured_ExportedSymbols *symbols = libohosfeatured_symbols();
if (symbols != nullptr && symbols->DisposeString != nullptr) {
    symbols->DisposeString(raw);     // 绝不 free()
}

libohosfeatured_api.h 按社区已审仓库同款手写,只声明 ExportedSymbols 结构与业务符号,不拖整份生成头(生成头里有几百行内部结构,跟着 K/N 版本漂移)。

4.4 CMake 和类型声明
if(NOT DEFINED OHOS_ARCH OR OHOS_ARCH STREQUAL "")
    message(FATAL_ERROR "OHOS_ARCH is empty; expected arm64-v8a or x86_64")
endif()
set(KN_SO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${OHOS_ARCH})

if(NOT EXISTS ${KN_SO})
    message(FATAL_ERROR "Missing ${KN_SO}. Run :example:nativeApp:linkDebugSharedOhosArm64 / OhosX64 first.")
endif()

add_library(ohosfeatured SHARED IMPORTED)
set_target_properties(ohosfeatured PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(featured_napi SHARED napi_init.cpp)
target_link_options(featured_napi PRIVATE "-Wl,-rpath,\$ORIGIN")
target_link_libraries(featured_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohosfeatured)

$ORIGIN rpath 加 POST_BUILD 拷贝,保证 NAPI so、KN so、libc++_shared.so 三件套同目录,dlopen 时互相可见。

// entry/src/main/cpp/types/libfeatured_napi/index.d.ts
export const featuredEval: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 请求构造与状态刷新
interface EvalRequest {
  action: string;
  key?: string;
  type?: string;
  value?: boolean | number | string;
}

private callEval(req: EvalRequest): EvalResult | undefined {
  this.lastJson = '';
  try {
    const raw: string = featuredEval(JSON.stringify(req));
    const parsed: EvalResult = JSON.parse(raw) as EvalResult;
    this.lastJson = raw;
    return parsed;
  } catch (e) {
    this.status = `异常: ${...}`;
    return undefined;
  }
}

四个动作各对应一次调用,结果写回 @State status 与 @State lastJson:

const req: EvalRequest = { action: 'get', key: this.selectedKey, type: this.selectedType };
const r = this.callEval(req);
this.status = `读取成功 value=${this.fmt(r.value)} source=${r.source ?? '-'} type=${r.type ?? '-'}`;
5.2 页面交互

三项 flag 用 chip 横排,选中项深色高亮;覆盖值区按类型自适应:bool 给 Toggle、int 给 Slider(0–10,步长 1)、string 给 TextInput。切换 flag 时把当前 flag 的默认值填进编辑器,避免拿上一个 flag 的残留值去覆盖。

「列出全部」把响应里的数组映射成 FlagRow[],每行左侧 key + type · 默认 xxx,右侧当前值 + source(LOCAL 用红色标出,与 DEFAULT 形成对比)。下方状态行给中文结论,再下一行是响应原文——验收时"人话"和"机器话"都能看到。

5.3 两个 ArkTS/ArkUI 坑

第一个是编译期的:

ERROR: 10605038 ArkTS Compiler Error
arkts-no-untyped-obj-literals

ArkTS 严格模式不允许无类型对象字面量,const req = { action: 'get', ... } 直接编译失败。解法是给请求声明 interface 并标注类型(const req: EvalRequest = {...},函数签名 callEval(req: EvalRequest))。

第二个是运行期的:页面最初用 private get selectedType() 计算当前 flag 类型,build() 里 if (this.selectedType === 'bool') 选分支。结果是切换 flag 后分支不刷新——选中 bool 开关却仍显示字符串输入框。ArkUI 对 struct getter 的依赖追踪不可靠,改成状态驱动即可:@State selectedType,在 selectFlag() 里显式赋值,build() 直接读该字段。

经验一句话:build() 里的条件判断只用 @State,不要用 getter。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── settings.gradle.kts                   独立 ohos 构建(eazytec 双仓置前,rootProject=featured-native)
│   ├── gradle.properties                     JDK 21、kotlin.native.* 开关
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosfeatured)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName("FeaturedEval") + MiniJson
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.oh.tpc.cmp.featured、versionName 1.2.0-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohosfeatured_api.h
        ├── src/main/cpp/types/libfeatured_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
# 1. 产出双 ABI 动态库(会自动拷贝三件套到 harmonyApp/entry/libs/<abi>)
./gradlew.bat -p example/nativeApp :linkDebugSharedOhosArm64 :linkDebugSharedOhosX64

# 2. 构建 HAP(本机可用 DevEco 自带 hvigor 直接跑,不必开 IDE)
cd example/harmonyApp
hvigorw --mode module -p product=default -p module=entry@default assembleHap --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

hdc list targets -v
hdc -t <设备序列号> install -r \
  entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b org.oh.tpc.cmp.featured -m entry

四、完整代码对照

4.1 整体架构

dev.androidbroadcast.featured/**(上游引擎原样编译)
    │ ConfigValues / ConfigParam<T> / ConfigValue.Source / Provider 抽象
dev.androidbroadcast.featured.platform/**(上游平台层 + ohos actual)
    │ defaultLocalProvider() / OhosPreferencesConfigValueProvider(kotlinx-io)
Bridge.kt + MiniJson
    │ FeaturedEval
libohosfeatured.so(arm64-v8a + x86_64)
    │ featuredEval
libfeatured_napi.so
    │ JSON parse + @State 更新
Index.ets
    ▼
ArkUI 真机页面(单页滚动)

4.2 文件清单

文件职责
example/nativeApp/settings.gradle.kts独立 ohos 构建、eazytec 双仓置前、featured-native
example/nativeApp/gradle.propertiesJDK 21、kotlin.native.ignoreDisabledTargets / downloadFromMaven
example/nativeApp/build.gradle.kts双 ABI sharedLib、srcDirs 复用上游源码、cxxShared 定位 libc++、copyKnSo* 回填
core/src/ohosMain/.../LogProviderError.ktlogProviderError 的 ohos actual(零依赖 println)
featured-platform/src/ohosMain/.../OhosPreferencesConfigValueProvider.kt文件型本地 Provider(key|type|value 行格式)
featured-platform/src/ohosMain/.../DefaultLocalProvider.ktdefaultLocalProvider() 的 ohos actual(含沙箱路径重载)
nativeApp/.../Bridge.kt1 个 C ABI、runBlocking 包裹、注册表、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(中英双份)
doc/本文与真机效果图

4.3 关键 API 对照

层次API作用
引擎ConfigParam(key, default)声明一个开关(类型由默认值推断)
引擎ConfigValues.getValue(param)按「本地 → 远端 → 默认」解析,返回 ConfigValue
引擎ConfigValues.override / resetOverride强制值 / 清除覆盖
引擎ConfigValues.observe(param)Flow<ConfigValue<T>> 订阅(不跨 C ABI,见 8.2)
平台defaultLocalProvider() / defaultLocalProvider(path)鸿蒙文件型持久化 Provider
平台OhosPreferencesConfigValueProviderkotlinx-io + SystemFileSystem 实现
NativeFeaturedEvalC ABI JSON 导出
N-APIfeaturedEval(req)向 ArkTS 暴露能力
ArkTScallEval(req: EvalRequest)触发操作并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(featuredEval(JSON.stringify({
  action: 'set', key: 'new_checkout', type: 'bool', value: true
})));
// res.ok === true, res.source === 'LOCAL', res.value === true
"set" -> {
    val value = coerce(type, json.str("value")) ?: return@runBlocking fail("cannot coerce")
    configValues.override(param, value)
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:独立 ohos 构建,而不是替换根构建

cmp_qrose / KStore 的根构建里没有 AGP、Compose 和发布链,整个换掉最省事。Featured 的根构建背着 AGP 9.2.1、Compose 1.10.3、SKIE、kover、mavenPublish、六个 Provider 模块和五个 sample,换掉全局 Kotlin 等于把整条发布链重新验一遍。独立构建把风险关进 example/nativeApp 一个目录:主构建零改动、ohos 侧自由选版本,代价是多一套 settings.gradle.kts 和一条 -p 参数。

决策 2:工具链钉 2.4.20-0.1.0-07,而不是沿用已审仓库的 2.3.21-1.0.0

版本由依赖反推:coroutines 1.11.0-0.1.0-07 的 klib 元数据是 2.4 代,2.3 线读不了;而且本机可解析到的鸿蒙 Kotlin 制品只有 2.4.20 这条线,2.3.21-1.0.0 解析不到,构建会卡在"下载工具链"而不是给出清晰的版本错误。Gradle 9.4.1 上游 wrapper 原样沿用,2.4.20 线支持 Gradle 9.x。

决策 3:持久化用 kotlinx-io 自实现,而不是照搬 java.io.File

Kotlin/Native 没有 java.io.File,鸿蒙也没有 DataStore 等价物。选 kotlinx-io 的 SystemFileSystem 是因为它在 eazytec 仓库有 0.9.1-0.1.0-07 的 ohos 变体,且 Path / createDirectories / sink / source 的 API 在 ohos 上完整可用。落地格式选「一行一条 key|type|value + 整文件重写」:开关覆盖数量少、读写频率低,这种格式不需要引入 JSON 序列化运行时,.so 里少一个依赖就少一处风险。

决策 4:单入口 JSON,而不是多符号导出

FeaturedEval(req): String 一个符号覆盖 get/set/reset/list。多符号(FeaturedGet / FeaturedSet / …)会让 N-API 层重复四遍参数校验与字符串释放,且每加一个动作都要改三处(Kotlin、C++、index.d.ts)。单入口的代价是 ArkTS 侧要拼 JSON——JSON.stringify 一行就够。

决策 5:Compose 模块不参与鸿蒙构建

featured-compose 与 featured-debug-ui 依赖 Compose Multiplatform 1.10.3,而 ohos 构建用的是 eazytec Kotlin 2.4.20 线,两条 Compose 版本线不在同一代。与 cmp_qrose 把渲染层排除在鸿蒙构建之外的做法一致:鸿蒙端只跑逻辑,UI 由 ArkUI 承担。featured-testing 同理留盘——它依赖测试框架,鸿蒙上没有对应制品。

六、测试与验证

6.1 测试环境

本次验证使用 Windows 11(x64)、JDK 21(Adoptium Temurin 21.0.12)、DevEco Studio 5.1.0 Release(HarmonyOS SDK 5.1.0(18))、DevEco 自带 hvigor / node 与鸿蒙真机(arm64)。

6.2 编译与符号验证

./gradlew.bat -p example/nativeApp :linkDebugSharedOhosArm64 :linkDebugSharedOhosX64
:linkDebugSharedOhosArm64: BUILD SUCCESSFUL
:linkDebugSharedOhosX64:    BUILD SUCCESSFUL
:copyKnSoArm64 / :copyKnSoX64: 三件套已回填 entry/libs/<abi>

产物与符号:

libohosfeatured.so (ohosArm64, debugShared)   6.6 MB
libohosfeatured.so (ohosX64,   debugShared)   6.2 MB

llvm-nm -D entry/libs/arm64-v8a/libohosfeatured.so | grep FeaturedEval
  T FeaturedEval

libc++_shared.so 由 cxxShared() 从 ~/.konan/dependencies/<triple>/lib/ 直接定位并随拷贝任务下发,两个 ABI 各一份。

6.3 HAP 验证

BuildNativeWithCmake:  finished(双 ABI 编出 libfeatured_napi.so)
BuildNativeWithNinja:  finished
CompileArkTS:          finished
PackageHap:            finished
BUILD SUCCESSFUL in 17s

entry-default-unsigned.hap 约 9.7 MB,内含双 ABI 各三件套(libfeatured_napi.so / libohosfeatured.so / libc++_shared.so)。仓库内 signingConfigs 为空数组,SignHap 会跳过并提示由 IDE 配置签名——这是预期行为。

6.4 功能用例

#操作期望状态
1启动进入页面三个 chip 显示,默认选中 new_checkout,覆盖区显示 Toggle✅ 真机
2点「读取」value=true、source=DEFAULT、type=bool✅ 真机
3打开 Toggle 后点「覆盖」source 变为 LOCAL,值等于覆盖值✅ 真机
4点「列出全部」三行输出 key / type / default / value / source:new_checkout/true、max_retries/3、theme/system✅ 真机
5切到 max_retries覆盖区变成 Slider,初始值 3✅ 真机
6切到 theme覆盖区变成文本框,初始值 system✅ 真机
7点「重置」覆盖被清除,再读 source 回到 DEFAULT同一通道,建议复测

截图中的列表是"覆盖之前"那一次列举的结果(三行都是 DEFAULT),状态行则是"覆盖之后"再读的结果(LOCAL)——两者是不同时刻的快照,不是矛盾。

6.5 验证结论

解析引擎、Provider 抽象、kotlinx-io 文件层、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上安装并进入前台,页面展示了真实的开关会话:默认值解析、覆盖后 source 从 DEFAULT 变 LOCAL、三个开关共享同一 ConfigValues 实例(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/Featured.git
cd Featured
export JAVA_HOME="/path/to/jdk-21"
./gradlew.bat -p example/nativeApp :linkDebugSharedOhosArm64 :linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw --mode module -p product=default -p module=entry@default assembleHap --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.oh.tpc.cmp.featured -m entry
hdc shell hilog | grep Featured

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 core / featured-platform 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin 版本必须对着仓库实际发布核对:照抄别人的 2.3.21-1.0.0 会因解析不到对应编译器而卡在"下载工具链",错误信息不会指向版本本身;
  4. kotlinx-io-core 在 Maven Central 上的 0.9.1 没有 ohos 变体,必须钉 0.9.1-0.1.0-07——版本主体写对、后缀写错,解析照样失败;
  5. Kotlin/Native 没有 kotlin.io.File,Path / SystemFileSystem 才是鸿蒙上唯一可用的文件 API;
  6. @CName 的包名是 kotlin.native.CName,不是 kotlin.experimental.CName;
  7. ConfigValues.getValue / override / resetOverride 都是 suspend:@CName 入口用 runBlocking 包裹,辅助函数若调用它们也必须声明 suspend;joinToString 的 lambda 不是 suspend 上下文,不能在内部调 getValue;
  8. Kotlin/Native 上没有 putIfAbsent,注册表用 linkedMapOf(...) 预置 + registry[key] ?: ... 兜底;
  9. ArkTS 严格模式禁止无类型对象字面量(arkts-no-untyped-obj-literals):请求必须声明 interface 并标注类型;
  10. ArkUI 的 build() 条件判断不要用 struct getter,依赖追踪不可靠会导致分支不刷新——用 @State;
  11. 页面 FlagMeta.def 与桥注册表的默认值必须人工对齐,否则出现"界面写默认 true、实际读到 false"(本次首轮真机就撞上了,改完默认值必须重新链接 .so);
  12. DevEco 的 BuildNativeWithCmake 在配置阶段就会检查 entry/libs/<abi>/libohosfeatured.so,先跑 Gradle 再开 DevEco;失败过一次就删 entry/.cxx 重来;
  13. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝(00303038 must have required property 'material')——仓库内留空数组,勾选自动签名后由 IDE 回填;
  14. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  15. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  16. Gradle 9.4.1 的 ~/.gradle/caches/9.4.1/jvms 在多构建并发时会报 “Cannot take ownership / metadata.bin”——等其它构建(如本机另一个 KMP 工程)结束再跑即可,与工程配置无关。

8.2 已知问题

  • 演示页使用 InMemoryConfigValueProvider,覆盖只在进程内有效,重启后回到默认值;真实应用应改用 defaultLocalProvider(filesDir + "/featured_flags.prefs");
  • observe 返回的 Flow 不跨 C ABI:桥是同步一次一调用,订阅式推送过不了 C 签名,ArkTS 侧以"操作后读回"观察状态;
  • featured-compose / featured-debug-ui / sample / providers/* / featured-testing 均未参与鸿蒙构建,源文件留在树里;
  • 主构建(Android / iOS / JVM / Compose)与 ohos 构建使用两条 Kotlin 线(2.3.21 / 2.4.20-0.1.0-07),这是有意为之,不要为"统一"把主构建也切过去;
  • example/nativeApp/gradle.properties 里的 org.gradle.java.home 是机器相关路径,换机器需调整;
  • ohos 构建目前没有接入自动化测试:jvm() target 保留作交叉验证入口,但尚未把 core 的测试源接进来。

8.3 未来优化方向

  • 把 featured-platform 的文件型 Provider 接进演示页,做"杀进程重启后覆盖仍在"的落盘闭环;
  • 增加 observe 的状态推送(N-API 线程安全回表),替代操作后读回;
  • 给桥补 clear / 批量读写入口,覆盖 LocalConfigValueProvider.clear() 语义;
  • 把 core 的 commonTest 接到 ohos 构建的 jvm() target,形成"同一份源码、两条工具链"的交叉验证闸门;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建。

九、总结

9.1 核心难点回顾

上游 core + featured-platform(全部 commonMain)
    → 验证 ohos kotlinx 制品线(coroutines / kotlinx-io)
    → 独立 ohos 构建(主构建零改动的代价与收益)
    → ohosArm64 / ohosX64 KLIB + 2 个 ohos actual + 1 个文件型 Provider
    → C ABI(@CName FeaturedEval)+ 注册表 + MiniJson
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析(interface + @State)
    → ArkUI 真机单页验收(读取 / 覆盖 / 重置 / 列举)

9.2 封装层次

  • 引擎层:上游原版 ConfigValues、ConfigParam<T>、ConfigValue.Source 解析顺序与 Provider 抽象;
  • 平台层:ohos actual + kotlinx-io 文件型 Provider(key|type|value 行格式);
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、类型自适应编辑器、请求分发、状态行与原始 JSON;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 路线选择要看主构建背了什么:库越"纯逻辑",根构建替换越划算;一旦根构建绑了 AGP / Compose / 发布链,独立 ohos 构建才是低风险解。Featured 属于后者,代价只是一个 -p 参数;
  2. 版本永远跟着依赖走:Kotlin 钉版由 coroutines 1.11.0-0.1.0-07 的 klib 元数据反推,而不是照抄上一个库的钉法;kotlinx-io 的后缀错一位就解析失败;
  3. ArkTS 侧的两个"隐形陷阱"最耗时间:无类型对象字面量是编译期硬失败,build() 里用 getter 是运行期静默错——前者靠 interface,后者靠 @State,这两条写进规范能省掉一整轮真机复测。

9.4 适配成果

当前适配已完成:

  • Featured 1.2.0 的 core + featured-platform 双 ABI Kotlin/Native 编译(commonMain 零修改,新增 4 个 ohos 侧源文件);
  • HarmonyOS kotlinx 制品线落地(coroutines 1.11.0-0.1.0-07 + kotlinx-io-core 0.9.1-0.1.0-07);
  • 1 个 C ABI 符号(FeaturedEval,llvm-nm 验证 T 符号)与 C++ N-API 包装层;
  • 鸿蒙持久化 Provider(kotlinx-io + SystemFileSystem,key|type|value 行格式)与 defaultLocalProvider() 的 ohos actual;
  • DevEco Stage ArkUI 单页验收页面(读取 / 覆盖 / 重置 / 列举 / 状态行 / 原始 JSON);
  • HAP 构建(双 ABI 六件套 so,约 9.7 MB)、签名、设备安装与真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份)与本地效果图。

适配过程 FAQ

以下问题来自 ohosArm64 目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。

Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?

  • 确认 DevEco Studio 版本与工程 build-profile.json5 的 compatibleSdkVersion 一致;
  • 在 File > Settings > Build, Execution, Deployment > Ohpm 配置可用的 ohpm 仓库镜像或代理;
  • 删除 oh_modules、oh-package-lock.json5 与 .hvigor 缓存后重新 Sync;
  • 公司网络需要代理时,同时给 ohpm 与 hvigor 配置 http.proxy。

Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?

  • 先在 example 目录执行 ./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用 linkDebugSharedOhosX64);
  • 确认 libxxx.so 与 libc++_shared.so 已复制到 ohos/entry/libs/<abi>/;
  • 检查 entry/build-profile.json5 的 abiFilters 与设备 ABI 是否一致(真机 arm64-v8a,模拟器 x86_64);
  • 清理 entry/.cxx 与 entry/build 后重试。

Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?

  • 确认 CMake 的 add_library(<target> SHARED ...) 名称与 import ... from 'libxxx_napi.so' 完全一致;
  • 解包 HAP,确认 libs/<abi>/libxxx_napi.so 已打包;
  • 删除 entry/build、entry/.cxx 与 oh_modules 后重新构建;
  • 检查 module.json5 的 srcEntry 与页面路径是否正确。

Q4:真机安装报 signature verification failed 或 install failed?

  • 在 DevEco Studio 的 Signing Configs 配置证书、Profile 与密码;
  • bundleName 必须与签名 profile 中的 bundle name 一致;
  • 设备需加入 profile 的 deviceIds,调试证书有效期需覆盖当前时间;
  • 用 hdc list targets 确认设备在线,再执行 hdc install -r xxx-signed.hap。

Q5:应用启动白屏、闪退或 NAPI 调用崩溃?

  • 用 hdc hilog | grep -i "napi\|kotlin" 查看原生日志;
  • 确认 libc++_shared.so 与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致;
  • 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用 XxxFree 释放;
  • 在 DevEco 的 Native 调试配置中附加 lldb,定位空指针与越界访问。

Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?

  • 使用鸿蒙发行版 Kotlin(如 2.2.21-1.0.0),并在 settings.gradle.kts 配置 eazytec 仓库;
  • Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
  • 确认 kotlin { ohosArm64() } 已应用,且 local.properties 的 SDK 路径有效;
  • 清理 ~/.gradle/caches 中对应版本的 Kotlin/Native 缓存后重试。

Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?

  • 对齐 DevEco Studio、HarmonyOS SDK 与 build-profile.json5 中的 compatibleSdkVersion;
  • 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免 modelVersion 与 IDE 不匹配;
  • 命令行构建时设置 DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录。

Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?

  • 使用 DevEco 自带的 node 与 hvigor:
    "$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...;
  • 设置 JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME;
  • 加上 --no-daemon 排除守护进程缓存影响,并清理 entry/build 与 .hvigor。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐