本文记录 uri-kmp 接入 OpenHarmony 的完整过程,覆盖 KMP 工程盘点、ohosArm64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 交互页面、无签名 HAP 构建、依赖检查和签名交接。

本次适配复用 URI KMP 的解析、编码、解码、Builder 和 URI 分类能力,再由 ArkTS 调用 Kotlin/Native 生成的 liburi.so。示例页面不是固定数据验证页,而是一个可以输入 URI、选择操作、查看结构化结果和回放最近操作的 URI 交互工作台。

项目地址: AtomGit/oh-tpc/uri-kmp

上游仓库: eygraber/uri-kmp

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做开源鸿蒙平台 KMP/CMP 适配

uri-kmp 是一个 Kotlin Multiplatform URI 和 URL 库,公共实现位于 commonMain,提供 URI 解析、编码、解码、Builder、查询参数和 URI 类型判断。原工程可以在 JVM、JS、Wasm 和其他 Kotlin 平台使用,但 OpenHarmony 需要一条独立的 ARM64 Native 构建和 ArkTS 消费链路。

如果只把示例页面重新写成 ArkTS,页面可以显示几个固定 URI,却无法证明公共 Kotlin 代码已经运行在 OpenHarmony ARM64 目标上。适配需要解决下面几个问题:

障碍具体问题
目标缺失KMP 模块默认没有 OpenHarmony 目标,必须增加 ohosArm64() 才能生成 KLIB。
工具链不一致Kotlin/Native、OpenHarmony LLVM、Native SDK、Gradle 和 JDK 需要保持兼容。
语言边界不同ArkTS 不能直接持有 Kotlin Uri 对象,必须经过 C ABI、C++ N-API 和 JSON。
内存所有权Kotlin/Native 返回的字符串需要由明确的 UriFree 接口释放。
API 体验示例不能只调用 runChecks(),需要让用户实际输入 URI 并查看库的能力。
交付链路复杂Native 动态库、CMake、HAP、签名和 ARM64 依赖需要分别验证。

因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、C ABI/N-API 桥接层和 ArkUI 交互层。URI 语义仍由 KMP 维护,ArkTS 只负责输入、操作选择、页面状态和错误展示。

1.2 库提供的能力

uri-kmp 公共模块提供以下能力:

  • Uri.parse:解析绝对 URI、相对 URI、hierarchical URI 和 opaque URI;
  • Uri.scheme、authority、host、port:读取 URI 的 authority 结构;
  • Uri.path、query、fragment:读取解码后的路径、查询和片段;
  • Uri.isHierarchical、isOpaque、isRelative:判断 URI 类型;
  • Uri.encode、Uri.decode:完成百分号编码和解码;
  • Uri.Builder:构建和追加路径、查询参数;
  • getQueryParameter、getQueryParameterNames:读取查询参数;
  • Uri.parseOrNull:将无法解析的输入转换为 null;
  • UriExamples.runChecks:在 JVM、Kotlin/Native 和 ArkTS 示例中复用同一组检查。

示例页面用三个真实场景展示这些能力:

场景示例主要能力
Web 链接https://example.com/path?q=hello world#sectionscheme、host、path、query、fragment
Content URIcontent://media/external/audio/media/1层级 URI 和路径解析
Mailto URImailto:nobody@example.comopaque URI 和 scheme-specific part

1.3 实现适配

维度要求
代码复用URI 解析、编码、解码、Builder 和公共检查继续由 Kotlin 共享。
平台目标为 uri 和示例 Native 模块加入 ohosArm64,生成 liburi.so。
桥接稳定使用少量 C ABI 函数和 JSON,避免把 Kotlin 对象地址交给 ArkTS。
UI 完整页面支持输入、模式切换、示例填充、结果查看、错误提示和最近操作。
可测试JVM 测试、Native 链接、ELF 依赖、C++ 编译和 HAP 打包分别验收。
签名安全源码只提供签名工程准备入口,证书、profile 和密码留在开发者本机。
仓库规范项目地址、文章链接和源码协作地址统一使用 AtomGit。

