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

pigment 是 serbelga 开发的 Compose Multiplatform 色板取色器库:ColorPicker.FlowRow(流式多行色板)与 ColorPicker.LazyRow(横向懒加载色板)两种布局,ColorPickerItem(color, enabled) 色块模型,Small / Medium / Large 三档尺寸,勾选对比色由 Color.luminance() 实时决定(暗底白勾、亮底黑勾),支持禁用项与 Color.Unspecified"非颜色"项。本次适配与社区早期路线(qrose 的"Compose 源码隔离到 composeMain 不编译")有本质区别:社区 Maven 上已经出现完整的 Compose 鸿蒙制品线(org.jetbrains.compose 的 runtime / ui / foundation / material3 / components-resources 1.12.0-0.1.0-07,全部带 ohosArm64 / ohosX64 klib),配合鸿蒙 Kotlin 2.4.20-0.1.0-07 自带的 compose 编译器插件,不需要隔离——上游 commonMain 的两个文件(690 行的 ColorPicker.kt 含全部 @Composable 组件,零改动)被原样编译成 OpenHarmony KLIB,一个带选中指示、禁用语义、未指定颜色项的完整取色器组件库编译进真机 .so,ArkUI 页面作为验收宿主,所有尺寸、亮度与对比色数值由真库实时计算。

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

上游仓库: serbelga/pigment

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

