本文记录
skiko接入 OpenHarmony 的完整过程,覆盖 Skia 二进制供体选型、社区 ohos klib 消费、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
skiko(Skia for Kotlin)是 JetBrains 的 Kotlin Multiplatform 图形库:把 Skia 的 Canvas / Paint / Path / Shader / 图像编解码 / 文本排版暴露为org.jetbrains.skia包下的 Kotlin API,是 Compose Multiplatform 在桌面与 iOS 上的渲染底座。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取"、KStore 的"真编译整库")有本质区别:skiko 的核心是 C++ 桥接层 + Skia 静态库二进制 ,上游从JetBrains/skiaGitHub releases 按平台下载预编译产物,而 Skia m155 没有 ohos 制品 ,自行编译 Skia 在评审环境不可复现。因此本仓采用"社区 klib 供体 "路线:消费 CPF-KMP-CMP 社区已发布在 eazytec Maven 上的org.jetbrains.skiko:skiko:0.9.22.2-1.0.0(ohos 版 Skia 静态内嵌于 klib),上游源码零改动 保留,新增 Kotlin/Native 桥@CName("SkikoRender")与 CMake NAPI 包装,把org.jetbrains.skia公共 API 的真实渲染结果(PNG)送到 ArkUI 真机页面上。
项目地址: AtomGit/oh-tpc/skiko.git
上游仓库: JetBrains/skiko
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
skiko 为 Kotlin 应用提供"2D 渲染"这一件事的完整答案:Surface 建画布,Canvas + Paint 画图形,Path 描述任意轮廓,Shader 出渐变,MaskFilter / ImageFilter / PathEffect 做滤镜与描边特效,Image 负责 PNG/JPEG 解码回编,FontMgr + Font + Canvas.drawString 负责文本。Compose Multiplatform 桌面端的每个像素都经过这套 API。OpenHarmony 应用同样需要高性能 2D 渲染能力,但上游构建(Kotlin 2.4.20 / Gradle 9.5.0 / Skia m155)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明 org.jetbrains.skia 渲染引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证:Skia 二进制在 ohos 侧的可达性、Kotlin/Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| Skia 二进制缺失 | 上游按平台从 JetBrains/skia releases 下载静态库(Skia-m155-...-<os>-<bt>-<arch>.zip),没有 ohos 产物;自编译需 depot_tools/GN + OHOS NDK,~30GB 构建树不可复现。 |
| 构建体系重型 | 上游 buildSrc 自定义 OS 枚举、cinterop 生成、MergeStaticArchivesTask、C++ 交叉编译全链路,为 ohos 改造等于重写发布流水线。 |
| 工具链不一致 | 上游 Kotlin 2.4.20 / Gradle 9.5.0 与鸿蒙工具链(HarmonyOS Kotlin 2.2.21 线 / Gradle 8.x 验证区间)无法共存于一个 Gradle 构建。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 渲染语义跨不了桥 | SkiaLayer、GPU 上下文、DirectContext 是平台 UI 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:社区 klib 供体的消费与验证、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。渲染语义完全由社区 klib 里的上游原版 org.jetbrains.skia 实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示 PNG。
1.2 库提供的能力
Demo 提供九个真实渲染场景,对应桥接的全部入口:
- 握手(info) :返回桥版本、klib 坐标、场景清单,并探测系统
FontMgr字体族数------启动即自检 JSON 链路; - 基础图形(shapes) :
drawRect/drawRRect/drawOval/drawCircle/drawLine/drawArc/drawPoints/drawRectShadow九宫格,前景色五选一、描边宽度 1...20 实时可调; - 渐变(gradient) :linear / radial / sweep 三种
Shader,暖冷两套色标,叠加BlendMode.MULTIPLY圆; - 路径(path) :五角星
PathFillMode.WINDINGvsEVEN_ODD中心对比、conicTo/cubicTo/arcTo,端帽(BUTT/ROUND/SQUARE)与拐角(MITER/ROUND/BEVEL)对照; - 路径效果(patheffect) :
PathEffect.makeDash/makeDiscrete/makeCompose作用在波浪线与五角星描边上; - 模糊(blur) :
MaskFilter.makeBlur四种FilterBlurMode(NORMAL/SOLID/OUTER/INNER)网格,sigma 2...60 滑杆实时调节; - 混合模式(blend) :8 种
BlendMode双色圆交叠网格; - 图像(image) :内嵌 6.8KB 示例 PNG 经
Image.makeFromEncoded解码 → 棋盘格透明背景 → 旋转 8° / 缩放绘制 → 左上角 2× 局部放大 →encodeToData(PNG)回编; - 文本(text) :
FontMgr.default默认字体族多字号渲染(含 CJK),notes回传ascent/descent度量与字体族诊断。
响应包含:
| 字段 | 作用 |
|---|---|
ok |
操作是否成功,失败时 error 带原文。 |
png / bytes |
渲染结果 PNG 的 Base64 与字节数(info 场景无此字段)。 |
kind / width / height |
回显场景与画布尺寸。 |
ms |
桥内耗时(TimeSource.Monotonic 计时)。 |
notes |
场景附注(image 的解码诊断、text 的字体度量)。 |
引擎侧完整能力(Canvas/Paint/Path/Shader/编解码/FontMgr/skparagraph 等约 240 个 Kotlin API 文件)全部随社区 klib 静态链接进 .so,真机页面上看到的所有像素都是 org.jetbrains.skia 真实渲染的产物,而非 ArkTS 自绘。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游 skiko/ 源码零改动 ;适配代码全部集中在 example/(桥 576 行 + MiniJson 235 行 + 工程配置)。 |
| 平台目标 | example/nativeApp 声明 ohosArm64 + ohosX64 出双 ABI 动态库 libohosskiko.so;上游构建独立保留(JVM/桌面可对照验证)。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式,Gradle 8.14.1);skiko/ 自带 9.5.0 wrapper 原样可跑,benchmarks/ samples/ 留盘不进构建。 |
| 桥接稳定 | 使用 1 个 C ABI(SkikoRender)和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 不重复发布任何 ohos 二进制:消费社区 skiko:0.9.22.2-1.0.0(其元数据声明的 stdlib 约束恰为 2.2.21-1.0.0,与 qrose 已过审工具链完全同线)。 |
| UI 完整 | 单页覆盖全部桥接入口:八场景 chip、参数控件、画布尺寸、notes 诊断列表和效果图显示。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。skiko 渲染管线的鸿蒙化(XComponent + OpenGL ES 自渲染、Fusion Renderer)属于 CPF-KMP-CMP 主线工程;本仓刻意收敛在"公共 API 是否可用"这一层,demo 走 CPU 光栅(
Surface.makeRaster)。
二、实现路线图
text
第 1 阶段:项目初始化 ── 盘点上游构建与 Skia 二进制边界、确认社区 klib 供体、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 与供体成对钉版
第 3 阶段:桥协议建模 ── 九场景 JSON 契约、PNG Base64 载荷、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── info 握手、场景 chip、参数控件、Base64 解码显示与 notes 列表
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
text
skiko/ 上游 JetBrains skiko 原样保留(独立 Gradle 构建,自带 9.5.0 wrapper,不进根 settings)
benchmarks/ samples/ 上游附属 includeBuild,留盘不进构建
example/nativeApp/ Kotlin/Native 动态库(SkikoBridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份,CN 版 19 节主文档)
README.upstream.md 上游原文保存
doc/ 博客文章与效果图
构建拓扑走 cmp_qrose 模式 (根构建即 ohos 构建):settings.gradle.kts 只 include :example:nativeApp,rootProject.name = "ohos_skiko"。上游的 dokka / 加密签名 / benchmark / 发布插件全部随根构建替换剥离------本 fork 不发 Maven Central,也不重新发布任何 ohos 二进制。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,"公司 OHOS Konan",含 HiLog/FFRT 平台 cinterop) |
| JDK | 21(Temurin 21.0.12,org.gradle.java.home 钉死) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(上游 skiko/ 构建独立保留作对照) |
| Gradle | 8.14.1(根适配构建,qrose 同款;上游 skiko/ 保留自带 9.5.0 wrapper) |
| 供体 klib | org.jetbrains.skiko:skiko:0.9.22.2-1.0.0(ohosArm64/ohosX64 双 klib,Skia 静态库打包于 included/,27.0 MB) |
| 传递依赖 | kotlinx-coroutines-core 1.10.2-1.0.0、atomicfu 0.31.0-1.0.0(由 klib 元数据解析) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.skiko |
powershell
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 供体制品验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 skiko 的 ohos 制品。用 .module 元数据验证:
text
org.jetbrains.skiko:skiko:0.9.22.2-1.0.0
variants: ohosArm64Api-published / ohosX64Api-published(klib, org.jetbrains.kotlin.native.target = ohos_arm64 / ohos_x64)
dependencies: kotlinx-coroutines-core 1.10.2-1.0.0 + atomicfu 0.31.0-1.0.0
dependencyConstraints: kotlin-stdlib → 2.2.21-1.0.0
三个关键结论:其一,skiko 的 ohos klib 由社区 skiko fork(develop-0.9.22.2-OH 线)生产,Skia 静态库(libskia.a、libicu.a、libharfbuzz.a、libskparagraph.a、libsvg.a 及编解码库共 ~17 个)整体打进 klib 的 included/ 目录 ,链接期静态并入产物------不需要任何 Skia 下载;其二,klib manifest 依赖 org.jetbrains.kotlin.native.platform.HiLog / .FFRT 平台 cinterop,这些随 HarmonyOS Kotlin 发行版发布,工具链必须是同线版本;其三,.module 里的 stdlib 约束版本就是应选的 Kotlin 版本------这条反推规则是本次适配最重要的经验(见第 2 阶段)。
skiko-ohosarm64 还发布了 sources jar(242 个 Kotlin 文件),供体 API 面可直接核对:FontMgr.default、Canvas.drawString、Image.encodeToData 等桥接所需 API 全部在 commonMain,无 ohos 特化缺口。
1.4 页面边界
text
标题区 skiko · OpenHarmony / 桥信息行(SkikoRender/1 ... org.jetbrains.skiko:skiko 0.9.22.2-1.0.0)
场景区 八 chip:基础图形 / 渐变 / 路径 / 路径效果 / 模糊 / 混合模式 / 图像 / 文本
参数区 随场景切换:色板+描边滑杆(shapes)/ 暖冷(gradient)/ 效果三选(patheffect)/ sigma 滑杆(blur)...
画布区 560 / 720 / 1080 三档
操作区 生成 (SkikoRender) 按钮
状态行 生成成功 shapes 720x720 png=28077B / 失败原文
notes 区 场景诊断(字体族、图像解码尺寸、FontMetrics 等)
图像区 Base64 解码为 PixelMap 全宽显示
页面启动时先发 scene:'info' 做握手:一行调用同时验证 N-API 导入、KN 链路、JSON 往返和 FontMgr 探针,把"桥是否活着"变成页面顶部可见的文字。
第 2 阶段:目标与依赖打通
2.1 根构建替换
settings.gradle.kts 把 eazytec 仓库写在最前:
kotlin
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_skiko"
include(":example:nativeApp")
根 build.gradle.kts 收敛为版本声明 + 全局禁用 ohos 测试任务(ohos 工具链没有测试运行器):
kotlin
plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
}
allprojects {
group = "org.jetbrains.skiko"
version = providers.gradleProperty("version").orElse("0.9.22.2-ohos.1").get()
}
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
gradle/libs.versions.toml 只钉两个版本:
kotlin
[versions]
kotlin = "2.2.21-1.0.0" # 必须等于供体 klib 的 stdlib 约束版本
skiko = "0.9.22.2-1.0.0" # CPF-KMP-CMP 社区 ohos klib
[libraries]
skiko-ohos = { module = "org.jetbrains.skiko:skiko", version.ref = "skiko" }
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
根构建与上游构建解耦 :上游 skiko/ 是重型构建(buildSrc、Gradle 9.5.0、Skia 下载、C++ 交叉编译),根 settings 不再 includeBuild 它------组合构建会强迫上游降到 8.14.1。上游构建入口仍是 skiko/gradlew.bat(9.5.0),随时可跑 JVM 目标做对照,这正是"零改动"的可信来源。
2.2 工具链钉版(为什么是 2.2.21-1.0.0)
最初按较新的 2.2.21-1.2.0-02 线配对(本机 ~/.konan 已有该发行版缓存),compileKotlinOhosArm64 直接报:
text
Execution failed for task ':example:nativeApp:compileKotlinOhosArm64'.
> java.lang.ClassNotFoundException: org.jetbrains.kotlin.cli.utilities.MainKt
排查结论:HarmonyOS Kotlin 的 KN 编译器以 in-process 模式运行,编译器类来自 ~/.konan/kotlin-native-prebuilt-windows-x86_64-<ver>/konan/lib/kotlin-native-compiler-embeddable.jar;本机缓存的 1.2.0-02 / 1.1.0-05 发行版该目录为空(eazytec Maven 上也无此 jar,HTTP 404),只有 2.2.21-1.0.0 发行版完整 。而 skiko:0.9.22.2-1.0.0 klib 的元数据约束恰好就是 kotlin-stdlib 2.2.21-1.0.0------工具链与供体成对切到 1.0.0 线后一次通过。
规则沉淀:供体 klib 的 .module 里 stdlib 约束版本就是应选的 Kotlin 版本,不要按"越新越好"配对。
2.3 编译桥模块
example/nativeApp/build.gradle.kts:
kotlin
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
ohosArm64 {
binaries.sharedLib { baseName = "ohosskiko" } // → libohosskiko.so
}
ohosX64 {
binaries.sharedLib { baseName = "ohosskiko" }
}
sourceSets {
ohosMain.dependencies {
// Gradle 模块元数据自动解析 ohosArm64/ohosX64 变体,
// 并传递拉取 coroutines-core / atomicfu 的 ohos 制品
implementation(libs.skiko.ohos)
}
}
}
fun cxxShared(triple: String): File {
val deps = file("${System.getProperty("user.home")}/.konan/dependencies")
val hits = deps.listFiles().orEmpty()
.map { File(it, "lib/$triple/libc++_shared.so") }.filter { it.isFile }
check(hits.isNotEmpty()) { "libc++_shared.so for $triple not found under $deps" }
return hits.first()
}
tasks.register<Copy>("copyKnSoArm64") {
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) { include("libohosskiko.so") }
from(cxxShared("aarch64-linux-ohos"))
into(rootProject.layout.projectDirectory.dir("example/harmonyApp/entry/libs/arm64-v8a"))
}
// copyKnSoX64 同构;linkDebugSharedOhos{Arm64,X64} finalizedBy 拷贝任务
实测:
powershell
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 # BUILD SUCCESSFUL in 49s
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosX64 # BUILD SUCCESSFUL in 34s
产物自动落入 example/harmonyApp/entry/libs/<abi>/:
| 产物 | arm64-v8a | x86_64 |
|---|---|---|
libohosskiko.so(含静态 Skia 全家) |
22,847,736 B ≈ 21.8 MB | 22,419,000 B ≈ 21.4 MB |
libc++_shared.so(konan sysroot 随附) |
1,850,184 B | 1,731,432 B |
so 内确认导出 SkikoRender 与 libohosskiko_symbols 两个 C ABI 符号;22 MB 的体积就是 Skia 静态库(libskia、libskunicode_icu、libicu、libharfbuzz、libskparagraph、libsvg、编解码库等)整体链接的结果,与 skiko 在 iOS 上的产物同量级。
第 3 阶段:桥协议建模
3.1 JSON 契约
text
org.jetbrains.skia/**(社区 klib,Skia 静态内嵌,原样链接)
↓ Surface.makeRaster / Canvas / Paint / Shader / Image / FontMgr
SkikoBridge.kt(@CName × 1,扁平 JSON 进、PNG Base64 出)
↓ UTF-8 JSON
N-API skikoRender()
↓
ArkUI Index.ets(八场景单页)
请求与响应:
text
请求 : {"scene":"shapes","width":720,"height":720,"fg":"#7C3AED","strokeWidth":4}
{"scene":"gradient","width":720,"height":720,"warm":true}
{"scene":"image","width":720,"height":720,"image":"<base64 PNG>"}
响应 : {"ok":true,"kind":"shapes","png":"<base64>","width":720,"height":720,"bytes":28077,"ms":37}
{"ok":false,"error":"...","ms":2}
两个设计选择:png 以 Base64 字符串承载(ArkTS util.Base64Helper 一次解码成 PixelMap);所有场景参数(fg / bg / strokeWidth / warm / effect / sigma / image / family)都是可选键,缺省值在桥内收敛,非法值走 ok:false 通道------任何异常都被捕获并转成 JSON,ArkTS 侧不会因非法输入崩溃。
3.2 序列化为什么手写 MiniJson
桥内 JSON 解析/序列化是 235 行手写 MiniJson(解析器 + 逗号状态机 Writer),不引入 kotlinx.serialization:序列化运行时进 so 的体积代价远超 30 行解析逻辑,qrose 适配时论证过同一取舍。真机闭环证明这个决定需要配套纪律------第一版 Writer 的逗号状态机有 bug(键值对之间漏逗号,输出 {"ok":true"kind":...} 非法 JSON),本机双 ABI 编译全绿也没暴露,直到真机 JSON.parse 抛 SyntaxError 才抓出来 。修复后 info 握手成为每次启动的序列化自检(见 8.1 踩坑 #5)。
3.3 错误通道
导出整体 try/catch,Kotlin 异常永远不跨 FFI:
kotlin
@CName("SkikoRender")
fun skikoRender(request: String): String {
val started = TimeSource.Monotonic.markNow()
return try {
dispatch(parse(request) as JsonValue.Obj, started)
} catch (t: Throwable) {
fail("${t::class.simpleName ?: "error"}: ${t.message ?: "no message"}", started)
}
}
非法 JSON、未知 scene、image 场景缺 image 字段、坏 Base64、字体后端缺失,全部走同一个 ok:false 通道,ArkTS 侧统一渲染错误原文。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
text
ArkTS
│ JSON string
▼
C++ N-API entry(libskiko_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosskiko.so)
│ SkikoRender
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复刻渲染语义,所以选择 C ABI + JSON。与 KStore 的两个符号不同,skiko 只需要一个入口:场景分发本来就是 JSON 里的一个键,渲染参数天然结构化。
4.2 Kotlin/Native 导出函数
kotlin
@CName("SkikoRender")
fun skikoRender(request: String): String // info + 八渲染场景一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
SkikoRender |
skikoRender(req) |
Surface.makeRaster 建画布 + 九场景全部 org.jetbrains.skia 调用 + encodeToData(PNG) 回编 |
4.3 C++ N-API 方法分发
cpp
napi_property_descriptor desc[] = {
{"skikoRender", nullptr, NapiSkikoRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosskiko_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosskiko_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构前缀与 SkikoRender 业务符号),与 KN 生成的完整头逐项核对过 ABI 兼容,不拖整份生成头。
4.4 CMake 和类型声明
cmake
add_library(ohosskiko SHARED IMPORTED)
set_target_properties(ohosskiko PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(skiko_napi SHARED napi_init.cpp)
target_link_libraries(skiko_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosskiko)
OHOS_ARCH 为空或 libohosskiko.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
typescript
export const skikoRender: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 握手与分发
typescript
aboutToAppear(): void {
this.runInfo(); // scene:'info' → 桥信息 + FontMgr 字体族数进顶栏
}
private generate(): void {
const scene = this.scenes[this.sceneIndex].id;
const req: RenderRequest = {
scene: scene,
width: this.canvasSize,
height: this.canvasSize
};
if (scene === 'shapes') {
req.fg = this.fgs[this.fgIndex]; // 参数按场景补可选字段
req.strokeWidth = this.strokeWidth;
} /* ... gradient/patheffect/blur/image ... */
const res = JSON.parse(skikoRender(JSON.stringify(req))) as RenderResponse;
if (!res.ok || res.png === undefined) {
this.status = `失败: ${res.error ?? 'no png'}`;
return;
}
this.decodePng(res.png); // Base64 → PixelMap → @State 刷新
}
RenderRequest / RenderResponse 都是显式声明的 interface(可选字段用 ?:),ArkTS 严格模式(arkts-no-untyped-obj-literals)下对象字面量必须对应显式类型 ------第一版用 Record<string, Object> 动态构造请求被编译器拒绝,改为 interface 化后通过(见 8.1 踩坑 #6)。
5.2 Base64 到像素
typescript
private decodePng(b64: string): void {
const bytes: Uint8Array = new util.Base64Helper().decodeSync(b64);
const sliced = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
image.createImageSource(sliced).createPixelMap().then((pm: image.PixelMap) => {
this.pixelMap = pm; // Image(this.pixelMap) 全宽显示
});
}
5.3 页面交互
单页自上而下:标题与桥信息行、八场景 chip、画布尺寸三档、随场景切换的参数区(色板圆点 + 描边滑杆 / 暖冷切换 / dash·discrete·compose 三选 / sigma 滑杆 / 场景说明文字)、生成按钮、状态行(生成成功 shapes 720x720 png=28077B)、notes 诊断列表(字体族、解码尺寸、FontMetrics)、渲染结果图。文本场景的说明直接标注"失败时会显示错误信息"------字体后端属系统侧能力,页面把不确定性如实交给用户。
第 6 阶段:示例与验证
6.1 工程结构
text
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosskiko)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── SkikoBridge.kt @CName("SkikoRender") + 九场景(576 行)
│ └── MiniJson.kt 手写 JSON 解析/序列化(235 行)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.skiko、versionName 0.9.22.2-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/libohosskiko_api.h
├── src/main/cpp/types/libskiko_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
powershell
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp(勿开仓库根目录)
# File → Project Structure → Signing Configs 勾选自动签名,Build HAP(s)
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。
powershell
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.skiko -m entry
四、完整代码对照
4.1 整体架构
text
org.jetbrains.skia/**(社区 klib 原样链接,Skia 静态内嵌)
│ Surface / Canvas / Paint / Path / Shader / Image / FontMgr
SkikoBridge.kt + MiniJson
│ SkikoRender(九场景分发 + PNG Base64)
libskiko_napi.so
│ skikoRender
Index.ets
│ JSON parse + Base64 解码 + @State 更新
▼
ArkUI 真机页面(八场景单页)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts |
极简 ohos 构建、eazytec Maven 置前、ohos_skiko、上游 includeBuild 剥离 |
gradle/libs.versions.toml |
Kotlin 2.2.21-1.0.0(= 供体 stdlib 约束)+ skiko 0.9.22.2-1.0.0 钉版 |
build.gradle.kts(根) |
版本 0.9.22.2-ohos.1、全局禁用 Ohos*Test* 任务 |
example/nativeApp/build.gradle.kts |
双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../SkikoBridge.kt |
1 个 C ABI、九场景、错误通道 |
nativeApp/.../MiniJson.kt |
手写 JSON 解析器 + Writer |
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 验收页面(interface 化请求/响应类型) |
README.OpenHarmony_CN.md |
鸿蒙适配主文档(19 节,含去重定位与故障排查) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | Surface.makeRaster(ImageInfo.makeN32Premul) |
CPU 光栅画布(RGBA8888 预乘) |
| 引擎 | Canvas.drawRect/drawRRect/drawCircle/drawArc/drawPoints/drawString |
图元与文本绘制 |
| 引擎 | Paint(mode/color/strokeWidth/blendMode/maskFilter/pathEffect/shader) |
画笔全参数 |
| 引擎 | Shader.makeLinear/Radial/SweepGradient |
三种渐变 |
| 引擎 | PathEffect.makeDash/makeDiscrete/makeCompose、MaskFilter.makeBlur |
描边特效与模糊 |
| 引擎 | Image.makeFromEncoded / Image.encodeToData(PNG) |
图像解码 / 回编 |
| 引擎 | FontMgr.default / Font / Font.metrics |
系统字体、度量 |
| Native | SkikoRender |
C ABI JSON 导出(九场景一口进) |
| N-API | skikoRender(req) |
向 ArkTS 暴露能力 |
| ArkTS | generate() / decodePng() |
触发渲染并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
typescript
const req: RenderRequest = { scene: 'shapes', width: 720, height: 720, fg: '#7C3AED', strokeWidth: 4 };
const res = JSON.parse(skikoRender(JSON.stringify(req))) as RenderResponse;
// res.ok === true;res.png 是 Skia 渲染结果的 PNG Base64,decodePng 后成 PixelMap
kotlin
val surface = Surface.makeRaster(ImageInfo.makeN32Premul(w, h))
shapesScene(surface.canvas, w, h, req) // drawRect / drawRRect / drawCircle / ...
val data = surface.makeImageSnapshot().encodeToData(EncodedImageFormat.PNG, 100)
val png = Base64.Default.encode(data.bytes) // → 响应 JSON
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:社区 klib 供体,而不是自编译 Skia 或改造上游构建
上游 ohos 编译的三条路(给 buildSrc 加 OS.Ohos、自编译 Skia、等官方发布 ohos zip)全部卡在"Skia m155 没有 ohos 二进制"这一件事上。社区 fork(develop-0.9.22.2-OH)已经解决了这件事并把 klib 发布在 eazytec Maven------再自己编一遍既是重复劳动,也无法在评审环境复现。消费制品让"Skia 可用"从构建问题降级为依赖声明问题。
决策 2:上游源码零改动 + 双构建解耦
本仓对上游的唯一改动是根构建与 example/ 新增文件,skiko/ 目录一个字节没动,master 可以随时 fast-forward。根构建(8.14.1,HarmonyOS Kotlin 验证区间)与上游构建(9.5.0)各自独立,互不拖累------组合构建会强迫上游降级,风险不可控。
决策 3:工具链与供体成对,版本由元数据反推
先查 klib .module 的 stdlib 约束,再钉 Kotlin 版本:skiko:0.9.22.2-1.0.0 → 2.2.21-1.0.0。反过来按"本机缓存最新"配对(1.2.0-02)会踩进 konan 发行版 konan/lib 为空的坑,报出毫无指向性的 ClassNotFoundException: MainKt。
决策 4:单 C ABI + JSON-in/PNG-out + CPU 光栅
渲染管线的鸿蒙化(XComponent + GLES 自渲染、Fusion Renderer)是 CPF-KMP-CMP 主线的战场。本仓把验证面收敛在"公共 API 是否可用":一个符号、一份 JSON 契约、CPU 光栅出 PNG------问题定位最直接,也不与主线重复。GPU 路径留给供体升级时评估。
决策 5:真机闭环 + info 握手自检
编译全绿证明不了序列化正确性:第一版 JsonWriter 逗号 bug 只有真机 JSON.parse 能暴露。scene:'info' 被设计成每次启动的必经握手------桥版本、klib 坐标、字体族数一次调用全回来,序列化链路坏没坏在页面顶部直接可见。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11(x64)、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 本机构建验证
text
:example:nativeApp:linkDebugSharedOhosArm64 : BUILD SUCCESSFUL(49s,含 klib 下载与链接)
:example:nativeApp:linkDebugSharedOhosX64 : BUILD SUCCESSFUL(34s)
libohosskiko.so 导出符号检查 : SkikoRender / libohosskiko_symbols 均存在
libc++_shared.so 随拷贝任务进入 entry/libs/{arm64-v8a, x86_64}
手写 libohosskiko_api.h 与 KN 生成头逐项核对 ABI 兼容
6.3 HAP 验证
text
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
BuildNativeWithCmake: finished(双 ABI 编出 libskiko_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
HAP 内双 ABI 各三件套(libskiko_napi.so / libohosskiko.so / libc++_shared.so);KN so 因静态含 Skia 全家(arm64 ≈ 21.8 MB)明显大于纯 Kotlin 桥的库,属预期体积。
6.4 功能用例
真机已验证(截图见下节):
- 启动即握手:标题下出现
SkikoRender/1 (ohos_skiko example) | org.jetbrains.skiko:skiko 0.9.22.2-1.0.0 (CPF-KMP-CMP)------来自真实 Native 调用,同时证明 JSON 序列化链路正常; - 基础图形场景:
生成成功 shapes 720x720 png=28077B,3×3 网格完整呈现矩形、圆角矩形、椭圆、圆、交叉线、弧、点阵、阴影方块与胶囊------全部为org.jetbrains.skia真实渲染; - 参数联动:色板选中紫色(
#7C3AED)与描边 4 直接体现在渲染结果上; - 首次调用含静态初始化的耗时在
ms字段可见,第二次起回落稳定值。
验收清单(逐场景过点,随真机测试回填):
- 渐变 / 路径 / 路径效果 / 模糊 / 混合模式五场景图形正确、参数即时生效;
- 图像场景:内嵌示例 PNG 解码绘制、透明区呈棋盘格、旋转缩放无锯齿崩坏、
notes回传解码尺寸; - 文本场景:CJK 渲染或明确
fontError诊断(后端缺失不崩溃),notes显示 ascent/descent; - 非法输入(未知 scene、缺
image字段)返回ok:false原文,应用不崩溃; hdc shell hilog | grep SkikoNapi可见 request bytes 打点。
6.5 验证结论
社区 klib 供体、Kotlin/Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面顶部握手信息与基础图形场景渲染结果均为真实 Native 调用产物(见下节效果图),其余场景按验收清单进行中。
七、运行效果
7.1 真机截图

7.2 命令速查
powershell
git clone https://atomgit.com/oh-tpc/skiko.git
cd skiko
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp,自动签名后 Build HAP(s)
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.skiko -m entry
hdc shell hilog | grep SkikoNapi
八、遗留问题与改进方向
8.1 踩坑复盘
- 工具链与供体必须成对 :按本机缓存最新配 Kotlin
2.2.21-1.2.0-02报ClassNotFoundException: org.jetbrains.kotlin.cli.utilities.MainKt------该发行版konan/lib为空且 Maven 上无 embeddable jar;正确做法是读供体 klib.module的 stdlib 约束反推版本(本例0.9.22.2-1.0.0→2.2.21-1.0.0); gradlew.bat报JAVA_HOME is not set:gradle.properties的org.gradle.java.home只在 JVM 起来后生效,wrapper 启动阶段只认环境变量------先set JAVA_HOME再执行;settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则 ohos 制品与 HarmonyOS Kotlin 插件都解析不到;- 鸿蒙工具链把
kotlin.system.getTimeMillis的弃用当 error 报,计时改TimeSource.Monotonic.markNow()/elapsedNow().inWholeMilliseconds;skiko API 全 Float,参数解析 Double 要显式.toFloat(); - 编译全绿 ≠ 序列化正确 :第一版手写 JsonWriter 的逗号状态机漏了键值对间隔符,输出
{"ok":true"kind":...}非法 JSON,本机双 ABI 构建毫无异常,真机JSON.parse才抛SyntaxError------修复后把scene:'info'设计成每次启动的握手自检; - ArkTS 严格模式(
arkts-no-untyped-obj-literals)拒绝Record<string, Object>动态构造请求与裸对象字面量入JSON.stringify:请求/响应全部 interface 化(可选字段?:),组件 Options 字面量(Slider({...})等)不受限; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; dlopen找不到 KN so:CMake 设BUILD_RPATH/INSTALL_RPATH $ORIGIN+ POST_BUILD 把 NAPI so、KN so、libc++ 拷到同一输出目录;- KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();手写 api 头只声明ExportedSymbols结构前缀与业务符号,并与 KN 生成头核对; - ArkTS 只能 import NAPI 模块(
libskiko_napi.so),不能 import KN so(libohosskiko.so)------CAdapter 不会把@CName业务符号挂给 ArkTS; - 签名材料由 DevEco 自动写入
build-profile.json5,仓库内保持signingConfigs空数组,证书绝不进入 AtomGit; - 全仓清扫第三方镜像链接(含文档、配置、示例),社区与仓库链接统一 AtomGit(oh-tpc / CPF-KMP-CMP)。
8.2 已知问题
- Demo 走 CPU 光栅(
Surface.makeRaster);DirectContext/GPU、XComponent 自渲染与 Fusion Renderer 不在本仓验证范围(属 CPF-KMP-CMP 主线); - 文本场景依赖系统
FontMgr:后端缺失时text场景返回ok:false与诊断串(info的fontFamilies/fontError可提前探知),其余场景不受影响; - 供体 klib 是 0.9.22.2 线(Skia m150 时代),上游 master 已到 m155:
org.jetbrains.skia公共 API 两线间高度稳定,但 m155 新增符号不在验证面内; linkReleaseSharedOhos*未在本机跑(文档体积数据以 debugShared 为准);- 上游
skiko/构建未编排进根构建,JVM 对照(:compileAwtKotlinJvm)需独立执行且依赖宿主 Skia 下载与 MSVC 工具链; - HAP 安装必须使用与 bundle name
org.terminator.ohos.skiko匹配的签名。
8.3 未来优化方向
- 社区 0.150.1 线(Kotlin 2.4.20-0.1.0)转正后切换供体:改
gradle/libs.versions.toml两行版本号 + 按编译错误微调场景调用; - 桥增加 skparagraph / skottie 场景(klib 已静态含 libskparagraph.a / libskottie.a,只差桥接入口);
- 评估 XComponent + OpenGL ES 的 GPU 光栅化 demo,与社区自渲染主线对表;
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)验收矩阵与自动化截图脚本,
scene:'info'接入冒烟断言。
九、总结
9.1 核心难点回顾
text
上游 skiko master @ 8f09bee48(Kotlin 2.4.20 / Skia m155)
→ 验证 Skia ohos 二进制不可达 → 选定社区 klib 供体(0.9.22.2-1.0.0)
→ 工具链成对钉版 Kotlin 2.2.21-1.0.0(供体 stdlib 约束反推)
→ 根构建替换(cmp_qrose 模式,Gradle 8.14.1),上游构建零改动解耦
→ ohosArm64 / ohosX64 双 ABI libohosskiko.so(Skia 静态内嵌)
→ C ABI(@CName × 1,九场景 JSON-in/PNG-out)
→ C++ N-API(DisposeString 纪律)
→ ArkTS interface 化请求 + Base64 解码
→ ArkUI 真机单页验收(握手 + shapes 已验证,效果图留档)
9.2 封装层次
- 供体层:社区 klib 里的上游原版
org.jetbrains.skia(Canvas/Paint/Path/Shader/编解码/FontMgr)+ 静态 Skia 全家; - 桥层:九场景分发、参数收敛、PNG 回编与错误通道(手写 MiniJson,零额外运行时依赖);
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、场景与参数、Base64 解码显示、notes 诊断;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 二进制型库的适配答案是消费社区制品,而不是重造二进制:skiko 的价值载体是 Skia 静态库,"把上游 Kotlin 源码编进 ohos"这条路天然不通;识别"供体已存在"并明确与供体仓库的分工(本仓不重复发布任何 ohos 二进制),比改造上游构建体系更快也更可复现;
- 工具链版本由供体元数据反推 :klib
.module的 stdlib 约束就是应选的 Kotlin 版本;"本机缓存最新"的配对会踩进发行版残缺的坑,报出毫无指向性的 ClassNotFound; - 真机闭环是序列化正确性的唯一验证 :编译期一切正常的手写 Writer 仍可能带着逗号 bug 出厂;把握手(
info)做成启动必经路径,让"桥活着"在页面顶部可见,问题从用户反馈降级为自检红灯。
9.4 适配成果
当前适配已完成:
- 上游 master(
8f09bee48,Kotlin 2.4.20 / Skia m155)源码零改动的 OpenHarmony 验证仓(根构建 cmp_qrose 模式,唯一改动是根配置与 example/ 新增文件); - 社区 klib 供体的双 ABI Kotlin/Native 落地(
libohosskiko.soarm64 ≈ 21.8 MB / x86_64 ≈ 21.4 MB,Skia 静态内嵌,C ABI 符号验证); - 1 个 C ABI 符号 + 手写 MiniJson 的九场景渲染桥(576 + 235 行 Kotlin,全部在 example/);
- C++ N-API 包装层(
libskiko_napi.so,DisposeString 纪律 + hilog 打点); - DevEco Stage ArkUI 单页验收页面(八场景 chip、参数联动、notes 诊断、Base64 像素显示);
- 真机安装运行验证(握手 + 基础图形场景效果图留档,其余场景按验收清单推进);
- AtomGit 项目文档、OpenHarmony README(中英双份,CN 版 19 节)和本地效果图。
适配过程 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。