本项目的 ArkUI 页面是独立的 URI 功能验收宿主,公共 API 仍然保持平台无关。其他 KMP/CMP 应用可以复用 Uri,再自行决定如何呈现 URI 解析结果。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点 URI API、KMP 模块和 OpenHarmony 示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:URI 与序列化   ── 建立示例模型、JSON 字段和统一错误响应
第 4 阶段:原生桥接       ── Kotlin/Native C ABI、C++ N-API、内存释放和类型检查
第 5 阶段:交互页面       ── ArkUI 输入、解析、编码解码、结果卡片和操作历史
第 6 阶段:构建与验证     ── Native 依赖、Hvigor HAP、签名交接和效果图

每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证 URI 公共模型,再把相同代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后由 ArkUI 页面调用解析、编码和解码接口。

三、逐步实现过程

第 1 阶段:项目初始化

1.1 盘点公共 API 和工程边界

项目采用与参考 KMP/CMP 工程一致的分层:

uri/                         URI KMP 核心库
example/shared/              示例门面、固定样例和 JVM 验收测试
example/nativeApp/           ohosArm64 Kotlin/Native 动态库
example/ohosApp/             DevEco Stage 工程和 ArkUI 页面
scripts/                     Native、HAP 和签名工程辅助脚本
docs/openharmony/            构建、验收和文章素材

example 是独立 Gradle 工程,不把 DevEco 工程当作 Kotlin 子模块。这样可以分别执行 Gradle 和 Hvigor,也可以把 ohosApp 复制到其他目录后在 DevEco Studio 中配置签名。

1.2 固定工具链和版本矩阵
项目配置用途
Kotlin Multiplatform2.2.21-1.0.0JVM、Kotlin/Native 和 KLIB
JDK21Gradle、Kotlin 编译和 Native 任务
OpenHarmony 目标ohosArm64ARM64 Native 动态库
DevEco product6.0.0(20)Stage 工程兼容和目标版本
DevEco Studio6.0.0 ReleaseIDE 版本,负责工程同步、HAP 构建与签名
ABIarm64-v8aHAP 原生库架构

执行 Gradle 脚本前先选择 JDK 21:

export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 创建 OpenHarmony 示例页面

示例页面围绕“输入、操作、结果和历史”组织,而不是把 runChecks() 放在页面中心:

标题区          URI KMP 三方库 / OpenHarmony 交互示例
输入区          URI 或普通文本输入框
操作区          解析 URI / 编码 / 解码
示例区          Web 链接 / Content URI / Mailto
结果区          URI 字段卡片或编码解码结果
历史区          最近操作和再次使用

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

2.1 加入 ohosArm64 目标
plugins {
  kotlin("multiplatform")
}

kotlin {
  explicitApi()
  jvm()
  ohosArm64()
}

OpenHarmony 的 Native 目标会直接编译 commonMain,因此 URI 解析器、编码器和 Builder 不需要再写一份 ArkTS 或 C++ 实现。

2.2 Native focused build 的作用

example/nativeApp 构建一个受 linker map 约束的 shared library:

kotlin {
  ohosArm64 {
    binaries.sharedLib {
      baseName = "uri"
      linkerOpts(
        "--entry=0",
        "--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
      )
      linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
    }
  }
}

链接器只导出示例需要的符号:

UriCatalog
UriGet
UriRunChecks
UriParse
UriEncode
UriDecode
UriFree
2.3 配置独立消费工程

example/shared 通过项目依赖消费 URI 库:

kotlin {
  sourceSets {
    commonMain.dependencies {
      api("com.eygraber:uri-kmp:${property("uriVersion")}")
    }
  }
}

example/nativeApp 再依赖 shared,从而保证 Native 示例和 JVM 示例使用同一份 URI 门面。

2.4 通过构建产物消费共享库

prepareOhos 在 Native 链接成功后复制动态库和 C 头文件:

val prepareOhos by tasks.registering(Copy::class) {
  dependsOn("linkDebugSharedOhosArm64")
  from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
    include("liburi.so")
    into("libs/arm64-v8a")
  }
  from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
    include("liburi_api.h")
    into("src/main/cpp/include")
  }
  into(rootProject.layout.projectDirectory.dir("../example/ohosApp/entry"))
}

生成文件为:

example/ohosApp/entry/libs/arm64-v8a/liburi.so
example/ohosApp/entry/src/main/cpp/include/liburi_api.h

第 3 阶段:URI 与序列化

3.1 为什么需要统一 JSON 契约

ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin Uri 对象,因此边界使用 UTF-8 JSON:

ArkTS input / operation
        ↓ string
N-API parseUri / encodeUri / decodeUri
        ↓ C ABI pointer
Kotlin Uri.parse / Uri.encode / Uri.decode
        ↓ JSON string
ArkUI result card

页面只消费 JSON,不复制 URI 的解析规则。这样 JVM 测试和 OpenHarmony 页面使用同一份 Uri 实现。

3.2 解析结果模型
{
  "value": "https://example.com/path?q=hello%20world#section",
  "scheme": "https",
  "authority": "example.com",
  "host": "example.com",
  "port": -1,
  "path": "/path",
  "query": "q=hello world",
  "fragment": "section",
  "schemeSpecificPart": "//example.com/path?q=hello world",
  "hierarchical": true,
  "opaque": false,
  "relative": false
}

对于 mailto:nobody@example.com,opaque 为 true,path 和 host 没有层级 URI 的含义;这正是页面区分 HIERARCHICAL 和 OPAQUE 的依据。

3.3 Kotlin/Native JSON 边界
private fun Uri.toJson(): String = buildString {
  append("{\"value\":${quote(toString())}")
  append(",\"scheme\":${scheme.toJsonValue()}")
  append(",\"authority\":${authority.toJsonValue()}")
  append(",\"host\":${host.toJsonValue()}")
  append(",\"port\":$port")
  append(",\"path\":${path.toJsonValue()}")
  append(",\"query\":${query.toJsonValue()}")
  append(",\"fragment\":${fragment.toJsonValue()}")
  append(",\"hierarchical\":$isHierarchical")
  append(",\"opaque\":$isOpaque")
  append(",\"relative\":$isRelative}")
}

Native 异常统一转换成 {"error":"URI input is required"},ArkTS 客户端在解析 JSON 后检查 error 字段,再将它显示在页面状态区。

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

4.1 Kotlin/Native 对象不能直接交给 ArkTS

Kotlin/Native 的 Uri 属于 Kotlin 运行时对象。ArkTS 只能接收 JavaScript 值,C++ 也不能把 Kotlin 对象地址直接当成 JavaScript 对象。

最终采用三层桥接:

ArkTS
  │ JSON string
  ▼
C++ N-API entry
  │ void* / UTF-8 input
  ▼
Kotlin/Native C ABI
  │ Uri.parse / UriCodec
  ▼
UTF-8 JSON + explicit free
4.2 方案对比
方案优点缺点选用
直接导出 Kotlin 对象代码少ABI、生命周期和类型不可控❌
只导出字符串结果实现简单页面无法展示 URI 结构❌
C ABI + JSON边界清晰、易调试、易扩展有一次序列化开销✅
在 ArkTS 重写 URI 解析页面调用简单KMP 和 ArkTS 逻辑容易分叉❌
4.3 Kotlin/Native 导出函数

解析、编码和解码分别复用公共 Uri API:

@CName("UriParse")
public fun parseNative(input: CPointer<ByteVar>?): CPointer<ByteVar> = response {
  requireNotNull(input) { "URI input is required" }
  Uri.parse(input.toKString()).toJson()
}

@CName("UriEncode")
public fun encodeNative(input: CPointer<ByteVar>?): CPointer<ByteVar> = response {
  requireNotNull(input) { "Text input is required" }
  val value = input.toKString()
  "{\"input\":${quote(value)},\"result\":${quote(Uri.encode(value))}}"
}