pigment 为 Compose Multiplatform 应用提供"从预设色板里挑一个颜色"这一件事的完整答案:ColorPicker.FlowRow 一个调用出流式色板(maxItemsInEachRow / maxLines 控制换行),ColorPicker.LazyRow 出横向滚动色板(带 LazyListState / flingBehavior / reverseLayout 全套懒加载参数),Map 泛型重载把任意业务键映射到色块;选中态由 ColorIndicatorColors.checkColor(color) 按 color.luminance() 决定勾选图标用白还是黑,边框宽度由 ColorIndicatorBorderWidth.borderWidth(color, selected) 按"选中 2dp / 极亮极暗 1dp / 其余无框"三段分支决定;ColorPickerItem.enabled = false 的色块不可点,Color.Unspecified 渲染为"非颜色"重置项。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套色板交互,但上游构建(Kotlin 2.4.10 / Gradle 9.8.0 / AGP 9.4.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示色块,却无法证明公共组件库在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证组件模型、Compose 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.4.10 / Gradle 9.8.0 / AGP 9.4.1(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建;旧钉法 Kotlin 2.2.21-1.0.0 读不了 2.4 元数据。
依赖矩阵组件依赖 compose.ui / compose.foundation / material3(ripple)/ components-resources(矢量图标与字符串),每一个都要有 ohos 变体才能解析。
代码生成缺失上游源码引用 compose 插件生成的 Res 资源访问器(Res.drawable.ic_check_24px 等),org.jetbrains.compose Gradle 插件不在鸿蒙线上,生成物不存在。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
内部 API 不跨模块尺寸映射(36/48/56dp)、checkColor / borderWidth 分支是上游 internal / private 成员,桥模块读不到。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:组件库整体的 ohos 编译、根构建替换(cmp_qrose 模式)、手写 Res 访问器补位和平面 JSON 桥接层。组件语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和重绘视觉。

与社区已入库的两个取色器并存不冲突:

compose-pipettekmp-kolor-pickerpigment
取色方式圆盘 / 方形 / 圆环连续 HSV 拖动HSV 光谱画布预设色板点选
输入手势坐标 → HSV手势坐标 → HSVList<ColorPickerItem> / Map<T, ColorPickerItem>
布局单组件单组件FlowRow 流式 + LazyRow 懒加载
特有语义thumb 对比色光谱渐变禁用项、非颜色项、三档尺寸、勾选对比色分支

1.2 库提供的能力

Demo 提供一个真实的色板选择会话,对应桥接的全部入口:

  • 默认值核验(defaults):ColorPickerDefaults.Shape === CircleShape 用对象同一性核验默认形状,colorIndicatorColors() 与 colorIndicatorColors(White, Black) 的数据类相等性核验默认对比色,真机页面渲染"真库校验通过";
  • 尺寸模型(sizes):Small / Medium / Large 三档真实枚举项,返回指示器 36 / 48 / 56dp、间距 4dp、内边距 2dp;
  • 单色模型(item):一个颜色过真库——构造真实 ColorPickerItem(color, enabled),Color.luminance() / toArgb() 实时计算,返回对比色、边框宽度与分支;
  • 整板模型(items):色板列表 + 选中项 + 禁用索引 + 尺寸一揽子下发,ArkTS 按 items[] 重绘整板,选中判定与组合件内 colorItem.color == selectedColor 同语义;
  • 版本握手(version):PigmentVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op回显本次操作。
luminance / isDark真库 Color.luminance() 结果与明暗判定。
checkColorHex / checkColorIsOnDark勾选图标颜色(< 0.5 取 onDark,否则 onLight)。
borderWidthDp / borderBranch边框宽度(未指定时为 null)与三段分支名。
indicatorSizeDp / itemSpacingDp / innerPaddingDp复刻自上游私有常量的布局度量。

组件侧完整能力(四个公共组合件、ColorPickerSize、ColorIndicatorColors / ColorIndicatorBorderWidth、ColorPickerDefaults、私有渲染内部件 ColorItem / ColorIndicator / ColorUnspecifiedIndicator)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留编译级交叉验证(上游本无 commonMain 测试,快照测试是 Android 专属 Paparazzi 模块,留盘不进构建),证明 ohos .so 里链接的组件代码与上游零漂移。

1.3 实现适配

维度要求
代码复用pigment 的全部 commonMain 文件(ColorPicker.kt 690 行 + ColorPickerItem.kt)原样编译,不隔离、不改写;仅新增 1 个 Res 访问器 shim 源文件(全仓唯一新增 Kotlin 源文件)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);build-logic(Spotless 约定插件)、:pigment:samples、catalog 四端 Demo、Paparazzi 快照测试留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律版本主体与上游完全一致,只把仓库换到 ohos 制品线(换线不换版);jvm 交叉验证走上游同款 1.11.1 线。
UI 完整三页签覆盖全部桥接入口:FlowRow 色板、LazyRow 色板、默认值 / 尺寸 / 亮度探针验收页。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。OHOS 侧暂无社区 Compose 线的组合宿主,组合件编译并链接进 .so 但不在真机组合;ArkTS Demo 用真库返回的模型数值重绘视觉——与 qrose / compose-pipette 审核通过的"真库模型 + ArkTS 重绘"同一条路。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 Compose ohos 制品线(material3 / components-resources 可达性)、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、双线依赖、skiko 排除、Res shim 补位
第 3 阶段:桥协议建模     ── 色板载荷、item/items JSON 契约、真库直调与复刻公式的边界、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── FlowRow / LazyRow 色板重绘、尺寸切换、模型验收页、dispatch 日志
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
pigment/                              上游组件库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
pigment/src/commonMain/.../resources/ 新增:Res 访问器 shim(全仓唯一新增源文件)
pigment/samples/                      上游 @sample 文档示例,留盘不进 settings
pigment/screenshot-tests/             上游 Paparazzi 快照测试(本就未进 settings),留盘
build-logic/                          上游 Spotless 约定插件 composite build,留盘不进构建
catalog/                              上游四端 Demo(android/desktop/shared/web),留盘不进构建
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :pigment 与 :example:nativeApp,rootProject.name = "ohos_pigment"。上游的 AGP / Dokka / Compose 插件别名、vanniktech 发布、Spotless 约定全部随根构建替换剥离——本 fork 不发 Maven Central。被替换的上游文件逐一保全为 *.upstream(settings.gradle.kts.upstream、build.gradle.kts.upstream、gradle.properties.upstream、gradle/libs.versions.toml.upstream、pigment/build.gradle.kts.upstream),上游 README 存为 README.upstream.md。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle9.6.1(自上游 9.8.0 对齐到社区同款 wrapper;2.4.20 线支持 Gradle 9.x)
Composeohos 线 1.12.0-0.1.0-07(runtime / ui / foundation / material3 / components-resources);jvm 交叉验证线上游同款 1.11.1 + material3 1.9.0
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.pigment
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
pigment2(ColorPicker.kt 690 行 / ColorPickerItem.kt)compose.ui + foundation(FlowRow/LazyRow)+ material3(ripple)+ components-resources(Res 访问器)整体编译(jvm + 双 ohos,源码零改动)
pigment:samples7compose foundation/ui + :pigment,文档 @sample 专用留盘不进构建
pigment:screenshot-tests3Paparazzi + TestParameterInjector(Android 专属)留盘不进构建(上游本就未 include)

expect/actual 声明全库为 零—— pigment 没有任何平台源码集,commonMain 就是全部实现。挡在面前的是两件小事:其一,源码引用 compose 插件生成的 Res 资源访问器(Res.drawable.ic_check_24px、Res.string.color_off 等),代码生成不在鸿蒙线上;其二,桥需要的尺寸/对比色/边框分支是上游 internal / private 成员,跨模块不可见。前者用 shim 补位(第 2.4 节),后者按 compose-pipette 审核先例逐字复刻(第 3.2 节)。

1.4 页面边界
标题区          Pigment · OpenHarmony / 版本握手行(0.7.0-ohos.1)
Tab 切换        取色器 / 横向 / 模型
取色器页签      18 色 Material 调色板 + 非颜色项、禁用项淡化、小/中/大尺寸切换、选中信息行
横向页签        31 色 HSL 光谱 List 横滑、首个可见索引读数、非颜色项置首
模型页签        版本握手、默认值核验表(真库校验通过)、三档尺寸表、灰阶亮度探针、dispatch 日志
日志区          最近 Native 调用的原始 JSON 响应留痕

页面启动时先调 pigmentVersion() 做握手,再按当前尺寸 pigmentDispatch({op:'items',...}) 拉整板模型。

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

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

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 Compose 制品。用 .module 元数据逐个验证:

org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07          ohosArm64 / ohosX64 klib ✓
org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07                    ohosArm64 / ohosX64 klib ✓
org.jetbrains.compose.foundation:foundation:1.12.0-0.1.0-07    ohosArm64 / ohosX64 klib ✓
org.jetbrains.compose.material3:material3:1.12.0-0.1.0-07      ohosArm64 / ohosX64 klib ✓
org.jetbrains.compose.components:components-resources:1.12.0-0.1.0-07  ohosArm64 / ohosX64 klib ✓

三个关键结论:其一,material3 与 components-resources 都在 ohos 线上(inklet / compose-font-icons / SpriteView-KMP 先例),pigment 的三件依赖全部可达,不需要 qrose 式的 composeMain 隔离;其二,ohos 线不发布 *-desktop 子制品,jvm() 交叉验证必须从 Maven Central 解析上游同款 1.11.1 线——依赖按目标线声明(“line switch, not a version switch”);其三,鸿蒙 Kotlin 2.4.20-0.1.0-07 自带 org.jetbrains.kotlin.plugin.compose 编译器插件同版本制品,@Composable 变换在 ohos 目标上直接可用。

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

社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / kmp-kolor-picker 用的)。但上游 pigment 是 Kotlin 2.4.10 构建的 2.4 代元数据,Kotlin 2.2.21 只能读到 2.3.0,编译期直接报:

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.

