开源鸿蒙平台 KMP/CMP 三方库「系统分享」适配全流程
本文记录
kmp-share-kit接入 OpenHarmony 系统分享能力的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、文件 URI 授权、签名 HAP 和真机验收。本次适配复用 Kotlin 侧的分享请求、内容类型、不可变状态归约、JSON 和验收逻辑,再由 ArkTS 调用 HarmonyOS
@kit.ShareKit的systemShare.SharedData与systemShare.ShareController。这样验证的是一份稳定的 KMP 分享契约如何落到 OpenHarmony 系统面板,而不是在页面中临时拼装一套只对单个示例有效的数据。
项目地址: AtomGit/oh-tpc/kmp-share-kit
开发工具: 华为云码道
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
分享是移动应用中非常常见的平台能力。业务只需要描述文本、链接或文件,系统分享面板负责列出可接收内容的应用,并提供复制、保存、打印、超级中转站等系统操作。应用不需要自己维护目标应用列表,也不应该模拟一个“看起来像分享面板”的普通弹窗。
如果只在 ArkTS 页面中写死几段文本并打开面板,虽然能够展示效果,却无法证明 KMP/CMP 工程的公共数据模型、Native 产物和 OpenHarmony 平台边界已经打通。适配需要解决下面几个问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | KMP 模块默认只有 JVM,必须增加 ohosArm64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | Kotlin/Native、OpenHarmony LLVM、Native SDK、Gradle 插件和 JDK 必须使用匹配版本。 |
| 系统类型隔离 | UIAbilityContext、SharedRecord 和 ShareController 都属于平台 API,不能泄漏到 commonMain。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin data class,可选 Native 链路必须经过 C ABI、C++ N-API 和 JSON。 |
| 内容模型不同 | 文本使用 content,文件使用 uri,内容类型还要正确映射到统一数据类型 utd。 |
| URI 权限边界 | 文件选择器返回的 file://docs/... URI 可供当前应用读取,却不一定允许再次授权给分享扩展。 |
| 面板生命周期 | 打开、关闭、完成和异常需要与应用状态对应,取消选择不能误报为分享失败。 |
| 交付链路复杂 | Native 动态库、CMake、HAP、签名、安装和真机面板需要分层验证。 |
因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、可选 C ABI/N-API 验证层和 ArkTS Share Kit 平台层。请求模型与约束仍由 KMP 维护,ArkTS 只负责系统文件选择器、可共享 URI、面板参数和页面生命周期。
1.2 库提供的能力
share-kit 公共模块提供以下能力:
ShareContentType:文本、链接、图片、视频、音频和普通文件六类内容;ShareAttachment:描述 URI、类型、标题和说明的不可变附件;ShareRequest:统一承载标题、正文、链接、附件、预览模式和选择模式;ShareResult:记录空闲、展示中、完成、关闭和失败五种生命周期状态;ShareEngine.reduce:以不可变方式推进分享状态;ShareEngine.runChecks:在 JVM 与 Kotlin/Native 中复用八项公共检查,ArkTS 示例展示同一组验收项;ShareRequest.toJson:生成稳定的跨语言 JSON;ShareKit:为业务提供catalog、request和initialResult简单门面。
内容类型约定如下:
| KMP 类型 | OpenHarmony UTD | 数据字段 | 含义 |
|---|---|---|---|
ShareContentType.TEXT | general.text | content | 普通文本 |
ShareContentType.HYPERLINK | general.hyperlink | content | HTTP/HTTPS 链接 |
ShareContentType.IMAGE | image | uri | 图片附件 |
ShareContentType.VIDEO | video | uri | 视频附件 |
ShareContentType.AUDIO | audio | uri | 音频附件 |
ShareContentType.FILE | general.file | uri | 普通文件 |
ShareRequest 会在构造阶段校验请求 ID、URL 协议、正文大小、附件数量和 URI 长度,让明显错误在进入系统面板前失败。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 请求、附件、类型映射、状态归约、JSON 和自检由 Kotlin 共享。 |
| 平台目标 | 为公共模块和示例加入 ohosArm64,生成 libshare_kit.so。 |
| 桥接稳定 | 使用少量 C ABI 函数和 JSON,避免把 Kotlin 对象地址交给 ArkTS。 |
| UI 完整 | 页面支持文本、链接、文件选择、内容刷新和真实系统分享面板。 |
| 文件可达 | 选择器文件先复制到应用缓存,再把应用自有 URI 交给 Share Kit。 |
| 可测试 | JVM 测试、Native 链接、HAP 构建、设备安装和真实分享面板分别验收。 |
| 签名安全 | 证书、profile、p12 和口令仅在开发者本机配置。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面是独立的真机验收宿主,公共 API 仍然保持平台无关。其他 KMP/CMP 应用可以复用
ShareRequest和ShareEngine,再为 Android、iOS、桌面或 OpenHarmony 编写各自的平台适配器。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 KMP 模块、分享模型和 OpenHarmony 示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:请求与序列化 ── 建立 ShareRequest、Attachment、Reducer 和 JSON 契约
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、内存释放和类型检查
第 5 阶段:系统能力封装 ── ArkTS 文件选择、URI 缓存、SharedData 和 ShareController
第 6 阶段:示例与验证 ── HAP 构建、签名、设备安装、分享面板和效果图
每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享模型,再把相同代码链接为 ARM64 动态库,随后由 CMake 和 N-API 验证跨语言边界,最后安装签名 HAP,分别打开文本、链接和文件分享面板。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目采用与参考 CMP 工程一致的分层:
share-kit/ KMP 请求模型、归约、自检和 JSON 边界
vico/ 与参考工程一致的库聚合层
sample/ android/desktop/shared/web/ios 主机入口
example/shared/ 示例门面和 JVM 验收测试
example/nativeApp/ ohosArm64 Kotlin/Native 动态库
example/ohosApp/ DevEco Stage 工程和 ArkUI 页面
scripts/ Native、HAP 和签名工程辅助脚本
docs/openharmony/ 验收记录和真机效果图
guide/ 集成指南
example 是独立 Gradle 工程,不把 DevEco 工程当作 Kotlin 子模块。这样可以分别执行 Gradle 和 Hvigor,也可以通过辅助脚本创建单独的签名工程副本,避免签名材料进入源码交付范围。
1.2 固定工具链和版本矩阵
本项目使用以下版本约定:
| 项目 | 配置 | 用途 |
|---|---|---|
| Kotlin Multiplatform | 2.2.21-1.0.0 | JVM、Kotlin/Native 和 KLIB |
| JDK | 21 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 | ARM64 真机动态库 |
| DevEco product | 6.0.0(20) | 工程兼容和目标版本 |
| ABI | arm64-v8a | HAP 原生库架构 |
| 系统接口 | @kit.ShareKit | 系统分享面板 |
| 文件接口 | @kit.CoreFileKit | 文件选择、复制和 URI 转换 |
执行 Gradle 脚本前先选择 JDK 21:
export JAVA_HOME="/path/to/jdk-21"
java -version
API 版本满足要求只说明工程可以编译。系统分享目标数量、可用操作和预览样式仍由设备版本、已安装应用及内容类型共同决定。
1.3 创建 OpenHarmony 示例目录
示例页面围绕“请求类型、内容预览、文件选择、分享动作和自检状态”组织:
标题区 系统分享 / 同一份 KMP 内容,交给鸿蒙系统分享面板
类型区 文本 / 链接 / 文件
内容区 标题、内容类型、正文、链接或已选文件
操作区 选择文件 / 打开分享面板 / 刷新内容
状态区 第 N 次刷新、预览模式、8/8 自检结果
文本和链接可以直接打开系统面板。文件请求若还没有附件,会先打开 DocumentViewPicker;用户选择文件并完成缓存复制后,页面再调用系统分享面板。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
公共模块同时保留 JVM 测试和 OpenHarmony Native 目标:
plugins {
kotlin("multiplatform")
`maven-publish`
}
kotlin {
explicitApi()
jvm()
jvmToolchain(21)
ohosArm64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
JVM 目标让请求约束、生命周期和 JSON 可以快速测试;ohosArm64 则把同一份 commonMain 代码编译成 OpenHarmony KLIB。
2.2 Native focused build 的作用
example/nativeApp 构建一个受 linker map 约束的 shared library:
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "share_kit"
linkerOpts(
"--entry=0",
"--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
)
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
链接器只导出四个 C ABI 符号:
ShareKitCatalog
ShareKitGet
ShareKitRunChecks
ShareKitFree
这样外部调用方只能通过明确的边界获取请求和自检结果,Kotlin/Native 内部实现不会变成不受控的 ABI。
2.3 配置仓库和独立消费工程
example/settings.gradle.kts 使用 OpenHarmony 社区 Maven、Maven Central 和 Gradle Plugin Portal:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
}
}
共享示例通过项目依赖消费本地 share-kit:
sourceSets {
commonMain.dependencies {
api(project(":share-kit"))
}
}
业务 KMP 模块接入发布产物时,可以把依赖放在 commonMain:
commonMain.dependencies {
implementation("com.ohos.sharekit:share-kit:1.0.0")
}
源码、问题反馈和后续版本统一从 AtomGit 项目入口获取:
git clone https://atomgit.com/oh-tpc/kmp-share-kit.git
cd kmp-share-kit
2.4 通过构建产物消费共享库
prepareOhos 在 Native 链接成功后复制动态库和 C 头文件:
val prepareOhos by tasks.registering(Copy::class) {
dependsOn("linkDebugSharedOhosArm64")
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libshare_kit.so")
into("libs/arm64-v8a")
}
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libshare_kit_api.h")
into("src/main/cpp/include")
}
into(rootProject.layout.projectDirectory.dir("ohosApp/entry"))
}
成功后,DevEco 工程可以得到:
example/ohosApp/entry/libs/arm64-v8a/libshare_kit.so
example/ohosApp/entry/src/main/cpp/include/libshare_kit_api.h
.so、生成头文件和 HAP 都可以从源码重新生成。签名材料则始终留在开发者本机,不作为库的一部分分发。
第 3 阶段:请求与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此 Native 验证边界使用 UTF-8 JSON:
KMP ShareRequest / ShareResult
↓ toJson()
Kotlin/Native C ABI
↓ const char*
C++ N-API
↓ JavaScript string
ArkTS 可选 Native 消费方
当前 Stage 真机页面把 Share Kit 调用集中在 ShareClient.ets,并按同一套公共字段维护平台映射;页面没有伪装成直接通过 libentry.so 读取目录。Native/N-API 链路已经构建并打包,用于验证 KMP 代码能进入 OpenHarmony ARM64 产物,也是后续切换为 Native 数据源时的稳定接口。
3.2 ShareRequest 和 ShareAttachment
公共请求模型位于 share-kit/src/commonMain:
public data class ShareAttachment(
val uri: String,
val type: ShareContentType = ShareContentType.FILE,
val title: String = "",
val description: String = "",
) {
init {
require(uri.isNotBlank()) { "Attachment URI must not be blank" }
require(uri.length <= 4_096) { "Attachment URI is too long" }
}
}
public data class ShareRequest(
val id: String,
val title: String = "",
val text: String = "",
val url: String? = null,
val subject: String? = null,
val attachments: List<ShareAttachment> = emptyList(),
val contentType: ShareContentType? = null,
val previewMode: SharePreviewMode = SharePreviewMode.DEFAULT,
val selectionMode: ShareSelectionMode = ShareSelectionMode.SINGLE,
val excludedAbilities: Set<ShareAbility> = emptySet(),
)
primaryType 会优先使用显式内容类型,其次读取第一个附件,再根据 URL 或正文推断链接与文本。运行时文件通过 withAttachments 返回新请求,不修改原始模板。
3.3 Engine reducer 和 JSON
ShareEngine 内置文本、链接和文件三个确定性模板,并通过 reducer 描述生命周期:
public fun reduce(result: ShareResult, actionId: String): ShareResult {
if (actionId.isEmpty()) return result
require(actionId in setOf("present", "complete", "dismiss", "retry"))
return when (actionId) {
"present" -> result.copy(
status = ShareStatus.PRESENTING,
message = "正在打开系统分享面板",
)
"complete" -> result.copy(
status = ShareStatus.COMPLETED,
message = "分享已完成",
)
"dismiss" -> result.copy(
status = ShareStatus.DISMISSED,
message = "用户关闭了分享面板",
)
else -> result.copy(
status = ShareStatus.IDLE,
message = "可以再次发起分享",
)
}
}
JSON 边界保持字段稳定:
{
"id": "article-link",
"title": "OpenHarmony 分享能力",
"text": "KMP/CMP 可以把同一份内容交给系统分享面板。",
"url": "https://www.openharmony.cn/",
"subject": "推荐阅读",
"previewMode": "DEFAULT",
"selectionMode": "SINGLE",
"primaryType": "HYPERLINK",
"excludedAbilities": [],
"attachments": []
}
所有字符串经过 jsonQuote 处理,双引号、反斜杠、换行和控制字符都不会破坏跨语言数据。
3.4 自检和错误边界
ShareEngine.runChecks() 覆盖八项检查:
- 三个分享模板都存在;
- 请求 ID 全部唯一;
- 文本分享可用;
- 链接分享可用;
- 文件请求模板可用;
- 生命周期以不可变方式归约;
- 单条记录使用安全的单选模式;
- 正文没有超过 IPC 上限。
Native 异常会转换为 {"error":"..."},C++ 在创建 ArkTS 字符串后立即调用 ShareKitFree。Kotlin 构造器还会拒绝空内容、非法请求 ID、非 HTTP 链接、超长 URI、超过 500 个附件和超过 200 KB 的正文。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
Kotlin/Native 的 ShareRequest 和 ShareResult 属于 Kotlin 运行时对象。ArkTS 只能接收 JavaScript 值,C++ 不能把 Kotlin 对象地址直接当成 JavaScript 对象。
项目保留三层桥接:
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ ShareEngine / ShareKitExamples
▼
UTF-8 JSON + explicit free
这个链路与系统分享调用是两个职责:Native 链路证明共享模型可以进入 OpenHarmony;ShareClient.ets 负责把已经确定的请求契约映射到 Share Kit 系统对象。
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出字符串正文 | 实现简单 | 附件、模式和类型信息丢失 | ❌ |
| C ABI + JSON | 边界清晰、易扩展、易调试 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写完整业务模型 | 页面调用简单 | KMP 和 ArkTS 逻辑容易分叉 | ❌ |
Stage 示例当前为降低真机验收变量,在平台边界维护与公共 JSON 对齐的静态目录;这不是第二套业务模型。请求约束、字段语义和 Native 导出仍由 KMP 定义,后续可把目录数据源替换为 getCatalog(),而 Share Kit 映射无需改变。
4.3 Kotlin/Native 导出函数
@CName("ShareKitCatalog")
public fun catalogNative(): CPointer<ByteVar> = response {
ShareKitExamples.catalog()
.joinToString(prefix = "[", postfix = "]") { it.toJson() }
}
@CName("ShareKitGet")
public fun requestNative(index: Int, revision: Int): CPointer<ByteVar> = response {
ShareKitExamples.request(index, revision).toJson()
}
@CName("ShareKitRunChecks")
public fun checksNative(): CPointer<ByteVar> = response {
val checks = ShareKitExamples.runChecks()
"{\"passed\":true,\"checks\":" +
checks.joinToString(prefix = "[", postfix = "]") { it.jsonQuote() } + "}"
}
@CName("ShareKitFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer)
}
返回值使用 Native heap 分配的、以 0 结尾的 C 字符串。每一块返回缓冲区都由 ShareKitFree 释放。
4.4 C++ N-API 方法分发
C++ 注册三个 ArkTS 方法:
napi_property_descriptor methods[] = {
{"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"getShare", nullptr, Share, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
getShare 会校验参数数量、有限数字、整数范围和非负条件,再调用 ShareKitGet。目录、单个请求和自检都遵循“调用 Native、创建 ArkTS 字符串、释放 Native 缓冲区”的顺序。
CMake 将 Kotlin/Native 动态库作为 imported library:
add_library(share_kit SHARED IMPORTED)
set_target_properties(share_kit PROPERTIES
IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libshare_kit.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE share_kit libace_napi.z.so)
4.5 N-API 生命周期
ArkTS getShare(index, revision)
│
▼
ReadNumber + argument check
│
▼
ShareKitGet(index, revision)
│
▼
napi_create_string_utf8(...)
│
▼
ShareKitFree(nativeBuffer)
│
▼
return JS string
C++ 负责完成跨语言字符串转换和 Native 释放,ArkTS 消费方不需要知道 Kotlin/Native 的堆实现。
第 5 阶段:系统能力封装
5.1 ArkTS 调用 Share Kit
系统分享封装在 ShareClient.ets。它先把公共请求转换为 SharedRecord:
function recordFor(item: ShareRequest): systemShare.SharedRecord {
if (item.attachments.length > 0) {
const attachment = item.attachments[0];
return {
utd: attachment.type === 'IMAGE' ? 'image' : 'general.file',
uri: attachment.uri,
title: attachment.title.length > 0 ? attachment.title : item.title,
description: attachment.description,
label: attachment.type,
};
}
if (item.url !== null) {
return {
utd: 'general.hyperlink',
content: item.url,
title: item.title,
description: item.text,
label: 'HYPERLINK',
};
}
return {
utd: 'general.text',
content: item.text,
title: item.title,
description: item.subject ?? '',
label: 'TEXT',
};
}
然后创建共享数据和控制器:
const data = new systemShare.SharedData(recordFor(item));
const controller = new systemShare.ShareController(data);
await controller.show(context, {
selectionMode: item.selectionMode === 'BATCH'
? systemShare.SelectionMode.BATCH
: systemShare.SelectionMode.SINGLE,
previewMode: item.previewMode === 'DETAIL'
? systemShare.SharePreviewMode.DETAIL
: systemShare.SharePreviewMode.DEFAULT,
});
链接请求还会附加一条文本记录,使接收方既能得到 URL,也能读取业务说明。
5.2 文件选择与 URI 授权
最初实现把 DocumentViewPicker.select() 返回的 URI 直接交给 SharedRecord.uri。选择文件后分享面板没有稳定显示,HiLog 中出现:
PROXY_AUTHORIZATION_URI: PERMISSION_DENIED
Both content and uri are empty
1003703001
问题不在按钮或 ShareController.show(),而在 URI 所有权。file://docs/... 表示当前应用被允许读取文档选择器中的文件,不代表当前应用还能把这份临时权限代理给系统分享扩展。
修复后的流程是:
DocumentViewPicker.select()
↓ file://docs/... 临时读取 URI
fileIo.open(..., READ_ONLY)
↓
fileIo.copyFile(source.fd, context.cacheDir/...)
↓ 应用自有缓存文件
fileUri.getUriFromPath(destination)
↓ 可交给系统面板的应用 URI
SharedRecord { utd: "general.file", uri: shareUri }
↓
ShareController.show()
关键实现如下:
async function copyToShareCache(
context: common.UIAbilityContext,
sourceUri: string,
displayName: string,
): Promise<string> {
const source = await fileIo.open(sourceUri, fileIo.OpenMode.READ_ONLY);
try {
const destination = `${context.cacheDir}/${cacheFileName(displayName)}`;
await fileIo.copyFile(source.fd, destination);
return fileUri.getUriFromPath(destination);
} finally {
fileIo.closeSync(source);
}
}
这种实现不需要声明系统级 ohos.permission.PROXY_AUTHORIZATION_URI。应用只使用文件选择器授予的读取能力,把内容复制为自己的缓存文件,再把应用自有 URI 交给系统分享面板。
5.3 ArkUI 页面状态
Index.ets 保存当前请求、请求索引、刷新版本、操作状态和错误文本:
@State private request: ShareRequest = emptyRequest();
@State private requestIndex: number = 0;
@State private revision: number = 0;
@State private status: string = '就绪';
@State private errorText: string = '';
文件模式下点击“打开分享面板”时,如果还没有附件,页面会先执行 chooseFile();只有用户选择并成功复制文件,才继续执行 showSystemShare()。取消文件选择会停留在页面,不会打开一个空内容面板。
5.4 页面交互预设
页面提供三种请求预设:
- 文本:分享“每日摘录”和文本主题;
- 链接:分享 OpenHarmony 链接与推荐说明;
- 文件:打开系统文件选择器,缓存所选文件后分享;
- 刷新内容:增加 revision,验证 UI 使用的是新不可变请求;
- 打开分享面板:创建真实
SharedData和ShareController。
文本与链接使用默认预览、单目标选择。图片附件可以切换为详细预览;普通文件保留默认预览,避免把不适合渲染的内容强制当作图片展示。
第 6 阶段:示例与验证
6.1 example 工程结构
example/
├── shared/
│ ├── src/commonMain/.../ShareKitExamples.kt
│ └── src/commonTest/.../ShareKitExamplesTest.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
└── share/ShareClient.ets
shared 验证公共请求,nativeApp 产生 Native 动态库,ohosApp 负责 ArkUI 页面、文件选择器和系统 Share Kit。三者边界清晰,任何一层失败都能单独定位。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块,类型声明位于:
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
声明向 ArkTS 暴露:
export const getCatalog: () => string;
export const getShare: (index: number, revision?: number) => string;
export const runChecks: () => string;
这些 API 是已编译、已打包的 Native 验证入口。当前真机页面直接使用平台映射层;若业务需要从 Native 动态读取目录,可以在 ShareClient.ets 中解析这些 JSON,而不改变 showSystemShare。
6.3 Native 动态库准备
执行:
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
脚本依次执行根模块测试、示例测试、linkDebugSharedOhosArm64 和 prepareOhos。成功后生成:
example/ohosApp/entry/libs/arm64-v8a/libshare_kit.so
example/ohosApp/entry/src/main/cpp/include/libshare_kit_api.h
还可以检查 ARM64 ELF 的强依赖:
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
本次检查结果是 libshare_kit.so: 0 unresolved strong imports。
6.4 构建、签名和安装
在 DevEco Studio 打开 example/ohosApp,等待工程同步后配置本机 HarmonyOS 签名。也可以在签名工程中执行:
./scripts/build-hap.sh /absolute/path/to/signing-project
产物位于:
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
安装并启动:
hdc list targets -v
hdc -t <设备序列号> install -r \
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b com.ohos.sharekit.sample
签名证书、profile、p12 和密码只保存在本机,不要提交到 AtomGit。
四、完整代码对照
4.1 整体架构
真机分享主链路:
ArkUI Index.ets
│ ShareRequest 公共字段
▼
ShareClient.ets
│ 文本/链接/文件映射
▼
systemShare.SharedRecord
│
▼
systemShare.SharedData
│
▼
systemShare.ShareController.show(UIAbilityContext)
│
▼
HarmonyOS 系统分享面板
KMP/Native 验证链路:
ShareEngine -> ShareRequest -> JSON
│ Kotlin/Native
▼
libshare_kit.so
│ C ABI
▼
libentry.so
│ N-API
▼
getCatalog / getShare / runChecks
4.2 文件清单
| 文件 | 职责 |
|---|---|
share-kit/.../Share.kt | 内容类型、附件、请求、结果和业务门面 |
share-kit/.../ShareEngine.kt | 请求目录、生命周期归约和八项自检 |
share-kit/.../ShareJson.kt | JSON 编码和字符串转义 |
example/nativeApp/.../NativeBridge.kt | C ABI、JSON 返回和内存释放 |
example/ohosApp/.../ShareClient.ets | 文件选择、缓存复制和 Share Kit 映射 |
example/ohosApp/.../Index.ets | 真机交互页面 |
example/ohosApp/entry/src/main/cpp/napi_init.cpp | N-API 导出和参数检查 |
scripts/build-openharmony.sh | 测试、Native 链接和产物复制 |
scripts/check-native-deps.py | ARM64 动态库强依赖检查 |
scripts/build-hap.sh | 已配置签名工程的 HAP 构建 |
docs/openharmony/VALIDATION.md | 自动检查和真机验收入口 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | ShareRequest.withAttachments | 绑定运行时文件并返回新请求 |
| Kotlin | ShareEngine.reduce | 推进分享生命周期 |
| Kotlin | ShareRequest.toJson | 生成跨语言 JSON |
| Native | ShareKitGet | 按索引和版本返回 JSON 请求 |
| N-API | getShare | 向 ArkTS 暴露请求方法 |
| ArkTS | DocumentViewPicker.select | 打开系统文件选择器 |
| ArkTS | fileIo.copyFile | 把临时可读文件复制到应用缓存 |
| ArkTS | fileUri.getUriFromPath | 生成应用自有文件 URI |
| ArkTS | ShareController.show | 打开真实系统分享面板 |
4.4 ArkTS 与 Kotlin 的边界
Kotlin 负责稳定、平台无关的数据语义:
val request = ShareKit.request(index = 2)
val ready = request.withAttachments(
listOf(
ShareAttachment(
uri = "file://app-owned-cache/report.pdf",
type = ShareContentType.FILE,
title = "report.pdf",
),
),
)
ArkTS 负责必须依赖 Stage 上下文的平台动作:
const attachment = await selectDocument(this.hostContext());
if (attachment !== null) {
this.request = withAttachment(this.request, attachment);
await showSystemShare(this.hostContext(), this.request);
}
两者之间共享请求字段和 JSON 契约,不传输 UIAbilityContext、SharedRecord、ArkTS class 实例或 Kotlin 对象地址。
五、关键决策说明
决策 1:把 ohosArm64 加入公共构建约定
分享面板运行在 ARM64 设备上。只有真正链接 ohosArm64 动态库,才能证明共享 Kotlin 请求模型可以进入 OpenHarmony 产物,而不仅是 JVM 上可用。
决策 2:独立消费者必须通过构建产物消费
example 单独解析 share-kit,ohosApp 单独接收 .so 和头文件,避免根工程编译通过却无法在 DevEco 中打包。
决策 3:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续新增字段,而不暴露 Kotlin 对象布局或依赖编译器内部 ABI。
决策 4:桥接层只开放四个 C ABI 入口
目录、单条请求、自检和释放已经覆盖示例需要的 Native 能力。减少 ABI 符号可以降低参数、内存和版本兼容风险。
决策 5:文件先进入应用缓存再分享
选择器 URI 的读取权限不等于可代理权限。复制到 context.cacheDir 后,文件归属和生命周期都由应用控制,Share Kit 能稳定读取内容,也不需要申请高权限。
决策 6:把库验证和设备验证分开
JVM 测试验证请求规则,Native 链接验证 ABI,Hvigor 验证 HAP,真机验证文件选择器、URI 可达性和系统分享面板。每一层都有明确的失败边界。
六、测试与验证
6.1 测试环境
本次真机验证使用:
- macOS;
- JDK 21;
- Kotlin Multiplatform
2.2.21-1.0.0; - DevEco Studio 及 OpenHarmony ARM64 Native SDK;
- OpenHarmony/HarmonyOS API 20 工程;
- 已签名
entry-default-signed.hap; - USB 连接的 HarmonyOS ARM64 真机;
hdc设备序列号FMR0223825079397。
6.2 静态检查与单元测试
./gradlew :share-kit:jvmTest :sample:shared:jvmTest
(cd example && ./gradlew :shared:jvmTest)
公共模块共有 5 个 JVM 测试,示例模块共有 2 个 JVM 测试,全部通过。测试覆盖目录类型、不可变生命周期、八项自检、JSON 转义、运行时附件、非法 ID、空请求和非法 URL。
6.3 原生桥接和 HAP 验证
./scripts/build-openharmony.sh
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
./scripts/build-hap.sh /absolute/path/to/signing-project
验证结果:
:share-kit:jvmTest PASSED
:sample:shared:jvmTest PASSED
:shared:jvmTest PASSED
:nativeApp:linkDebugSharedOhosArm64 PASSED
:nativeApp:prepareOhos PASSED
libshare_kit.so: 0 unresolved strong imports
entry-default-signed.hap generated
6.4 功能验证用例
用例 1:默认页面和自检
启动应用后页面显示“系统分享”和 8/8 自检通过,默认选中“文本”,请求标题为“每日摘录”。
用例 2:文本分享
点击“打开分享面板”,系统面板显示标题“每日摘录”、说明“来自 CMP Share Kit 的文本”,并列出设备当前可用的分享目标和复制等操作。
用例 3:链接分享
切换到“链接”,确认卡片显示 OpenHarmony URL。打开面板后,主记录是 general.hyperlink,附加记录携带说明文本。
用例 4:文件选择与分享
切换到“文件”,选择一个 PDF。应用只读打开选择器 URI,将文件复制到 context.cacheDir,生成应用自有 URI 后打开系统分享面板。面板保持稳定显示,文件记录数量为 1。
用例 5:取消、重新选择和刷新
在选择器中取消时,页面显示“未选择文件”并停止后续分享;重新选择会替换附件;点击“刷新内容”会增加 revision 并生成新的不可变请求。
用例 6:错误 URI 修复验证
修复前直接转发 file://docs/... 会出现 PROXY_AUTHORIZATION_URI: PERMISSION_DENIED、Both content and uri are empty 和错误码 1003703001。修复后日志显示 Records total count is 1、CreateFromWantParams finish: 0 和 Share data view-model loaded,说明 Share Kit 已成功解析文件记录。
6.5 验证结论
自动测试、Kotlin/Native ARM64 链接、ELF 依赖检查、Hvigor HAP 构建、签名安装和真机系统分享面板均已完成。文本分享面板和真实文件分享面板都能稳定显示,说明 KMP 请求契约、ArkTS 平台映射以及文件 URI 链路可用。
七、运行效果
7.1 真机截图

截图中可以看到:
- 页面标题为“系统分享”;
- 副标题说明同一份 KMP 内容交给鸿蒙系统分享面板;
- 当前选中“文本”请求;
- 系统面板正确显示“每日摘录”;
- 说明文字为“来自 CMP Share Kit 的文本”;
- 面板列出华为分享、微信、钉钉、QQ 等当前设备可用目标;
- 操作区显示复制、小艺帮记和添加至中转站;
- 分享面板覆盖在应用页面之上,证明调用的是真实系统组件。
7.2 命令速查
# 根工程测试和 OpenHarmony Native 准备
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
# 单独运行共享测试
(cd example && ./gradlew :shared:jvmTest)
# 设备依赖检查
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
# 在已配置签名的工程中构建 HAP
./scripts/build-hap.sh /absolute/path/to/signing-project
# 安装和启动
hdc -t <设备序列号> install -r \
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b com.ohos.sharekit.sample
八、遗留问题与改进方向
8.1 踩坑复盘
- 只打开自定义弹窗不算系统分享适配:必须由
ShareController.show打开真实系统面板。 - 选择器 URI 不能直接转发:
file://docs/...的临时读取权限不一定能够代理给分享扩展。 - N-API 不负责平台分享:C++ 只做参数检查、字符串转换和释放;Stage 上下文与 Share Kit 调用留在 ArkTS。
- UTD 和字段必须匹配:文本、链接使用
content,文件使用uri;字段不完整会被系统判定为空记录。 - 构建通过不等于真机可分享:还要验证接收端读取、系统面板加载和日志中的记录数量。
- 签名配置需要绑定产品:
products[].signingConfig必须指向本机配置,否则 Hvigor 只会生成未签名 HAP。
8.2 已知问题
- 当前页面示例只绑定单个附件,公共模型虽然允许最多 500 个附件,但批量选择尚未接入 UI;
- 缓存副本可能在系统存储紧张时被清理,正式产品需要按业务生命周期清理过期文件;
- 不同接收应用对
general.file、标题和说明字段的展示方式可能不同; - 系统面板显示的应用与操作取决于设备环境,不能在库中写死;
- 当前真机页面按公共契约维护 ArkTS 目录,尚未直接通过 N-API 加载 KMP 目录;
- 文章中的签名环境只用于本机验收,不能直接复制到其他开发环境。
8.3 未来优化方向
- 将 Stage 页面目录数据源切换为
libentry.so的getCatalog(),进一步减少示例层重复字段; - 增加多文件选择与
SelectionMode.BATCH真机用例; - 按图片、视频、音频分别完善 UTD 推断和详细预览;
- 增加缓存文件过期策略和成功分享后的延迟清理;
- 提供 Android
Intent、iOSUIActivityViewController与桌面端actual适配器; - 在持续集成中加入 Native 链接、依赖检查和未签名 HAP 构建任务。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个 ShareController.show 调用,而是一条完整跨端链路:
KMP ShareRequest
→ 内容类型和参数约束
→ ArkTS 平台映射
→ DocumentViewPicker 与缓存 URI
→ SharedRecord / SharedData
→ ShareController
→ HarmonyOS 系统分享面板
与此同时,Kotlin/Native、C ABI 和 N-API 提供了独立可验证的 OpenHarmony ARM64 产物链路。
9.2 封装层次
- KMP 层:定义稳定的请求、附件、生命周期、约束和 JSON;
- Native 层:生成 ARM64 动态库,并通过 C ABI 输出有限入口;
- N-API 层:完成参数检查、字符串转换和内存释放;
- ArkTS 层:管理文件选择、缓存复制、系统记录、面板参数和视觉展示;
- DevEco 层:完成 CMake、HAP、签名、安装和运行。
9.3 三条经验
- 先让共享请求模型在 JVM 和 Native 通过,再接入 ArkUI;
- 用 JSON 和少量 C ABI 代替跨语言对象传递,把平台对象留在 ArkTS;
- 把文件“可读取”和“可继续分享”当作两个权限边界,必须用真实接收端和系统日志验收。
9.4 适配成果
当前 kmp-share-kit 已完成:
- 文本、链接、图片、视频、音频和文件六类公共内容模型;
- 请求参数校验、不可变生命周期和稳定 JSON;
- JVM 和 OpenHarmony ARM64 共用的八项自检逻辑;
- Kotlin/Native + C ABI + N-API 验证链路;
DocumentViewPicker系统文件选择;- 应用缓存复制与可共享 URI 转换;
SharedData+ShareController真实系统面板;- 签名 HAP 构建、设备安装和真机效果图;
- 文件分享无响应问题的定位、修复和日志验证;
- 与参考 CMP 工程一致的模块和脚本组织;
- AtomGit 项目文档和 OpenHarmony 验收入口。
参考文档
更多推荐


所有评论(0)