@CName("UriDecode")
public fun decodeNative(input: CPointer<ByteVar>?): CPointer<ByteVar> = response {
  requireNotNull(input) { "Encoded input is required" }
  val value = input.toKString()
  "{\"input\":${quote(value)},\"result\":${quote(Uri.decode(value, convertPlus = true, throwOnFailure = true))}}"
}

@CName("UriFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
  if (pointer != null) nativeHeap.free(pointer.rawValue)
}
4.4 C++ N-API 方法分发

C++ 注册三个面向用户的 ArkTS 方法,同时保留目录和公共检查方法:

napi_property_descriptor methods[] = {
    {"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
        napi_default, nullptr},
    {"getUri", nullptr, GetUri, nullptr, nullptr, nullptr,
        napi_default, nullptr},
    {"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
        napi_default, nullptr},
    {"parseUri", nullptr, ParseUri, nullptr, nullptr, nullptr,
        napi_default, nullptr},
    {"encodeUri", nullptr, EncodeUri, nullptr, nullptr, nullptr,
        napi_default, nullptr},
    {"decodeUri", nullptr, DecodeUri, nullptr, nullptr, nullptr,
        napi_default, nullptr},
};

输入参数经过 napi_get_value_string_utf8 读取,Native 返回文本被复制成 ArkTS 字符串后立即释放:

ArkTS parseUri(input)
        │
        ▼
napi_get_value_string_utf8
        │
        ▼
UriParse / UriEncode / UriDecode
        │
        ▼
napi_create_string_utf8
        │
        ▼
UriFree(nativeBuffer)
4.5 CMake 引入 Kotlin/Native 动态库
add_library(uri SHARED IMPORTED)
set_target_properties(uri PROPERTIES
  IMPORTED_LOCATION
  "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/liburi.so")

add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE uri libace_napi.z.so)

第 5 阶段:交互页面

5.1 ArkTS 客户端接口

UriClient.ets 对 Native JSON 做轻量类型封装:

export function parseUri(input: string): UriDetails {
  return parse<UriDetails>(uriNative.parseUri(input));
}

export function encodeUri(input: string): UriTransform {
  return parse<UriTransform>(uriNative.encodeUri(input));
}

export function decodeUri(input: string): UriTransform {
  return parse<UriTransform>(uriNative.decodeUri(input));
}

页面不在 ArkTS 重新实现 URI 解析规则,只负责把返回字段映射到结果卡片。

5.2 输入和模式切换

页面提供输入框和三个模式按钮:

TextInput({ text: this.input, placeholder: '输入 URI 或普通文本' })
  .onChange((value: string) => this.input = value)

Button('解析 URI').onClick(() => this.mode = 'parse')
Button('编码').onClick(() => this.mode = 'encode')
Button('解码').onClick(() => this.mode = 'decode')

主按钮根据当前模式执行 parseUri、encodeUri 或 decodeUri。输入为空时,页面直接显示提示,不调用 Native。

5.3 结果卡片和历史记录

解析模式显示 scheme、authority、host/port、path、query、fragment 和 scheme-specific part;编码和解码模式显示转换结果。

每次成功操作都会追加一条最近记录:

const record: OperationRecord = {
  id: this.nextRecordId,
  mode: this.modeTitle(),
  input: input,
  output: output,
};
this.history = [record, ...this.history].slice(0, 5);

点击历史项会把输入和模式重新填入页面,再次走完整 Native 链路。

5.4 页面效果图

页面标题、输入区、三种操作模式、快速示例、状态提示和解析结果都集中在一条可滚动工作流中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第 6 阶段:构建与验证

6.1 example 工程结构
example/
├── shared/
│   ├── src/commonMain/.../UriExamples.kt
│   └── src/commonTest/.../UriExamplesTest.kt
├── nativeApp/
│   ├── src/ohosArm64Main/.../NativeBridge.kt
│   └── src/ohosArm64Main/linker/shared-library.map
└── ohosApp/
    ├── AppScope/
    ├── entry/src/main/cpp/
    │   ├── CMakeLists.txt
    │   └── napi_init.cpp
    └── entry/src/main/ets/
        ├── pages/Index.ets
        └── uri/UriClient.ets