工具链版本跟着上游走:Kotlin 钉 2.4.20-0.1.0-07(KStore / compose-pipette / channel-event-bus 先例)。源码级兼容成本为零;Gradle 对齐到社区同款 9.6.1(2.4.20 线支持 Gradle 9.x),不像 2.2.21 旧线必须降 8.14.1。

settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_pigment"

include(":pigment")
include(":example:nativeApp")
2.3 编译真实组件库

pigment/build.gradle.kts 收敛为纯 KMP 模块,Compose 依赖按目标线声明:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.kotlin.compose.compiler)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvm()                  // 交叉验证用
    ohosArm64()            // the adaptation
    ohosX64()

    sourceSets {
        ohosMain.dependencies {
            api(libs.compose.runtime)      // 1.12.0-0.1.0-07
            api(libs.compose.ui)
            api(libs.compose.foundation)
            api(libs.compose.material3)
            api("org.jetbrains.compose.components:components-resources:${libs.versions.compose.get()}") {
                exclude(group = "org.jetbrains.skiko", module = "skiko-ohosarm64-api")
                exclude(group = "org.jetbrains.skiko", module = "skiko-ohosx64-api")
                exclude(group = "org.jetbrains.skiko", module = "skiko-ohosarm64-fusionrenderer")
                exclude(group = "org.jetbrains.skiko", module = "skiko-ohosx64-fusionrenderer")
            }
        }
        jvmMain.dependencies {
            api(libs.compose.runtime.jvm)  // 上游同款 1.11.1 线
            api(libs.compose.ui.jvm)
            api(libs.compose.foundation.jvm)
            api(libs.compose.material3.jvm) // material3 独立版本线 1.9.0
            api(libs.compose.resources.jvm)
        }
    }
}

skiko 排除是硬要求:components-resources 会传递 skiko-ohos{arm64,x64}-api 与 -fusionrenderer,而 compose.ui 带完整的 org.jetbrains.skiko:skiko klib——两者都携带 Skia 实现,KLIB 加载器(unique_name 相同)和链接器(Skia 符号重复)先后拒绝。完整 klib 覆盖前者,排除之(SpriteView-KMP / compose-font-icons 同款处置)。

实测:

./gradlew :pigment:compileKotlinJvm                    # BUILD SUCCESSFUL(上游源码 + 上游 1.11.1 线)
./gradlew :pigment:compileKotlinOhosArm64              # BUILD SUCCESSFUL(上游源码 + ohos 1.12.0-0.1.0-07 线)
./gradlew :pigment:compileKotlinOhosX64                # BUILD SUCCESSFUL

690 行 ColorPicker.kt 一个字符没动,含全部 @Composable、FlowRow / LazyRow、material3 ripple()、vectorResource 调用。

2.4 Res shim 补位(pigment 特有的一步)

