开源鸿蒙平台 CMP 三方库「compose-shadow」适配全流程
本文记录
compose-shadow接入 OpenHarmony 的完整过程,覆盖绘制类 Compose 库的适配路线盘点、阴影合成语义的纯 Kotlin 内核提取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
compose-shadow是一个"单 Modifier"的 Kotlin Multiplatform 库:Modifier.boxShadow(blurRadius, color, spreadRadius, offset, shape, clip, inset)一个调用复刻 Figma 的 drop shadow 与 inner (inset) shadow,CSS box-shadow 风格,上游支持 android / jvm / wasmJs / ios。本次适配与 KStore(依赖 ohos kotlinx 制品线、整库真编译)是两类不同的题面:compose-shadow的全部实现都站在 Compose 的DrawScope/Paint/saveLayer/ColorMatrix之上,而鸿蒙侧已审核的工具链不在库内编译 Compose——因此适配的核心动作是把上游的阴影合成语义逐条提取为纯 Kotlin 软件光栅化内核,编译成 OpenHarmony 双 ABI 动态库,经@CNameC ABI + CMake NAPI 暴露给 ArkTS,与 qrose / compose-continuous-rounded-corner-shape / segmented-control 走的是同一条社区已审路线。
项目地址: AtomGit/oh-tpc/compose-shadow.git
上游仓库: LennartEgb/compose-shadow
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-shadow 为 Compose Multiplatform 应用回答"元素阴影怎么画"这一件事:Figma 设计稿里的 drop shadow 与 inner shadow,导出时是一组 (y, blur, spread, color, inset) 参数,boxShadow 把这组参数变成一个修饰符——blur 高斯模糊、spread 四向收缩扩张、offset 平移、inset 反相成内阴影、clip 只裁内容。阴影是 UI 还原度里最显眼的细节之一,KMP 项目把它放在 commonMain,由 Android、桌面、Web、iOS 共享。OpenHarmony 应用同样需要这套绘制能力,但上游构建(Kotlin 2.2.20 / Gradle 8.11.1 / AGP 8.10.1 / Compose 1.9.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示滑杆,却无法证明阴影合成语义在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证语义内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.2.20 / Gradle 8.11.1 / AGP 8.10.1 与鸿蒙工具链无法共存于一个 Gradle 构建;ohos 目标只存在于 HarmonyOS Kotlin 定制发行版。 |
| Compose 依赖 | 上游全部 6 个源文件都 import androidx.compose.ui.*;社区已审核的约 60 个库适配一致不在库内编译 Compose。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 绘制语义跨不了桥 | DrawScope / Paint / BlurMaskFilter / saveLayer 是 Compose/Skiko 的运行时概念,Modifier 没有任何 @CName 面。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:把上游 Shadows.kt 的合成语义(spread 缩放、blur 的 sigma 约定、inset 的 ColorMatrix 反相、clip 只裁内容、层叠顺序)提取为纯 Kotlin 内核并编译成双 ABI 动态库,ArkTS 通过 NAPI 调用、按参数实时重渲染 PNG 验收。渲染数学完全由内核负责,ArkTS 只负责组请求、解码位图和展示。
1.2 库提供的能力
Demo(阴影画板)提供一个真实的阴影渲染会话,对应桥接的全部入口:
- 版本握手:
shadowVersion()返回{"ok":true,"version":"0.1.3-ohos.1"},来自真实 Native 调用而非静态文字; - drop shadow:blur(高斯模糊)、spread(含负值收缩)、offset(双向)、形状(方形 / 圆角 / 圆形,桥还支持每角独立圆角)、颜色与透明度;
- inset 内阴影:反相合成语义,
blur = spread = 0时完全不可见(与上游一致); - clip:只裁剪内容填充,不影响阴影(上游语义最容易误解的一条);
- 内容层叠:drop 时内容在阴影上、inset 时内阴影在内容上;
- 预设:Figma 默认 / 内阴影 / 硬阴影一键还原上游
art/shadow.png与shadow-inset.png的典型形态。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
width / height | 输出 PNG 的像素尺寸(内容盒 + 两侧 margin)。 |
sigma | 本次渲染实际采用的高斯 σ(px),验证 sigma = blur / 2 约定。 |
ms | 内核渲染 + PNG 编码耗时(TimeSource.Monotonic 计时)。 |
bytes | PNG 字节数,与 base64 长度互为印证。 |
pngBase64 | RGBA PNG 的 Base64 文本,ArkTS 解码为 PixelMap 直接显示。 |
内核侧完整语义(spread 双向缩放、每角圆角钳位、精确可分离高斯卷积、inset 反相、clip、层叠、参数校验)全部在 lib/src/commonMain 里随编译进入 .so,21 项 JVM 测试钉住"语义没有漂移"。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游 6 个 Compose 文件 git mv 到 lib/src/composeMain(未注册源码集,永不编译,一行不改);合成语义逐条移植为内核,ShadowRenderer.kt KDoc 带上游行号对照表。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证;lib 出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);samples/(上游 CMP 示例 App)留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,载荷为 PNG Base64;不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 内核零第三方依赖(不引 Skiko、不引序列化库);工具链钉 HarmonyOS Kotlin 2.2.21-1.0.0 社区多数派线。 |
| UI 完整 | 单页覆盖全部桥接入口:形状、八个参数滑杆、色板与透明度、inset/clip/内容三个开关、三个预设、状态行与版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。绘制类库的验收面不是"页面长得多像上游 Demo",而是"内核的合成语义在真机上逐像素成立"——阴影的模糊半径、扩张量、偏移、内阴影反相都要与上游在 JVM 上的行为对得上。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认"语义提取"路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:内核与协议建模 ── 上游 Shadows.kt 语义逐条对照、PNG 载荷、JSON 契约、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 版本握手、参数面板、防抖重渲染、状态行
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
lib/src/commonMain/ 新增:纯 Kotlin 阴影内核(ShadowShape / ShadowSpec / ShadowBitmap / ShadowRenderer)
lib/src/commonTest/ 新增:21 项 JVM 交叉验证测试
lib/src/composeMain/ 上游 Compose 源文件隔离区(未注册、永不编译,git mv 保留历史)
example/nativeApp/ Kotlin/Native 动态库(Bridge + PngEncoder)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
samples/ 上游 CMP 示例 App,留盘不进 settings
README.md / README.upstream.md fork 主页 + 上游原文保存
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
doc/ 本文章与真机效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :lib 与 :example:nativeApp,rootProject.name = "ohos_compose_shadow"。上游的 AGP / Compose 插件 / vanniktech 发布 / GitHub Packages 发布段 / binary-compatibility-validator / ktlint 全部随根构建替换剥离——本 fork 不发 Maven 仓库,samples/ 依赖 compose 插件,随之留盘。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,提供 ohosArm64() / ohosX64()) |
| JDK | 21(org.gradle.java.home 钉 Temurin 21.0.12) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 8.11.1 升级;2.2.21 线要求) |
| 依赖 | 内核与桥零第三方依赖(不引 Skiko / kotlinx 库) |
| DevEco product | 5.1.0(18) 兼容,API 18 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.composeshadow |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 位置 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
lib/src/commonMain(上游) | Shadows.kt + BlurMaskFilter.kt (expect) | androidx.compose.ui.* 全量 | git mv 隔离到 composeMain |
lib/src/{android,jvm,native,wasmJs}Main(上游) | BlurMaskFilter 四平台 actual | android.graphics.BlurMaskFilter / org.jetbrains.skia | git mv 隔离到 composeMain |
lib/src/commonMain(新增) | 内核 4 文件 | 纯 Kotlin,零依赖 | 进 ohos + jvm 构建 |
lib/src/commonTest(新增) | 2 个测试类 | kotlin("test") | 仅 JVM 运行 |
expect 声明全库只有 1 处:internal expect fun createBlurPaint(density, blurRadius, color): Paint。它返回的是 Compose Paint(带 BlurMaskFilter),本身就是"平台绘制管线"的一部分——内核不移植这个函数,而是移植它参与形成的合成语义:jvm/native actual 里 MaskFilter.makeBlur(NORMAL, blurRadiusPx / 2) 的 sigma 约定、画笔颜色与遮罩的乘法关系,这些才是跨桥时必须保真的部分。
1.4 页面边界
标题区 compose-shadow · OpenHarmony / 副标题(CSS box-shadow · Kotlin/Native ohosArm64/ohosX64)
版本握手行 aboutToAppear 调 shadowVersion(),显示 {"ok":true,"version":"0.1.3-ohos.1"}
Tab 切换 阴影画板 / 关于
预览区 8×8 棋盘格(透明度指示)+ Image(PixelMap),280×280
控制区 形状三选 / blur·spread·offsetX·offsetY·radius·width·height·margin 八滑杆
8 预设色板 + alpha 滑条 / inset·clip·内容填充三开关 / 三预设按钮
状态行 最近一次渲染的 sigma、耗时、尺寸、PNG 字节数与错误信息
关于区 版本握手、五层架构、上游溯源、仓库与社区链接
页面启动时先做版本握手,再按当前参数发起首次渲染;参数变化 150ms 防抖后重渲染。
第 2 阶段:目标与依赖打通
2.1 制品线验证(绘制类库的题面更简单)
KStore 适配要先验证 kotlinx 四件套的 ohos 制品;compose-shadow 内核零第三方依赖,需要验证的只有两件事:其一,eazytec Maven 上的 HarmonyOS Kotlin 2.2.21-1.0.0 发行版提供 ohosArm64() / ohosX64() target(这是唯一入口,Maven Central 官方 Kotlin 没有这两个 target);其二,本机 Konan 已缓存 ohos 交叉编译工具链(sysroot-hms / cpf-llvm)。验证通过后,commonMain 一行依赖坐标都不用写。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
社区两条工具链线:多数派 2.2.21-1.0.0 + Gradle 8.14.1(qrose / collapsing-toolbar / ktoast 等约 40 个),新派 2.4.20-0.1.0-07 + Gradle 9.x(跟着 kotlinx 新制品走的库)。本内核零依赖,没有"依赖元数据反推版本"的约束,直接选多数派线;上游源码本来就是 Kotlin 2.2.20 编的,2.2.21 与之同代,源码级兼容成本为零。Gradle 从上游 8.11.1 升到 8.14.1(与 qrose wrapper 逐字节一致),gradle.properties 钉 JDK 21 与 kotlin.native.ignoreDisabledTargets=true。
settings.gradle.kts 把 eazytec 双仓写在最前(顺序错了配置期直接 Unresolved reference: ohosArm64):
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()
}
}
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 = "ohos_compose_shadow"
include(":lib")
include(":example:nativeApp")
2.3 编译内核
lib/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
实测:
.\gradlew.bat :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64 # BUILD SUCCESSFUL
上游 6 个文件一个字没动(只是移动位置),内核 4 个文件是新增纯 Kotlin。
2.4 测试任务的处置
ohos target 没有 test runner,根构建全局禁用:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
测试统一在 JVM 跑(:lib:jvmTest),ohos 侧只承担编译与链接。
第 3 阶段:内核与桥协议建模(技术核心)
3.1 语义对照:上游 Shadows.kt → 内核
这是整个适配的锚点,逐条对照(行号指上游文件):
| 上游行为 | 位置 | 内核实现 |
|---|---|---|
spreadSize = ±2·spread 上求 outline | Shadows.kt:50-54,68 | 阴影盒四边外扩(drop)/ 内缩(inset)spread;圆角数值不变,按新尺寸 min(w,h)/2 钳位(对齐 createOutline) |
canvas.translate(offset - spread) | Shadows.kt:66 | drop 等价于「外扩后平移 (offsetX, offsetY)」;inset 采用对称解读(见 8.2) |
MaskFilter.makeBlur(NORMAL, blur/2) | BlurMaskFilter.jvm/native.kt | sigma = blurRadiusPx / 2,精确可分离高斯卷积,核半径 ceil(3σ)、边界 clamp |
clip 原 outline + ColorMatrix(alphaScale=-1, shiftAlpha(255·colorAlpha)) + saveLayer | Shadows.kt:97-112 | 内阴影 alpha = colorAlpha·(1−cov)·insideCov,RGB 取阴影色 |
shadowModifier.clip(shape) 只包内容链 | Shadows.kt:80 | 只把内容填充乘原 shape coverage,阴影永不裁剪 |
if (inset) drawContent() 先画内容 | Shadows.kt:48,77 | drop:内容在阴影上;inset:内阴影在内容上 |
inset 公式的完整推导(第 5 节"决策 4"展开):模糊层进入 ColorMatrix 时每个像素的 alpha 是 255·colorAlpha·cov(画笔颜色 alpha × 模糊遮罩),矩阵 alphaScale = −1、shiftAlpha(255·colorAlpha) 把它映射为 255·colorAlpha − 255·colorAlpha·cov = 255·colorAlpha·(1−cov)。所以内阴影在形状内边缘最强(cov→0)、深入内部趋于 0,blur = spread = 0 时 cov 在内部处处为 1,内阴影完全不可见——与上游视觉行为一致。
3.2 JSON 契约
dev.lennartegb.shadows/**(新增内核:光栅化 + 高斯 + 反相合成)
↓ ShadowRenderer.render(spec, w, h, content, margin) → ShadowBitmap
Bridge.kt(@CName × 2,扁平 JSON 进、PNG Base64 出)
↓ UTF-8 JSON
N-API shadowRender(request) / shadowVersion()
↓
ArkUI Index.ets(画板 + 关于)
请求(全部字段可选、带默认值与钳位)与响应:
请求 : {"shape":"rounded","radius":16,"blur":16,"spread":0,"offsetX":0,"offsetY":0,
"width":160,"height":120,"margin":48,"color":"#40000000",
"content":"#FF673AB7","clip":1,"inset":0}
响应 : {"ok":true,"width":256,"height":216,"sigma":8,"ms":6,"bytes":183297,"pngBase64":"..."}
{"ok":false,"error":"invalid color: #XYZ"} ← 校验失败/非法参数统一走错误通道
参数钳位表:width/height 16…640、margin 8…256、blur 0…200、spread −160…200、offsetX/Y −320…320、radius 与每角圆角 0…320;出现任一 topLeft/topRight/bottomRight/bottomLeft 字段即启用每角独立圆角(radius 作未指定角缺省);color 支持 #RRGGBB 与 #AARRGGBB,默认 #40000000(Figma 默认 drop shadow:黑 25%)。
3.3 载荷为什么是 PNG 位图而不是矢量
同为绘制类库,compose-continuous-rounded-corner-shape 的桥返回 SVG path 字符串(矢量几何无损重绘);阴影不一样——blur 是位图效果,模糊后的每个像素 alpha 都携带信息,没有等价的矢量表达。选择 PNG(zlib stored 未压缩块 + adler32 + crc32 全手写编码,qrose 同款)+ Base64 文本:一次典型渲染(256×216)PNG 约 180 KB,Base64 后约 240 KB,150ms 防抖下真机交互流畅;画布尺寸被钳位在 640+2×256 以内,体积上限可控。
3.4 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("ShadowRender")
fun shadowRender(request: String): String = try {
/* 解析 → 钳位 → ShadowSpec → render → PNG → Base64 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、非法颜色、超范围参数(钳位兜底后基本不会触发)、内核校验失败(消息与上游逐字一致,如 blurRadius can't be negative.),全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libshadow_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosshadow.so)
│ ShadowRender / ShadowVersion
▼
纯 Kotlin 内核 → PNG → Base64 → DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;Modifier 更是没有任何可导出面。所以与 qrose / KStore 相同:C ABI + JSON + 位图载荷。
4.2 Kotlin/Native 导出函数
@CName("ShadowRender")
fun shadowRender(request: String): String // 参数解析 + 钳位 + 渲染 + PNG,一口进
@CName("ShadowVersion")
fun shadowVersion(): String = """{"ok":true,"version":"0.1.3-ohos.1"}"""
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ShadowRender | shadowRender(req) | 语义对照表全部行为 + PNG 编码 |
ShadowVersion | shadowVersion() | fork 版本握手(真机版本行的数据来源) |
桥内 JSON 用手写 MiniJson(扁平 key-value,无数组无嵌套)——sharedLib 不引 kotlinx.serialization,避免把序列化运行时整包拖进 so;扁平协议也不需要。计时用 TimeSource.Monotonic(kotlin.system.getTimeMillis 在 2.2.21-ohos 发行版里是 error 级弃用)。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"shadowRender", nullptr, NapiShadowRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"shadowVersion", nullptr, NapiShadowVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打请求字节数、调 KN 函数并包装返回。KN 返回的 const char* 必须经 libohosshadow_symbols()->DisposeString(raw) 归还 Kotlin 运行时,绝不 free()。libohosshadow_api.h 按社区已审仓库同款手写:只声明 ExportedSymbols 结构体与两个业务符号,不拖整份生成头——前提是前两个成员必须是 DisposeStablePointer / DisposeString(已对照 KN 生成头逐行确认,截断结构体在这两个偏移上 ABI 安全)。
4.4 CMake 和类型声明
set(KN_SO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${OHOS_ARCH})
if(NOT EXISTS ${KN_SO_DIR}/libohosshadow.so)
message(FATAL_ERROR "Missing ${KN_SO_DIR}/libohosshadow.so. Run :example:nativeApp:linkDebugSharedOhosArm64 / OhosX64 first.")
endif()
add_library(ohosshadow SHARED IMPORTED)
add_library(shadow_napi SHARED napi_init.cpp)
target_link_libraries(shadow_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohosshadow)
OHOS_ARCH 为空或 KN so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。Gradle 侧 copyKnSoArm64/X64 任务在 linkDebugShared* 之后自动把 libohosshadow.so 与 ~/.konan/dependencies 里对应 triple(aarch64-linux-ohos / x86_64-linux-ohos)的 libc++_shared.so 拷进 entry/libs/{arm64-v8a,x86_64}。
export const shadowRender: (request: string) => string;
export const shadowVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 版本握手和触发渲染
aboutToAppear(): void {
this.version = shadowVersion(); // 版本握手:来自真实 Native 调用
this.scheduleRender();
}
private shadowColorHex(): string { // alpha 滑条(0..100) 合成进 #AARRGGBB
const alphaByte: number = Math.round(this.alpha * 2.55);
return `#${alphaByte.toString(16).toUpperCase().padStart(2, '0')}${this.color.substring(1)}`;
}
private render(): void {
const body: string = JSON.stringify({
shape: this.shape, blur: this.blurPx, spread: this.spread,
offsetX: this.offsetX, offsetY: this.offsetY, radius: this.radiusPx,
width: this.boxWidth, height: this.boxHeight, margin: this.marginPx,
color: this.shadowColorHex(),
content: this.showContent ? '#FF673AB7' : '',
inset: this.inset, clip: this.clipContent
});
const parsed: ShadowResponse = JSON.parse(shadowRender(body)) as ShadowResponse;
if (!parsed.ok || parsed.pngBase64 === undefined) { /* 状态行显示 error */ return; }
this.decodePng(parsed.pngBase64); // Base64Helper → createImageSource → PixelMap → @State 刷新
}
参数变化统一走 150ms setTimeout 防抖,滑杆拖动不会打爆 Native 调用;content 传空串表示不画内容填充。
5.2 页面交互
阴影画板自上而下:8×8 棋盘格预览区(灰白相间指示透明度,中央叠 PixelMap)、形状三选、八个带 px 标签的滑杆(blur / spread / offsetX / offsetY / radius / width / height / margin)、8 预设色板 + alpha 滑条、inset / clip / 内容填充三个开关、三个预设按钮、状态行(sigma、耗时、尺寸、PNG 字节数)。关于 Tab 展示版本握手、五层架构(ArkTS → libshadow_napi.so → libohosshadow.so → 纯 Kotlin 内核 → PNG)、上游溯源与仓库 / 社区链接。
5.3 对比度和错误边界
ok:false 时错误原文进状态行;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行并 hilog 打点;PixelMap 解码失败有独立 catch 分支。预览区用棋盘格而不是纯色背景,透明度差异(阴影 alpha、clip 前后)一眼可辨。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosshadow)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/org/terminator/ohos/shadow/
│ ├── Bridge.kt @CName × 2 + MiniJson + 参数钳位(167 行)
│ └── PngEncoder.kt 纯 Kotlin PNG(stored 块 + adler32 + crc32)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.composeshadow、versionName 0.1.3-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/libohosshadow_api.h
├── src/main/cpp/types/libshadow_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面(547 行)
6.2 构建、签名和安装
.\gradlew.bat :lib:jvmTest :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64
.\gradlew.bat :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 中勾选自动生成,证书写入本机,不进入仓库(仓库内 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.terminator.ohos.composeshadow -m entry
四、完整代码对照
4.1 整体架构
lib/src/commonMain(新增纯 Kotlin 内核)
│ ShadowSpec 校验(消息与上游逐字一致)
│ ShadowRenderer:coverage 光栅化 → 高斯(σ=blur/2) → spread/offset → 合成
lib/src/composeMain(上游原文件,git mv 隔离,永不编译)
│ Shadows.kt / BlurMaskFilter.kt + 4 actual
Bridge.kt + MiniJson + PngEncoder
│ ShadowRender / ShadowVersion(@CName × 2)
libshadow_napi.so(CMake NAPI)
│ shadowRender / shadowVersion
Index.ets
│ JSON.stringify → JSON.parse → Base64 → PixelMap
▼
ArkUI 真机页面(画板 + 关于)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_compose_shadow、samples 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单插件钉版 |
build.gradle.kts(根) | 版本 0.1.3-ohos.1、全局禁用 Ohos*Test* 任务 |
lib/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零依赖 |
lib/src/composeMain/**(移动) | 上游 6 个 Compose 文件原样隔离,git 历史 100% 保留 |
lib/src/commonMain/**(新增) | ShadowShape / ShadowSpec / ShadowBitmap / ShadowRenderer(359 行,KDoc 带上游行号对照) |
lib/src/commonTest/**(新增) | 21 项 JVM 交叉验证(216 + 109 行) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、MiniJson、参数钳位、错误通道、PNG Base64 |
nativeApp/.../PngEncoder.kt | 手写 PNG 编码(IHDR / stored IDAT / adler32 / crc32) |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面(画板 + 关于,547 行) |
README.OpenHarmony.md / _CN.md | 鸿蒙适配说明(中英双份) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 内核 | ShadowSpec(shape, blur, spread, offsetX/Y, colorArgb, inset, clip) | 参数模型 + 上游同款校验(消息逐字一致) |
| 内核 | ShadowRenderer.render(spec, w, h, content, margin) | 光栅化 + 高斯 + 反相合成 + 层叠,输出 ARGB 位图 |
| 内核 | spec.sigmaPx() | blurRadiusPx / 2,Skiko sigma 约定 |
| Native | ShadowRender / ShadowVersion | C ABI JSON 导出 |
| N-API | shadowRender(req) / shadowVersion() | 向 ArkTS 暴露能力 |
| ArkTS | render() / applyPreset() | 组请求、防抖、预设一键还原 |
4.4 ArkTS 与 Kotlin 的边界
const parsed: ShadowResponse = JSON.parse(shadowRender(JSON.stringify({
shape: 'rounded', blur: 16, spread: 0, offsetX: 0, offsetY: 0,
radius: 16, width: 160, height: 120, margin: 48,
color: '#40000000', content: '#FF673AB7', clip: 1, inset: 0
}))) as ShadowResponse;
// parsed.ok === true;Base64 解码 → PixelMap → Image 显示
val spec = ShadowSpec(shape, blur, spread, offsetX, offsetY, colorArgb, inset, clip)
val bitmap = ShadowRenderer.render(spec, width, height, contentArgb, margin)
val png = PngEncoder.encode(bitmap.argb, bitmap.width, bitmap.height)
// {"ok":true,"width":..,"height":..,"sigma":..,"ms":..,"bytes":..,"pngBase64":".."}
两者之间只传输扁平 JSON 字符串和 Base64 位图文本,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:提取"合成语义内核",而不是整库编译或重写 UI
上游 API 完全构建在 Modifier / DrawScope / Paint 上:没有纯 Kotlin 可编译的"引擎层",也没有任何 @CName 面。社区 CMP 鸿蒙线(1.9.2)与上游版本(1.9.1)虽然数字接近,但已审核的约 60 个库适配无一在库内编译 Compose——把 Compose 拖进 ohos 构建意味着引入一条没有审核先例的工具链路径。而阴影的合成语义(spread 缩放、sigma 约定、inset 反相、clip 边界、层叠)是确定性数学,可以在纯 Kotlin 里逐条复刻、在 JVM 上逐条测试。提取语义而不是搬源码集,是绘制类库适配的可复制模式。
决策 2:工具链选多数派线 2.2.21-1.0.0 + Gradle 8.14.1
内核零第三方依赖,不存在"依赖元数据反推 Kotlin 版本"的约束(KStore 选 2.4.20 线正是因为 coroutines 制品线逼的)。多数派线的好处是与其他已审仓库的 wrapper、gradle.properties、settings 模板逐字节一致,审核与维护成本最低;上游 Kotlin 2.2.20 与 2.2.21 同代,源码级兼容零成本。
决策 3:根构建替换(cmp_qrose 模式),samples 留盘
上游根构建(AGP / Compose 插件 / vanniktech 发布 / GitHub Packages / binary-compatibility-validator / ktlint)对鸿蒙没有保留价值,整体替换为极简 ohos 构建。samples/ 是完整 CMP App,依赖 compose 插件与 Android 配置,留盘不进 settings.gradle.kts——与 qrose 处理上游 androidapp/desktopApp 的方式一致,ohos Compose 制品线可用后可整体复活。
决策 4:inset 公式按 ColorMatrix 严格推导,spread 采用对称解读
字面上"反相"容易写成 clamp(colorAlpha − cov)——这个公式在 50% 透明色时内阴影完全消失,一眼看似合理、实则是错的。正确推导必须计入画笔颜色 alpha:模糊层像素 alpha 是 255·colorAlpha·cov,经 alphaScale = −1 + shiftAlpha(255·colorAlpha) 后为 255·colorAlpha·(1−cov)。开发中错误公式直接被两条 JVM 测试(半透明内阴影可见性、内边缘 alpha>0)拦下,避免了带病上线。另一处有意偏差:inset + spread≠0 时上游字面 translate(offset − spread) 会让内阴影向左上偏 2·spread,内核采用与 drop 对称的 Figma 读法(先内缩后平移 (offsetX, offsetY)),已在内核 KDoc 与适配说明中明确标注。
决策 5:21 项 JVM 交叉验证是"语义没漂"的唯一可信证明
与 KStore 不同,内核是新代码而非上游原件——"换线不换版"的论证不适用,必须用测试钉住每条语义:spread 扩张/收缩的包围盒、offset 质心位移、sigma 数值、inset 在 blur=spread=0 时全图 alpha=0、clip 只裁内容不裁阴影、层叠顺序、校验异常消息与上游逐字相同、圆形 coverage 旋转对称。测试先于桥接存在,桥接只是把被测试钉住的内核暴露出去。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin 21.0.12)、Gradle 8.14.1、HarmonyOS Kotlin 2.2.21-1.0.0、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :lib:jvmTest
21 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
ShadowRendererTest | 12 | blur=0 硬边位置;sigma=blur/2 与 3σ 外 alpha=0;spread 正/负对包围盒的影响(±2px 容差);offset 质心位移(±1px);inset 盒外恒 0、零参数不可见、内边缘可见;clip 只裁内容(圆角外无内容色);drop/inset 层叠顺序;圆形旋转对称(4 对称点相等) |
ShadowSpecTest | 9 | 校验异常消息逐字一致(blurRadius can't be negative. 等)、校验顺序、NaN/Infinity 映射到 must be specified、sigmaPx() 数值 |
性能实测:JVM 上 720×720(blur=20、spread=6、圆角 24、内容合成)单帧 72ms——真机画板默认 256×216 规模下为毫秒级,防抖交互无压力。
6.3 HAP 验证
:lib:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:example:nativeApp:linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL
llvm-nm -D: ShadowRender / ShadowVersion 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libshadow_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
| 产物 | arm64-v8a | x86_64 |
|---|---|---|
libohosshadow.so(debugShared,未 strip) | 2,608,616 B(≈2.61 MB) | 2,231,736 B(≈2.23 MB) |
libc++_shared.so | 1,850,184 B | 1,731,432 B |
libshadow_napi.so | CMake 产物,三件套同目录 | 同左 |
6.4 功能用例
- 启动后版本握手行显示
{"ok":true,"version":"0.1.3-ohos.1"},来自真实 Native 调用(hilog 可见ShadowNapi打点); - 默认参数(圆角 16 / blur 16 / 黑 25%)首帧即渲染,预览区棋盘格上出现带模糊外阴影的内容盒;
- 拖动 blur 滑杆 0↔60,阴影边缘从硬边连续过渡到弥散;状态行 sigma 数值同步变化(恒等于 blur/2);
- spread 拉到正值阴影扩大、负值收缩;offsetX/Y 改变投影方向(负 offsetY 即"向上"投影,与截图一致);
- 切换方形 / 圆角 / 圆形,阴影轮廓随之变化;圆角滑杆只影响 rounded 形态;
- 打开 inset:阴影转为内容盒内边缘的内阴影,
blur=spread=0时完全消失(与上游一致);offset 拖动可见内阴影偏向; - 关闭 clip:内容填充溢出到圆角外(直角矩形填充);开启 clip:内容严格裁剪进形状——验证"clip 只裁内容不裁阴影";
- 色板换色 + alpha 滑条改变浓度;三预设一键还原 Figma 默认 / 内阴影 / 硬阴影;
- 每次渲染状态行显示
sigma / ms / 尺寸 / PNG 字节数,异常参数被钳位、非法颜色走ok:false通道显示 error 原文。
6.5 验证结论
阴影合成内核、Kotlin/Native 双 ABI 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,画板按参数实时渲染:版本握手、drop/inset 阴影、spread/offset/形状/颜色全参数覆盖、clip 语义、层叠顺序与上游在 JVM 上被 21 项测试钉住的行为一致(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-shadow.git
cd compose-shadow
export JAVA_HOME="/path/to/jdk-21"
./gradlew :lib:jvmTest # 21 项交叉验证
./gradlew :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64 # klib
./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.composeshadow -m entry
hdc shell hilog | grep Shadow
八、遗留问题与改进方向
8.1 踩坑复盘
settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64(解析到了官方 Kotlin);- ohos target 没有 test runner,
Ohos*Test*任务要在根构建全局禁用,否则 CI/无设备环境卡死; kotlin.system.getTimeMillis()在 2.2.21-ohos 发行版是 error 级弃用,计时改用TimeSource.Monotonic.markNow().elapsedNow();- ArkTS 的
@Component struct继承CustomComponent,基类把通用属性方法(.blur()/.radius()/.margin()/.clip())合成为同名成员,@State blur / radius / margin / clip直接撞名编译失败——改名blurPx / radiusPx / marginPx / clipContent;而夹在报错行之间的color / alpha / inset未报错,说明冲突是按基类成员精确匹配的,不必无差别加前缀; - 桥两侧字段协议失配只在端到端首跑时暴露:ArkTS 读
pngBase64而桥发的是png;alpha 作为独立字段发送、桥只认#AARRGGBB——修复为响应补bytes字段、请求侧shadowColorHex()把 alpha 滑条合成进颜色。JSON 字段名应当作跨语言契约管理; - inset 公式的字面减法
clamp(colorAlpha − cov)是错的(漏了画笔 alpha 的乘法参与),半透明色的内阴影会整体消失——必须按 ColorMatrix 全链路推导(见决策 4),两条 JVM 测试把它拦在了集成之前; - 手写
libohosshadow_api.h的截断ExportedSymbols结构体只有在前两个成员恰为DisposeStablePointer/DisposeString时才 ABI 安全,写之前必须对照 KN 生成头逐行确认; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须经
DisposeString归还运行时,N-API 层不能free(); - 签名材料由 DevEco 自动写入
build-profile.json5,仓库内signingConfigs保持空数组,证书绝不进入 AtomGit。
8.2 已知问题
- inset + spread≠0 时内核采用对称解读(上游字面 translate 会向左上偏
2·spread),是与上游的一处有意偏差,已在内核 KDoc 标注; - coverage 光栅化是像素中心二值采样、无抗锯齿,边缘平滑完全依赖高斯模糊;上游 GPU 路径在绘制时抗锯齿;
- 上游 Android actual 用
android.graphics.BlurMaskFilter(radius)是另一套平台半径约定,上游本身跨平台就有此差异;内核跟 Skiko 多数派(sigma = blur/2); - PNG 为 zlib stored 未压缩块,大画布体积可观(受 640+2×256 钳位约束),Base64 再 ×4/3;
- 桥是同步一次一调用,适合参数化取图,不适合逐帧动画流水线;
samples/(上游 CMP 示例 App)不进构建;本 fork 不发 Maven 仓库,上游发布链路随根构建剥离。
8.3 未来优化方向
- 评估"显示列表/帧协议"(KLoading 桥先例)支持实时阴影动画,替代每次全量 PNG;
- PNG 编码升级为 deflate 压缩块,或按内容自适应调色板,压掉 stored 块的体积冗余;
- 增加 ArkUI 原生
.shadow()属性的并排对照模式,直观呈现内核与系统渲染的差异; - 在 CI 中加入 ohos 编译、ELF 符号检查(
llvm-nm断言ShadowRender/ShadowVersion为全局 T)和 HAP 构建; - 扩展形状族(多边形 / 路径)与多重阴影(上游单 modifier 叠加语义)。
九、总结
9.1 核心难点回顾
上游 Shadows.kt(全部站在 Compose 上)
→ 判定"提取合成语义"路线(社区 ~60 库一致不编 Compose)
→ 根构建替换(cmp_qrose 模式,Kotlin 2.2.21-1.0.0 + Gradle 8.14.1)
→ 上游 6 文件 git mv 隔离到 composeMain
→ 纯 Kotlin 内核(spread/offset/σ=inset 反相/clip/层叠,KDoc 行号对照)
→ 21 项 JVM 测试钉住语义(含 ColorMatrix 公式纠错)
→ ohosArm64/ohosX64 sharedLib(@CName × 2 + MiniJson + PNG)
→ C++ N-API(DisposeString 纪律,截断 api 头 ABI 校验)
→ ArkTS 画板(防抖重渲染 + 状态行)
→ ArkUI 真机验收(版本握手 / 全参数 / 预设 / clip 语义)
9.2 封装层次
- 内核层:参数校验(上游同款消息)、coverage 光栅化、精确高斯、inset 反相合成、clip 与层叠;
- Native 层:双 ABI 动态库,2 个 C ABI 输出 JSON + PNG Base64;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:参数面板、防抖调度、位图解码、状态行与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 绘制类库的适配单元是"合成语义"而不是"源码集":
Modifier没有任何可导出面,但它的行为是确定性数学——语义可推导、可测试,就可以跨桥;qrose 提编码器、continuous-corner-shape 提几何、本项目提阴影合成,是同一条路的三次落地; - 零依赖内核的工具链选多数派线而不是最新线:没有依赖元数据反推约束时,
2.2.21-1.0.0 + Gradle 8.14.1与其他已审仓库的模板逐字节一致,审核与维护成本最低;Gradle 版本跟着工具链线走(2.2.21 线升 8.14.1),不沿用也不抢跑; - 桥两侧的字段协议是跨语言契约,要端到端对拍:
pngvspngBase64、独立 alpha vs 合成#AARRGGBB这类失配,单侧编译都发现不了,只有首跑才暴露——桥协议字段表应当在写第一行桥代码前定稿,并让两侧从同一张表生成/核对。
9.4 适配成果
当前适配已完成:
- compose-shadow
0.1.3阴影合成语义的纯 Kotlin 内核(4 文件 359 行核心 + 21 项测试),ohosArm64/ohosX64双 ABI Kotlin/Native 编译(libohosshadow.so≈2.61 MB / ≈2.23 MB); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 验收页面(画板 + 关于,547 行):版本握手、全参数滑杆、三预设、inset / clip / 内容开关、状态行;
- 21 项 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。
参考文档
更多推荐


所有评论(0)