6.2 Native 动态库准备
export JAVA_HOME="/path/to/jdk-21"
cd example
./gradlew :shared:jvmTest :nativeApp:prepareOhos

成功后生成:

example/ohosApp/entry/libs/arm64-v8a/liburi.so
example/ohosApp/entry/src/main/cpp/include/liburi_api.h
6.3 构建无签名 HAP
cd example/ohosApp
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
  --mode module \
  -p module=entry@default \
  -p product=default \
  -p buildMode=debug \
  clean assembleHap --no-daemon

输出位于:

example/ohosApp/entry/build/default/outputs/default/entry-default-unsigned.hap

无签名 HAP 只用于构建和打包检查,真机安装需要签名 HAP。

6.4 Native 依赖检查
python3 scripts/check-native-deps.py \
  /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
  example/ohosApp/entry/libs/arm64-v8a

检查结果:

liburi.so: 0 unresolved strong imports

## 四、完整代码对照

### 4.1 整体架构

```text
ArkTS input
    │ parseUri / encodeUri / decodeUri
    ▼
libentry.so
    │ N-API
    ▼
UriParse / UriEncode / UriDecode
    │ C ABI
    ▼
liburi.so
    │ Kotlin/Native
    ▼
Uri.parse / Uri.encode / Uri.decode
    │ JSON
    ▼
ArkUI result card

4.2 文件清单

文件职责
uri/src/commonMain/.../Uri.ktURI 公共接口和解析入口
uri/src/commonMain/.../UriCodec.kt编码和解码实现
example/shared/.../UriExamples.kt固定示例、转换和公共检查
example/nativeApp/.../NativeBridge.ktC ABI、JSON 返回和内存释放
example/ohosApp/.../UriClient.etsN-API JSON 解析和错误处理
example/ohosApp/.../Index.etsURI 交互工作台页面
example/ohosApp/.../napi_init.cppN-API 导出和字符串参数读取
scripts/build-openharmony.sh测试、Native 链接和产物复制
scripts/build-hap.sh已配置签名工程的 HAP 构建
docs/openharmony/VALIDATION.md自动检查和验收记录

4.3 关键 API 对照

层次API作用
KotlinUri.parse解析 URI 并暴露结构字段
KotlinUri.encode编码普通文本
KotlinUri.decode解码百分号文本
NativeUriParse返回解析字段 JSON
NativeUriEncode返回编码结果 JSON
NativeUriDecode返回解码结果 JSON
N-APIparseUri向 ArkTS 暴露解析方法
N-APIencodeUri向 ArkTS 暴露编码方法
N-APIdecodeUri向 ArkTS 暴露解码方法

4.4 ArkTS 与 Kotlin 的边界

ArkTS 只负责输入和页面生命周期:

const details: UriDetails = parseUri(this.input.trim());
this.details = details;

Kotlin 负责 URI 语义:

val uri = Uri.parse(input)
val encoded = Uri.encode(input)
val decoded = Uri.decode(input, convertPlus = true, throwOnFailure = true)

两者之间只传输 UTF-8 字符串和 JSON,不传输 Kotlin 对象、ArkTS class 实例或未校验的 Native 指针。

五、关键决策说明

决策 1:把 ohosArm64 加入公共构建约定

只有真正链接 ohosArm64 动态库,才能证明 URI 公共实现可以进入 OpenHarmony 运行时。单独写一个 ArkTS 解析器只能验证页面,不能验证三方库适配。

决策 2:独立消费者必须通过构建产物消费

example 单独解析共享库,ohosApp 单独接收 .so 和头文件,避免根工程编译通过却无法在 DevEco 中打包。

决策 3:JSON 作为跨语言数据契约

JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加解析字段而不暴露 Kotlin 对象的内部布局。

决策 4:桥接层只开放必要 C ABI 入口

目录、单个样例、公共检查、解析、编码、解码和释放已经覆盖示例所需能力;减少 ABI 符号可以降低 Native 生命周期和兼容风险。

决策 5:交互操作和公共检查分开

公共检查用于自动验收,交互页面用于体验实际 URI 功能。用户输入会走真实的 Kotlin/Native 和 N-API 链路,不把固定检查结果当作页面主体。

决策 6:把库验证、HAP 验证和签名交接分开

JVM 测试验证 URI 规则,Native 链接验证 ABI,Hvigor 验证 HAP,签名工程负责证书和 profile。每一层都有明确失败边界。

六、测试与验证

6.1 测试环境

本次构建验证使用:

  • macOS ARM64;
  • JDK 21;
  • Kotlin Multiplatform 2.2.21-1.0.0;
  • DevEco Studio 及 OpenHarmony ARM64 Native SDK;
  • OpenHarmony API 20 Native SDK;
  • arm64-v8a 原生 ABI。

本次仓库验收以 JVM、Native、C++ 和无签名 HAP 为主。由于仓库不保存证书、profile 或设备密钥,未在仓库工程中执行签名和真机安装。

6.2 静态检查与单元测试

./gradlew :uri:jvmTest
(cd example && ./gradlew :shared:jvmTest)

测试覆盖 URI 往返解析、编码和解码、Builder 输出、hierarchical/opaque 分类、固定示例和非法索引错误。

6.3 Kotlin/Native 和 C ABI 验证

(cd example && ./gradlew :nativeApp:prepareOhos)

该任务成功完成 compileKotlinOhosArm64、linkDebugSharedOhosArm64,并生成 liburi.so 和 liburi_api.h。导出的 UriParse、UriEncode、UriDecode 和 UriFree 符号已加入 linker map。

6.4 ELF 强依赖验证

python3 scripts/check-native-deps.py \
  /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
  example/ohosApp/entry/libs/arm64-v8a

结果:

liburi.so: 0 unresolved strong imports

6.5 C++、ArkTS 和 HAP 验证

Hvigor 无签名构建已完成以下步骤:

BuildNativeWithCmake       SUCCESS
BuildNativeWithNinja       SUCCESS
CompileArkTS               SUCCESS
PackageHap                 SUCCESS
entry-default-unsigned.hap generated

6.6 功能验证用例

用例 1:输入并解析 Web URI

输入 https://example.com/path?q=hello world#section,点击“解析 URI”,页面显示 scheme 为 https、host 为 example.com、path 为 /path、query 和 fragment 等字段。

用例 2:解析 Content URI

点击“Content URI”快速示例,确认页面能够展示 content scheme 和层级路径。

用例 3:解析 opaque URI

点击“Mailto”快速示例,确认页面显示 OPAQUE,并把 scheme-specific part 作为主要结构字段。

用例 4:编码和解码

输入包含空格或中文的普通文本,点击“编码”;复制结果切换到“解码”,确认结果回到原始文本。

用例 5:历史记录回放

连续执行多个操作,点击“最近操作”中的条目,确认输入和操作模式恢复后重新走 Native 链路。

用例 6:错误处理

清空输入后执行,页面显示“请输入 URI 或文本”;输入非法百分号序列进行解码,页面显示 Native 返回的错误文本。

6.7 验证结论

自动测试、Kotlin/Native ARM64 链接、ELF 依赖检查、C++ 编译、ArkTS 编译和无签名 HAP 打包均已完成。由于签名材料不属于仓库交付物,签名 HAP 和真机安装需要在开发者自己的签名工程中继续完成。

七、运行效果

7.1 OpenHarmony 交互页面效果图

在这里插入图片描述

效果图中可以看到:

  • 页面标题为“URI KMP 三方库”;
  • 副标题为“OpenHarmony 交互示例”;
  • 输入框预填充 Web URI;
  • 当前模式为“解析 URI”;
  • 页面提供“编码”和“解码”入口;
  • 页面提供 Web 链接、Content URI 和 Mailto 快速示例;
  • 解析结果卡片展示 URI 的层级类型和字段值。

7.2 命令速查

# 根工程测试
export JAVA_HOME="/path/to/jdk-21"
./gradlew :uri:jvmTest

# 示例 JVM 测试和 Native 准备
(cd example && ./gradlew :shared:jvmTest :nativeApp:prepareOhos)

# Native 依赖检查
python3 scripts/check-native-deps.py \
  /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
  example/ohosApp/entry/libs/arm64-v8a

# 构建无签名 HAP
cd example/ohosApp
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
  --mode module -p module=entry@default -p product=default \
  -p buildMode=debug clean assembleHap --no-daemon

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配:必须把公共 URI 实现真正编译成 ohosArm64 动态库。
  2. N-API 不负责 URI 业务判断:C++ 只做类型检查、字符串转换和释放,URI 语义放在 Kotlin。
  3. Native 返回值必须成对管理:Native 分配 JSON 后,N-API 创建 JS 字符串后要立即调用 UriFree。
  4. 编码和解码输入要区分:解码模式应输入百分号编码文本,编码模式应输入普通文本。
  5. 无签名 HAP 不能代表设备验收完成:构建成功只说明工程和产物完整,真机安装还需要签名材料。

8.2 已知问题

  • 当前示例只打包 arm64-v8a,不提供其他 ABI;
  • libentry.so 的 Native 模块校验提示取决于 DevEco SDK 版本;
  • JS/Wasm 的 npm lock 由 Kotlin Gradle 插件管理,升级依赖时需要按插件提示更新锁文件;
  • 当前示例是单页面工作台,多页面应用需要在业务层集中管理 URI 操作历史;
  • 未配置证书的 HAP 不能直接通过 hdc 安装到真实设备。

8.3 未来优化方向

  • 增加 expect/actual 封装,让 Android、桌面端和 OpenHarmony 共用更高层的 URI 操作接口;
  • 将最近操作抽成共享 Kotlin 状态模型,减少页面状态代码;
  • 为 Compose Multiplatform 页面提供相同的 URI 结果卡片;
  • 增加查询参数的可视化编辑和 Builder 反向构建示例;
  • 在持续集成中加入 ohosArm64 链接和无签名 HAP 构建任务;
  • 在独立签名环境中补充真机安装和截图验收记录。

九、总结

9.1 核心难点回顾

本次适配真正需要处理的不是把一个页面放入 DevEco,而是一条完整跨端链路:

ArkUI 输入和交互
    → C++ N-API
    → Kotlin/Native C ABI
    → KMP URI 公共实现
    → JSON
    → ArkUI 结构化结果

9.2 封装层次

  • KMP 层:定义稳定的 URI 解析、编码、解码和 Builder API;
  • Native 层:生成 ARM64 动态库,并通过 C ABI 输出有限入口;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理输入、操作模式、错误状态、结果展示和历史记录;
  • DevEco 层:完成 CMake、ArkTS、HAP、签名和设备交付。

9.3 三条经验

  1. 先让公共 URI 模型在 JVM 和 Native 通过,再接入 ArkUI 页面;
  2. 用 JSON 和少量 C ABI 代替跨语言对象传递;
  3. 把自动测试、Native 链接、HAP 构建和签名安装分别记录,避免把“编译成功”误认为“设备交付完成”。

9.4 适配成果

当前 uri-kmp 已完成:

  • ohosArm64 Kotlin/Native 目标;
  • URI 解析、编码、解码和结构字段 JSON 桥接;
  • Kotlin/Native + C ABI + C++ N-API 三层调用链;
  • URI KMP 三方库 ArkUI 交互示例页面;
  • Web、Content URI、Mailto 三类快速示例;
  • Native ELF 依赖检查和无签名 HAP 构建;
  • AtomGit 项目文档、中英文适配说明和本地效果图。

适配过程 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