上游 ColorPicker.kt 引用代码生成的资源访问器:

import dev.sergiobelda.pigment.resources.Res
import dev.sergiobelda.pigment.resources.color_off
import dev.sergiobelda.pigment.resources.ic_check_24px
import dev.sergiobelda.pigment.resources.ic_format_color_reset_24px
import dev.sergiobelda.pigment.resources.selected

这些访问器由 org.jetbrains.compose Gradle 插件按 compose.resources { packageOfResClass = "dev.sergiobelda.pigment.resources" } 生成——插件不在鸿蒙线上。处置是手写一个与生成物同包名、同名字、同接收者形状的 shim(全仓唯一新增源文件 pigment/src/commonMain/kotlin/dev/sergiobelda/pigment/resources/Res.kt):

@file:OptIn(InternalResourceApi::class)

package dev.sergiobelda.pigment.resources

import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.InternalResourceApi
import org.jetbrains.compose.resources.ResourceItem
import org.jetbrains.compose.resources.StringResource

public object Res {
    public val drawable: DrawableRes = DrawableRes
    public val string: StringRes = StringRes
}

public object DrawableRes
public object StringRes

public val DrawableRes.ic_check_24px: DrawableResource
    get() = DrawableResource(
        "drawable/ic_check_24px",
        setOf(ResourceItem(setOf(), "drawable/ic_check_24px.xml", -1L, -1L)),
    )

public val StringRes.color_off: StringResource
    get() = StringResource(
        "string/color_off",
        "color_off",
        setOf(ResourceItem(setOf(), "values/strings.xml", -1L, -1L)),
    )
// ic_format_color_reset_24px / selected 同构

三个实现细节:其一,构造器全部显式传参(DrawableResource(id, items) 与 StringResource(id, key, items) 的 items 无默认值,ResourceItem 四参 (qualifiers, path, offset, size) 逐个写全);其二,构造器挂在 InternalResourceApi opt-in 后面——插件自己的生成物走的就是这道门,shim 显式 opt-in 即可;其三,物理资产(composeResources/drawable/ 下的矢量 XML 与 values/strings.xml)原样留在树里供上游构建使用,ohos 侧组合件只编译不组合,访问器仅需要编译期可解析。上游源码因此零改动——这与 compose-font-icons 的 headless facade 是同一个先例:适配面落在新增文件,而不是修改上游。

第 3 阶段:桥协议建模

3.1 JSON 契约
dev.sergiobelda.pigment.colorpicker/**(上游组件库,原样编译)
        ↓ ColorPickerItem / ColorPickerSize / ColorPickerDefaults / Color 数学
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API pigmentVersion() / pigmentDispatch()
        ↓
ArkUI Index.ets(三页签)

请求与响应:

请求 : {"op":"version"}
       {"op":"defaults"}
       {"op":"sizes"}
       {"op":"item","hex":"#607D8B","selected":"true","enabled":"true"}
       {"op":"items","colors":"#F44336,#E91E63,...,unspecified","selected":"E91E63",
        "size":"Medium","disabled":"4,9,17"}
响应 : {"ok":true,"op":"item","item":{...单色模型...}}
       {"ok":true,"op":"items","size":"Medium","indicatorSizeDp":48.0,"items":[...]}
       {"ok":false,"op":"...","error":"..."}

两个设计:colors 用逗号分隔的 hex 列表(扁平 JSON 解析器不递归数组),字面量 unspecified 或空项映射到 Color.Unspecified 非颜色项——与上游 ColorPickerItem(Color.Unspecified) 同语义;selected 归一化为 6 位 RGB 比对(UNSPECIFIED 字面量选中非颜色项),与组合件内 colorItem.color == selectedColor 的选中判定对齐。布尔值统一走 "true" / "false" 字符串。

3.2 真库直调与复刻公式的边界

桥内每一个色值都过真库类型:

val colorItem = ColorPickerItem(color = color, enabled = enabled)  // 真实数据类实例
val luminance = colorItem.color.luminance()                        // 真实 compose ui 数学
val argb = colorItem.color.toArgb()
val shapeIsCircle = ColorPickerDefaults.Shape === CircleShape       // 对象同一性
val defaultsVerified = ColorPickerDefaults.colorIndicatorColors() ==
    ColorPickerDefaults.colorIndicatorColors(
        onDarkColor = Color.White, onLightColor = Color.Black)     // 数据类相等性
val size = ColorPickerSize.valueOf(sizeName)                        // 真实枚举项

上游 internal / private 的四组公式在桥内逐字复刻(compose-pipette 审核先例,复刻处逐行注明出处):

复刻公式上游出处内容
indicatorSizeDp / itemSpacingDp / innerPaddingDpColorPickerSize.indicatorSize()/itemSpacing() + 私有常量Small 36 / Medium 48 / Large 56;间距 4;内边距 2
checkColorColorIndicatorColors.checkColorluminance() < 0.5 → onDark(#FFFFFF)否则 onLight(#000000)
borderWidthColorIndicatorBorderWidth.borderWidth选中 → 2dp;luminance() !in 0.1..0.9 → 1dp(neutral);其余 Dp.Unspecified(响应里 null)
非颜色项常量ColorUnspecifiedIndicator 私有色值白底、黑图标(选中时 0.2 alpha)、黑勾

亮度数值本身永远来自真库 Color.luminance()——复刻的只是分支形状,不是数学。

3.3 错误通道

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

@CName("PigmentDispatch")
fun pigmentDispatch(request: String): String = try {
    val json = MiniJson.parse(request)
    when (json.str("op")) {
        "version" -> versionResponse()
        "defaults" -> defaultsResponse()
        "sizes" -> sizesResponse()
        "item" -> itemResponse(json)
        "items" -> itemsResponse(json)
        else -> err(json.str("op").ifBlank { "(missing)" }, "unsupported op")
    }
} catch (t: Throwable) {
    err("dispatch", t.message ?: t.toString())
}

非法 hex、缺 op、不支持的 op、坏尺寸名,全部走同一个 ok:false 通道,ArkTS 侧统一渲染错误横幅。

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

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

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

4.2 Kotlin/Native 导出函数
@CName("PigmentVersion")
fun pigmentVersion(): String = "0.7.0-ohos.1"   // 版本握手

@CName("PigmentDispatch")
fun pigmentDispatch(request: String): String    // 五个 op 一口进
C ABI 符号ArkTS 方法覆盖内核
PigmentDispatchpigmentDispatch(req)ColorPickerItem 构造 + luminance + Defaults 核验 + 尺寸/分支复刻
PigmentVersionpigmentVersion()fork 版本握手(真机版本行的数据来源)

桥模块不 apply compose 编译器插件——它只触碰库的非组合面(数据类、枚举、Color 数学),没有插件变换的 lambda 需要链接(compose-pipette 同款纪律)。

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

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohospigment_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohospigment_api.h 直接取 Kotlin/Native 生成的 C Adapter 头(含 ExportedSymbols 结构与两个业务符号),不手写缩减版。

4.4 CMake 和类型声明
add_library(ohospigment SHARED IMPORTED)
set_target_properties(ohospigment PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(pigment_napi SHARED napi_init.cpp)
target_link_libraries(pigment_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohospigment)

OHOS_ARCH 为空或 libohospigment.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const pigmentDispatch: (request: string) => string;
export const pigmentVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 dispatch 模式
import { pigmentDispatch, pigmentVersion } from 'libpigment_napi.so';

private dispatch(req: DispatchRequest): PigmentResponse {
  const raw: string = pigmentDispatch(JSON.stringify(req));
  return JSON.parse(raw) as PigmentResponse;   // typed interface,禁 any
}

取色器页签在尺寸切换、选中项变化时重新 {"op":"items",...} 拉整板;每格的勾选颜色、边框宽度、分支名直接取 items[i].checkColorHex / borderWidthDp / borderBranch,ArkTS 不自行计算任何颜色数学。

5.2 页面交互

取色器页签:18 色 Material 500 调色板加末位非颜色项(斜杠圆圈图标),第 4 / 9 / 17 项禁用(淡化且不可点),小 / 中 / 大三枚尺寸片(36 / 48 / 56dp,点击重新 dispatch),选中项按 checkColorHex 画白勾或黑勾、按 borderWidthDp 画边框,borderBranch = neutral 的极亮极暗项常显 1dp 中性边框,并复刻上游"左半透明灰 #BDBDBD"细节;底部信息行显示"已选 hex · 亮度 · 对比色 · 边框分支"。横向页签:31 色 HSL 光谱(ArkTS 侧 hslToHex 生成色号,颜色数学仍在真库)横向 List 滚动,非颜色项置首,onScrollIndex 回读首个可见索引。模型页签:版本握手行、默认值核验表(shapeVerified / defaultsVerified 渲染为"真库校验通过 ✓")、三档尺寸表、12 级灰阶亮度探针——亮度跨 0.5 时对比色白黑翻转、跨出 0.1–0.9 时边框分支在 neutral / unselected 间切换,肉眼可见;底部 dispatch 日志留痕最近 Native 调用的原始 JSON。

5.3 对比度和错误边界

色块本身即色彩主体,文字一律深色底/浅色底双套;ok:false 时错误原文进横幅与日志,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohospigment)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.pigment、versionName 0.7.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/libohospigment_api.h
        ├── src/main/cpp/types/libpigment_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 三页签验收页面
6.2 构建、签名和安装
./gradlew :pigment:compileKotlinJvm                                 # 交叉验证(上游 1.11.1 线)
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

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

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.terminator.ohos.pigment -m entry

四、完整代码对照

4.1 整体架构

dev.sergiobelda.pigment.colorpicker/**(上游组件库原样编译)
    │ ColorPicker × 4 / ColorPickerItem / ColorPickerSize
    │ ColorIndicatorColors / ColorIndicatorBorderWidth / ColorPickerDefaults
dev.sergiobelda.pigment.resources.Res(手写 shim,唯一新增源文件)
    │ Res.drawable.ic_check_24px / Res.string.color_off …
Bridge.kt + MiniJson
    │ PigmentDispatch / PigmentVersion
libpigment_napi.so
    │ pigmentDispatch / pigmentVersion
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(三页签)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_pigment、上游模块移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + ohos Compose 线 1.12.0-0.1.0-07 + jvm 线 1.11.1/material3 1.9.0 双线钉版
build.gradle.kts(根)版本 0.7.0-ohos.1、全局禁用 Ohos*Test* 任务
pigment/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,依赖按目标线声明,skiko 排除
pigment/src/commonMain/.../resources/Res.kt唯一新增源文件(Res 访问器 shim,InternalResourceApi opt-in)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、真库直调 + 四组复刻公式、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鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次API作用
组件ColorPicker.FlowRow(colors, selectedColor, …)流式色板组合件(maxItemsInEachRow / maxLines)
组件ColorPicker.LazyRow(colors, selectedColor, …)横向懒加载色板(LazyListState / flingBehavior)
模型ColorPickerItem(color, enabled)色块模型(桥内真实构造)
模型ColorPickerSize.Small/Medium/Large三档尺寸(36/48/56dp,桥复刻映射)
模型ColorPickerDefaults默认形状 / 尺寸 / 对比色 / 边框宽度(桥内同一性与相等性核验)
数学Color.luminance() / Color.toArgb()compose ui 实时计算(桥内真实调用)
NativePigmentDispatch / PigmentVersionC ABI JSON 导出
N-APIpigmentDispatch(req) / pigmentVersion()向 ArkTS 暴露能力
ArkTSdispatch(op)触发操作并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(pigmentDispatch(JSON.stringify({
  op: 'items',
  colors: ['#F44336', '#E91E63', '#607D8B', 'unspecified'].join(','),
  selected: '607D8B',
  size: 'Medium',
  disabled: '2'
}))) as ItemsResponse;
// res.items[2].checkColorHex === '#FFFFFF'(亮度 0.19 < 0.5,onDark)
// res.items[2].borderBranch === 'selected'(2dp 边框)
val colorItem = ColorPickerItem(Color(0xFF607D8B), enabled = true)
val luminance = colorItem.color.luminance()          // 0.190…
val checkColor = if (luminance < 0.5f) Color.White else Color.Black

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

五、关键决策说明

决策 1:整库编译而不是 composeMain 隔离

qrose 适配时社区 Compose 线尚不存在,Painter 只能隔离到 src/composeMain 不编译、真机走 KN 光栅化。本次先验证制品线(runtime / ui / foundation / material3 / components-resources 的 ohos klib 全部存在),障碍已消失——再隔离等于把组件库雪藏,反而丢掉"上游原件进真机 .so"的证明力。真编译让四个公共组合件、选中分支、禁用语义、非颜色项全部是上游原件。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 对齐 9.6.1

上游本来就是 Kotlin 2.4.10,旧钉法 2.2.21 读不了 2.4 元数据;2.4.20 与之同代零降级。Gradle 方面 2.4.20 线支持 9.x,wrapper 对齐到社区同款 9.6.1(compose-pipette / inklet 同款),只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。

决策 3:Res shim 而不是改上游源码

代码生成不可用时有三条路:改上游源码删掉 Res 引用(引入漂移,违背零改动原则)、把资源引用挪进 expect/actual(改动面更大)、手写与生成物同形状的访问器 shim(只增不改)。选第三条——全仓唯一新增源文件落在 resources/ 包,上游 colorpicker/ 目录一字不动,上游构建(插件在场时生成物与 shim 同包同名会冲突的场景)因 build-logic 不进构建而不受影响。

决策 4:双线依赖(ohos 线 / jvm 交叉验证线)

ohos 线不发布 *-desktop 子制品,jvm 目标从同一条线解析必然失败;jvm 线(Maven Central 1.11.1 + material3 1.9.0)没有 ohos klib。处置是依赖声明在 ohosMain / jvmMain 各自的 source set 里(commonMain 保持线无关)——“line switch, not a version switch”,交叉验证编译的正是上游当年构建用的同款制品体。

决策 5:真库模型 + 复刻公式的边界

桥能拿到的是公共面:ColorPickerItem 构造、Color.luminance()、ColorPickerDefaults 的对象同一性与数据类相等性、ColorPickerSize 枚举。尺寸映射与三段分支是 internal / private,跨模块不可见——按 compose-pipette 审核先例逐字复刻并逐行注明出处,亮度数值本身永远来自真库。复刻的是分支形状,不是数学。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21、Gradle 9.6.1、DevEco Studio 5.1.0 Release(product 5.1.0(18))、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :pigment:compileKotlinJvm

上游无 commonMain 测试(快照测试是 Paparazzi / Android 专属模块,本就未进上游 settings),交叉验证为编译级:上游两个源文件在 Maven Central 的上游同款线(compose 1.11.1 + material3 1.9.0)上原样编译通过——同一份源码,两条制品线(1.11.1 与 1.12.0-0.1.0-07)都能吃下,是"源码零漂移"的直接证据。ohos 测试任务由根构建全局禁用(Ohos*Test* → enabled = false),ohos 侧只承担编译与链接。

6.3 HAP 验证

:pigment:compileKotlinJvm: BUILD SUCCESSFUL
:pigment:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(3m36s)
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/{arm64-v8a,x86_64}
llvm-nm -D libohospigment.so: PigmentDispatch / PigmentVersion 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libpigment_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully

KN so arm64 约 27.4 MB(28,728,984 字节,debugShared 未 strip,含 Kotlin/Native 运行时与 Compose klib)、x86_64 约 26.2 MB;libc++_shared.so arm64 / x86_64 分别约 1.8 MB / 1.7 MB,与 NAPI so 一起构成每个 ABI 的三件套。

6.4 功能用例

  1. 启动后版本握手行显示 0.7.0-ohos.1(来自 pigmentVersion() 真实 Native 调用,非静态文字);
  2. 取色器页签渲染 18 色 Material 调色板加非颜色项,第 4 / 9 / 17 项淡化禁用不可点(真机截图见 7.1);
  3. 点选深色蓝灰 #607D8B(亮度约 0.19 < 0.5):勾选图标取 onDark 白色、边框分支 selected(2dp)——对比色分支随亮度实时翻转;
  4. 点选极亮 / 极暗色块:未选中态显示 1dp 中性边框(neutral 分支);中间亮度色块无边框(unselected,Dp.Unspecified);
  5. 小 / 中 / 大尺寸片切换:色块直径随 indicatorSizeDp 36 / 48 / 56 变化,间距与内边距同步按桥返回值调整;
  6. 非颜色项选中:白底 + 斜杠圆圈图标 + 黑勾(iconAlpha 0.2);
  7. 横向页签 31 色光谱滚动选择,首个可见索引随滚动回读;
  8. 模型页签默认值核验表显示"真库校验通过 ✓"(shapeVerified / defaultsVerified 均为真);
  9. 灰阶探针拖到纯黑 / 纯白端:边框分支跳回 neutral;跨 0.5 亮度:对比色白黑互换;
  10. 每步操作在 dispatch 日志留痕原始 JSON 响应。

6.5 验证结论

组件库、Compose 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,取色器页签展示了真实的色板选择会话:版本握手、选中对比色分支、三段边框分支、尺寸模型与禁用 / 非颜色语义(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/pigment.git
cd pigment
export JAVA_HOME="/path/to/jdk-21"
./gradlew :pigment:compileKotlinJvm                                 # 交叉验证(上游 1.11.1 线)
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.pigment -m entry
hdc shell hilog | grep Pigment

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :pigment 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin KDoc 注释里写 composeResources/drawable/*.xml 这种通配路径会踩 Kotlin 块注释嵌套规则——*.xml 里的 /* 开启了一层未闭合的嵌套注释,报"Unclosed comment"且位置指向文件尾,写文档注释时通配符要拆开表述;
  4. compose-resources 的 DrawableResource(id, items) / StringResource(id, key, items) 构造器 items 无默认值,ResourceItem(qualifiers, path, offset, size) 四参——手写 shim 时先用 javap 对着 jar 确认签名,不要凭记忆省参数;
  5. ResourceItem 构造器挂在 InternalResourceApi opt-in 后面(报"This is internal API of the Compose gradle plugin"),shim 加 @file:OptIn(InternalResourceApi::class) 即可,这与插件生成物走的是同一道门;
  6. ohos 线不发布 *-desktop 子制品:jvm 交叉验证目标必须单独走 Maven Central 上游线(1.11.1 + material3 1.9.0),依赖声明在各自 source set,commonMain 保持线无关;
  7. components-resources 传递的 skiko-ohos{arm64,x64}-api / -fusionrenderer 与 compose.ui 带的完整 skiko klib 重复(KLIB unique_name 冲突 + 链接期 Skia 符号重复),exclude 前者;
  8. Gradle wrapper 从上游 9.8.0 对齐到 9.6.1:鸿蒙 Kotlin 2.4.20 线的社区验证矩阵在 9.6.x,不赌 9.8;
  9. 根构建 .gitignore 的 /build 是根锚定模式,盖不住 example/nativeApp/build/ 与 example/harmonyApp/entry/libs/,要单独追加,否则 so 与中间产物混进提交;
  10. ArkTS 只能 import 'libpigment_napi.so',绝不能 import libohospigment.so——KN CAdapter 不把 @CName 业务符号导出给 ArkTS;
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  12. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();签名材料由 DevEco 自动写入 build-profile.json5,提交前保护,证书绝不进入 AtomGit。

8.2 已知问题

  • OHOS 侧暂无社区 Compose 线的组合宿主:组合件编译并链接进 .so 但不在真机组合,视觉由 ArkUI 按真库返回值重绘(qrose / compose-pipette 审核路线的既定边界);
  • 桥内复刻的四组公式(尺寸映射、checkColor / borderWidth 分支、非颜色项常量)必须与上游同步——上游改这些 internal / private 成员时桥要跟着改(README.OpenHarmony_CN.md 已列入维护清单);
  • compose-resources 物理资产(矢量 XML / strings)不进 ohos 包,Res 访问器仅编译期可解析,vectorResource / stringResource 不在真机执行路径上;
  • 上游无 commonMain 测试,JVM 交叉验证是编译级而非用例级(Paparazzi 快照测试是 Android 专属,留盘不进构建);
  • android / ios / js 目标与 build-logic / samples / catalog 留盘不进构建,发布、dokka、Spotless 一并剥离,本 fork 不发 Maven Central;
  • 非颜色项响应里 iconColorHex 为 RGB(alpha 由 iconAlpha 字段单独承载),ArkTS 侧要组合使用。

8.3 未来优化方向

  • 等社区 compose.ui ohos 线出现组合宿主(ComposeArkUIViewController)后,把 ArkUI 重绘换回真组合件直接渲染 ColorPicker.FlowRow / LazyRow——终态是"整库编译"升级为"整库运行",届时桥退化为模型导出;
  • 社区 compose Gradle 插件上线 ohos 线后,用真代码生成替换手写 Res shim;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 pigment commonMain(2 个文件,690 行 ColorPicker.kt 含全部组合件)
    → 验证 ohos Compose 制品线(runtime / ui / foundation / material3 / components-resources)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1)
    → 根构建替换(cmp_qrose 模式,*.upstream 保全)
    → 双线依赖 + skiko 排除 + Res shim(唯一新增源文件)
    → ohosArm64 / ohosX64 KLIB,源码零改动
    → C ABI(@CName × 2)+ 真库模型直调 + 四组复刻公式
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析
    → ArkUI 真机三页签验收(色板选择 / 光谱横滑 / 模型核验)

9.2 封装层次

  • 组件层:上游原版四个 ColorPicker 组合件、ColorPickerItem / ColorPickerSize 模型、ColorPickerDefaults 默认值;
  • 模型层:真库 Color.luminance() / toArgb() 实时计算 + 复刻的尺寸与分支公式;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、JSON 解析、色板重绘、日志与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线:Compose ohos 线补齐 material3 / components-resources 后,UI 组件库的正确答案就是"整库真编译 + 一个资源 shim"——pigment 全库零 expect/actual、零平台源码集,适配面只剩代码生成缺口一处;路线选择必须基于当天的制品可达性,而不是 qrose 时代"Compose 上不了鸿蒙"的历史结论;
  2. 工具链版本跟着元数据走:上游 Kotlin 2.4.10 决定了必须用 2.4.20-0.1.0-07 新线,Gradle 矩阵按线区分(2.4.20 线 9.6.1,2.2.21 线 8.14.1),不能混用两条线的经验;
  3. "真库 / 复刻"的边界要写进文档:桥内每一个数值要么来自真库调用(亮度、默认值核验、数据类构造),要么逐行注明复刻出处(尺寸、分支)——审核时这张对照表就是漂移风险的验收面;UI 组件库的 Demo 验收重点落在"视觉随真库数值变化"而不是像素级复刻。

9.4 适配成果

当前适配已完成:

  • pigment 0.7.0(Apache-2.0)真实组件库的双 ABI Kotlin/Native 编译(上游 commonMain 文件零改动,仅 1 个 Res shim 新增源文件);
  • HarmonyOS Compose 制品线在取色器库上的完整落地(runtime / ui / foundation / material3 1.12.0-0.1.0-07 + components-resources 带 skiko 排除);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 三页签验收页面(色板选择、光谱横滑、默认值核验 / 亮度探针、dispatch 日志);
  • JVM 编译级交叉验证(上游源码 + 上游同款制品线)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