Air 系列应用交流/轻像素|AirPixi · 鸿蒙 PC 适配全记录

轻像素|AirPixi · 鸿蒙 PC 适配全记录

【Air 系列 · 鸿蒙 PC 适配全记录】30 / 56

■ 应用:轻像素(AirPixi)
■ 定位:PixiEditor 移植,轻量级像素画编辑器
■ 上游:PixiEditor | 路线:Avalonia 自绘直连 | 版本:1.1.0
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917617572987959265
■ 官网(免费下载): https://www.shunidc.cn/

————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/

PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:(AtomGit 仓库建立后回填)

环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743

▍一、为什么要适配 AirPixi

PixiEditor 是一款开源的轻量级像素画编辑器(C#/Avalonia 技术栈):图层系统、像素级画笔与矢量形状、调色板、动画帧、.pixi 工程格式,交互密度接近专业工具。它的技术栈代表了桌面开源软件里相当大的一族——.NET 应用,而鸿蒙上既没有 CLR 也没有 Mono 运行时,传统"带运行时"的移植路线在这里走不通。

AirPixi 选择的路线是把 .NET 推到极致:用 .NET 10 NativeAOT 把 PixiEditor 与 Avalonia 12.1.1 整体交叉编译成 linux-musl-arm64 目标的单个 libpixieditor.so,以动态库形态被鸿蒙应用加载;Avalonia 的窗口、输入、渲染平台接口则由一层自研 OpenHarmony 平台实现承接,渲染走 SkiaSharp 软件路径直连 XComponent。工程 PORTING.md 记录了从 Phase 0 工具链闭环到 S1-S14 标准审计销项的完整过程,一百二十余个实证坑位让这份记录本身就是一条路线的可行性报告。

当前鸿蒙工程的应用名为 AirPixi(轻像素),包名 com.airpixi.app,版本 1.0.0,deviceTypes 覆盖 phone、tablet 与 2in1,声明 ohos.permission.INTERNET。module.json5 里有两处值得注意的显式声明:launchType: "singleton"(native 层单一 XComponent 绑定与全局 MainImpl 无法承载同进程第二实例)与 supportWindowMode: ["fullscreen","split","floating"](不声明时自由多窗入口不可达、2in1 还原按钮置灰)。Native 侧为 napi_init.cpp 与 airpixibridge.cpp 两个模块,目标设备实证覆盖 MatePad 11.5 与 MatePad Mini 悦读版(API 26)。

▍二、先划清边界:不把编译通过当成适配完成

Avalonia 应用在桌面上的窗口管理、输入合成、渲染循环全部由操作系统与 Avalonia 自身平台层协作完成,NativeAOT 又在其上叠加了裁剪与反射限制。对这类应用,"so 编译出来"只是起点:XAML 资源加载、泛型裁剪、样式系统、弹窗层级,任何一环断裂都会表现为白屏或假死,而表面现象高度雷同。工程因此把验证锚点定在行为上:PORTING.md 的阶段表记录了从"真机出图像素实证"到"§9 十四项标准审计逐项销项"的推进,而不是以构建成功为里程碑。

能力边界与两侧方案的对照如下。

层次 原项目实现(桌面 PixiEditor) 鸿蒙侧当前方案
运行时 .NET 桌面 CLR(JIT) .NET 10 NativeAOT 静态编译,产物 libpixieditor.so
窗口系统 操作系统窗口 + Avalonia IWindowImpl EmbeddableWindowImpl 单表面窗口,弹窗为窗口内 overlay
渲染 GPU 合成(Skia GPU 互操作) 软件渲染(SkiaSharp 3 musl)→ GL 纹理上屏
主菜单 Avalonia 原生菜单控件 ArkTS 原生菜单栏,MenuBridge JSON 桥接真实 ICommand
文件对话框 桌面 StorageProvider OpenHarmonyStorageProvider + DocumentViewPicker + 沙箱 staging
字体 系统 fontconfig 内嵌 HarmonyOS Sans 兜底 /system/fonts/HarmonyOS_Sans_SC.ttf

其中"弹窗为窗口内 overlay"是最重要的一条结构差异:单 XComponent 表面下不存在第二个原生窗口,NewFilePopup 等对话框以独立 framebuffer 在主窗渲染循环里合成(PaintOverlays premul alpha blit),输入路由与渲染叠序由此必须同源管理。

▍三、鸿蒙版的整体架构

架构由四个部分组成:ArkTS 壳(菜单栏、文件选择器执行端、系统能力接入口)、两个 NAPI 模块(entry 承载 XComponent 载荷路径、airpixibridge 承载壳侧调用)、C# 平台层(PixiEditor.OpenHarmony 与 PixiEditor.Platform.OpenHarmony,实现 Avalonia 平台接口),以及 NativeAOT 产物本体。壳与 C# 之间的通道有三条:XComponent 输入回调(下行)、MenuBridge 菜单 JSON 与触发(双向)、PickerBridge 任务队列(壳执行系统选择器后回投)。

text
ArkTS 壳(entry/src/main/ets)
├── EntryAbility:singleton + onNewWant,FLOATING 态隐藏系统标题栏
├── Index.ets:菜单栏 Row + XComponent(TEXTURE,暗底防透底)
│ ├── AirTopMenuItem:bindMenu 渲染菜单树,2s 轮询 JSON 变化
│ ├── PickerBridge 执行端:400ms 轮询 → DocumentViewPicker → staging/writeback
│ └── IME 接入:inputMethod + AirPixiIme_Text 回注
├── napi_init.cpp(entry):XComponent surface 与输入回调
└── airpixibridge.so:getMenuJson/menuTrigger/takeTask/completeTask
│ dlsym 转发
▼
libpixieditor.so(NativeAOT:PixiEditor + Avalonia 12.1.1 + 平台层)
├── EmbeddableWindowImpl / WindowingPlatform:窗口、overlay 弹窗、输入接管
├── HarmonyInterop:hold 表面、chrome 矩形、overlay 文本编辑、拖拽钩
├── MenuBridge:MenuItem 树 ↔ JSON,UnmanagedCallersOnly 导出
├── OpenHarmonyStorageProvider / ClipboardImpl / OpenHarmonyInputMethod
└── Drawie 软件渲染 → SkiaSharp → InteropControl GL 上屏


主要目录如下:

text
AirPixi/
├── AppScope/ # 应用级配置(com.airpixi.app)
├── PORTING.md # 移植进度与 120+ 实证坑位记录
├── entry/src/main/
│ ├── module.json5 # singleton、supportWindowMode、权限
│ ├── ets/
│ │ ├── entryability/EntryAbility.ets # 沉浸式、自由多窗装饰管理
│ │ ├── entrybackupability/
│ │ └── pages/Index.ets # 菜单栏、XComponent、选择器/IME/分享
│ ├── cpp/
│ │ ├── napi_init.cpp # XComponent 载荷与输入回调
│ │ ├── airpixibridge.cpp # 菜单 JSON 与选择器任务桥
│ │ └── types/libairpixibridge/ # 桥接口 .d.ts
│ └── resources/rawfile/ # 随包资源
└── test-evidence/ # 真机回归证据(截图/布局清单)


▍四、核心适配过程

### 1. 平台层:把 Avalonia 12 的接口面接到鸿蒙

C# 平台层分两个工程:PixiEditor.OpenHarmony(AssemblyName 即 libpixieditor,UseClassicLifetime=true 模拟桌面启动时序)与 PixiEditor.Platform.OpenHarmony(HarmonyOsOperatingSystem,GetAvailableRenderers 只报 Software)。实现从 ITopLevelImpl 开始,最终落在 WindowingPlatform.cs 的 EmbeddableWindowImpl:主窗与弹窗共用一个实现类,主窗绑定 XComponent 表面,弹窗持有独立 framebuffer 并由 SyncToWindowBounds 每帧向内容尺寸收敛(增长立即、缩容三帧防抖)、RepairWindow 负责布局与渲染器修复。PORTING.md 的 Phase 1b 记录了平台层在 Avalonia 11.2.2 → 12.1.1 升级中的五个接口断裂点:ref 程序集毒丸(必须对实现程序集编译)、UseHarfBuzz 必需、Dispatcher 注入方式变更、FontManager 绑定顺序、初始 Resized 双触发——每一条都有对应修法。

### 2. NativeAOT 产物链与发布纪律

发布命令是一条带完整 OHOS 开关的 dotnet publish:-p:OpenHarmonyTarget=true -p:OpenHarmonyApiLevel=26 -p:OpenHarmonyAbi=arm64-v8a -p:OpenHarmonySdkRoot=... -p:JsonSerializerIsReflectionEnabledByDefault=true,RID 全局覆盖为 linux-musl-arm64。每个开关都对应一次实证事故:缺 JsonSerializerIsReflectionEnabledByDefault 会因 runtimeconfig 自带的重复 false knob 在 RuntimeConstants..cctor 抛 InvalidOperationException;缺 OHOS llvm 或 OpenHarmonyTarget 会走错链接器或漏编 seccomp 桩(即便链接侥幸通过,上机即 SIGSYS);PATH 不含 ~/.dotnet 时 PublishAotCross 内部裸调 dotnet 以 exit 127 假绿。反射与裁剪的对抗集中在三件套:AotAssetLoader 自解析 !AvaloniaResources blob、rd.xml 保根名单(含 Avalonia.Fonts.Inter 这类按名加载的资源程序集)、程序化生成 104 个闭式泛型的 AotRoots.All.cs。发布纪律里最硬的一条是字节级验证:#if HARMONY 块修完必须在产物 so 上做 UTF-16LE 符号扫描,"publish 成功"与"HAP 内 so md5 一致"都不算数——增量缓存的假绿在 PORTING.md 里反复出现。

### 3. 输入体系:从 XComponent 回调到 Avalonia RawInput

触摸、鼠标、键盘、悬停、滚轮轴事件全部从 XComponent 原生回调进入,经平台层翻译成 Avalonia RawPointerEventArgs/RawKeyEventArgs;笔输入按 toolType 分流到 PenDevice 并携带压感与倾角。这条链路上最核心的自建设施是手势保持窗:首指 TouchBegin 先保持 110ms 或 9px 滑动阈值内不分发,窗口内第二指落地则整段作为捏合手势接管(首指从未分发,零误笔画);超时或超阈则按正常路径补发。配套的还有 chrome 矩形注册表——工具栏、dock、菜单栏等浮在画布上的控件区域免保持(点击即时响应),判定规则为"可见 InputElement、非 Scene 子树、RendersAboveScene 层叠序在 Scene 之上",布局变化标脏后由壳帧泵消费、200ms 节流全树重算。硬件键方面,Phase 1c 记录了一条否定性实证:ETS import 转发路线不可行(详见难点一),硬件键一律走 XComponent 原生键回调。IME 则走显式通道:画布文本工具激活时经 HarmonyInterop.OverlayTextEditing 通知壳拉起软键盘,输入经 AirPixiIme_Text → RawTextInput 回注。

### 4. 壳层桥:菜单 JSON 与选择器任务队列

MenuBridge.cs 把 Avalonia MenuItem 树导出为 JSON(id 路径到活菜单项的映射,CanExecute 守卫生效,禁用项随导出同步),经 AirPixiMenu_GetJson/Free/Trigger 三个 UnmanagedCallersOnly 导出函数暴露;ArkTS 侧 libairpixibridge.so 用 dlsym 转发(独立 NAPI 模块,刻意不碰 XComponent 加载路径),Index.ets 以 2 秒轮询检测 JSON 变化重建菜单,AirTopMenuItem 用 bindMenu 渲染级联子菜单。文件选择器是对称的任务队列:C# 侧 PickerBridge 入队请求,壳侧 400ms 轮询 takeTask 出队,执行 DocumentViewPicker 后把 URI 结果落沙箱 staging(1MB 分块拷贝)再 completeTask 回投,保存路径则以 writeback 任务推回源 URI。退出流程同样走这条桥:Environment.Exit 会被 appspawn 的 exit 钩子拦截转 SIGABRT,正确路径是经 PickerBridge 发 terminate 任务由 ArkTS 调 terminateSelf()。

### 5. 按需渲染:软渲下的性能攻坚

Phase 4 前基线是空闲 19fps 全量重绘、笔画期 18fps。终态架构是五件套:_framebufferDirty 门控 GL 上传与 swap、_forcePaint 仅在 resize/首帧/overlay 变化时触发全量、retained 帧缓冲(FramebufferLockProperties(true) 让服务端不再每帧补全表面脏矩形)、每帧反射泵 Compositor.Server.Render 且泵序固定在 overlay 绘制之前、叠加层仅在锁代数前进或几何移动时 blit。配合单帧优化的双栅格消除(SkiaDrawOperation 零拷贝 blit 取代合成器二次栅格)与 glTexSubImage2D 复用显存,笔画期提升到 20-25fps、弹窗开态 55-59fps、空闲零绘制零上传。中途还有一条平台性发现:Avalonia 的 DispatcherPriority.Background 在 OpenHarmony 上因无消息循环 idle 机制而永不排空,增量渲染饿死的最终修复是把该优先级提为 Normal(#if HARMONY 隔离)。

▍五、适配中遇到的几个难点

### 难点1:ETS import 转发路线的否定性实证

最初设想的硬件键接入方式是让 ArkTS 直接 import Avalonia so 里的导出函数。实证结果两条路都死:import x from 'libavalonia.so' 的 default import 得到 undefined,后续调用抛 TypeError 直接 JsError 杀进程;import { injectKeyEvent } from 'libavalonia.so' 的命名导入则因 so 名与 NAPI 模块 nm_modname 不匹配返回空模块,SyntaxError 同样杀进程。这条路线被正式记为不可行,硬件键改为 XComponent 原生键回调(配套 focusable(true)+defaultFocus(true),Phase 1c 同时实证了主窗场景下 focusable 与触摸可以共存,推翻了手册中"focusable 拦触摸"的弹窗场景结论)。这个难点的价值在于排除了一条看似顺理成章的路线,避免后续工程重复试错。

### 难点2:NativeAOT 下的运行时静默降级

AOT 裁剪引发的故障几乎都不报错:StandardAssetLoader 经 AppDomain.GetAssemblies() 找程序集在 AOT 下为空,资源判定全部返回 false;FramebufferLockProperties 是 record struct,用 NonPublic 反射查它的 init-only 属性永远返回 null,被 catch 吞掉后服务端按"内容不保留"处理,静态场景陷入每秒 60 次全量重绘;MessagePack 动态 resolver 在 AOT 下直接失效,.pixi 工程反序列化全灭。工程的三板斧是:能直接引用就删除反射(record struct 直接 new)、必须反射的进 rd.xml 保根并警惕"ILC 裁剪让 GetProperty 静默返回 null"造成的假象、源生成器优先(GeneratedMessagePackResolver、JSON 上下文源生成)。配套的方法论是行为签名验证:每个反射点修复后都要有可观测的日志或计数变化,才算修复成立。

### 难点3:启动链的连环崩溃

第一阶段的上机经历是十连崩:System.Security.Cryptography 的 OpenSSL shim 在 dlsym 失败后 abort(表象为 SIGSYS+SIGABRT 且无日志),修法是分发 OpenHarmony .NET Runtime fork 自带的 musl-arm64 libcrypto.so/libssl.so.3;appspawn 环境无 HOME/XDG_*,GetFolderPath 塌到根目录 EACCES,需在启动最前显式设置四个环境变量;libpixieditor.so 携带 15 个 NetSecurityNative_* 未定义符号,OHOS 无对应库导致重定位失败,须把 GSS PAL stubs 编进 AOT 链接(RTLD_GLOBAL 预载在 musl 闭包解析下无效);XAML populate 在 Icon="/Images/favicon.ico" 处因缺 IPlatformIconLoader 中途抛出,表象竟是 Content 为 null 的"空跑"。启动链难点的共性是崩溃点与病灶距离很远,faultlog(hidumper 取 cppcrash 栈)加 Logger.Sink(AvaLog 直通 hilog)是唯二可靠的定位通道。

### 难点4:单表面下的弹窗与手势语义

没有多窗口,弹窗只能变成窗口内 overlay,由此派生一整族问题:模态弹窗不可见却吞输入即应用假死(需 Address != 0 防御加外点关闭兜底);弹窗的 Avalonia 命中测试依赖合成器 readback,而 overlay 弹窗的 ComboBox 下拉子树永远不进 readback,平台层必须接管弹窗输入(visual 树递归命中 + 直通 IInputManager.ProcessInput);输入回调里又禁止同步调用 readback(GetVisualAt 会死锁 UI 线程)。手势侧,双指捏合与单指绘画共享同一触点流,首指 pending-press 保持窗(难点三所述 110ms/9px)是消除"捏合误涂抹"的关键;保持窗内快点击的 begin/end 同栈投递又会被 click 处理器忽略,最终以"begin 立即、end 经 Dispatcher 分离一拍"的队列语义收口。这一族修复的共同原则是:渲染叠序与输入路由必须同源,任何一层单独调整都会制造"看得见点不着"的双错位。

### 难点5:序列化写读不对称毁掉恢复链路

真机回归曾暴露一个 P0:打开任何已保存文档,图层面板空、画布空白。四层插桩后定位到壳层 OhosObjectFormatter 的两个写读不对称:枚举写侧用 64 位紧凑编码而桌面链路是 int32,读回类型变 Byte,下游 value is int 判定不命中,精确拆箱抛 InvalidCastException,中断整个导入动作批次;SerializedObject 语义与桌面 object[]{id,data} 不一致,矢量 shape 与画笔属性全部变成死数据。修复是写侧对齐 D2 编码、读侧归一化 byte/sbyte/short 到 Int32(顺带救活已写坏的存量 autosave)。这个难点的特殊之处在于桌面 JIT 全绿掩盖一切——只有设备端 AOT 加设备端序列化往返才触发,因此真机回归必须是文档"保存→恢复"全链路,而不能只验证新建与绘制。

▍六、编译、安装与启动

构建分两段:先交叉发布 NativeAOT 产物,再打 HAP。第一段按 PORTING.md 记录的发布命令:

bash
cd /Users/andy/OHOSDotnet/PixiEditor/src/PixiEditor.OpenHarmony
export PATH="$HOME/.dotnet:$PATH"

dotnet publish -c ReleaseNoUpdate -r linux-musl-arm64 -p:OpenHarmonyTarget=true \
-p:OpenHarmonyApiLevel=26 -p:OpenHarmonyAbi=arm64-v8a \
-p:OpenHarmonySdkRoot=$HOME/OHOSdotnet/ohos-sdk -p:StripSymbols=false \
-p:JsonSerializerIsReflectionEnabledByDefault=true



产物与随行 native 库(libSkiaSharp 等)放入 entry/libs/arm64-v8a/ 后,第二段在工程目录构建 HAP:

bash
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk

hvigorw --mode module -p module=entry assembleHap --no-daemon


签名产物位于:

text
entry/build/default/outputs/default/entry-default-signed.hap


连接设备后安装并启动:

bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airpixi.app


两条纪律来自实证:替换 so 后必须清 entry/build 产物再部署(hvigor 增量不感知 so 变化);部署后核对 so 的 md5 与 mtime,防止增量假绿。

▍七、当前功能边界

按 PORTING.md 的 §9 标准审计与各轮真机回归记录,当前版本已覆盖:

- 完整像素编辑闭环:新建文档、画笔/矢量绘制、图层管理、调色板取色、缩放平移、.pixi 保存与恢复;
- ArkTS 原生菜单栏:全中文菜单树来自真实 Avalonia 命令,CanExecute 禁用态实时同步;
- 触控、鼠标、键盘、手写笔(压感/倾角/拒触)、滚轮与双指捏合的全模输入;
- 画布文本与 IME 端到端:软键盘拉起、组合输入、完成键收起、图层落字;
- 系统文件选择器打开/另存为、沙箱 staging 与 writeback 回写;
- 自由多窗:FLOATING 形态无系统标题栏、singleton 单实例与 onNewWant 复用;
- 系统分享送达(ShareKit 面板 + 图库落图)、暗黑主题钉死、空载零绘制;
- 应用内剪贴板文本与图像、跨应用文本读取(HasType 门控免权限噪音)。

尚未覆盖或规划中的能力:

- GPU 渲染路径:Avalonia 与 Drawie 的 GPU 互操作未在平台层实现,当前全部走软件渲染;
- 跨应用剪贴板图片读取:READ_PASTEBOARD 为受限权限,普通应用无法获得(平台限制,PORTING.md 明确记录);
- 触屏拖拽的完成语义:触屏拖放目前依赖取消路径收尾,体验专项待立项(坑119 修复后的遗留记录);
- 上游扩展运行时:Wasmtime 保持懒构造,未展开设备端验证;
- LGPL 合规通道:LGPL 条款下的重链接机制尚未建立(PORTING.md 遗留清单在列)。

因此,当前版本更准确的定位是"在鸿蒙平板与 2in1 设备上完成全功能闭环的轻像素编辑器移植版":S1-S14 审计项全部销项,剩余边界集中在 GPU 加速与平台受限能力。

▍八、总结

AirPixi 的意义超出了一款像素编辑器:它验证了"无运行时可依赖的 C#/XAML 桌面应用"在鸿蒙上的完整落地路径——NativeAOT 解决运行时缺位,自研 Avalonia 平台层解决窗口与输入缺位,软件渲染解决 GPU 互操作缺位,三层都有可复用的结论。PORTING.md 里一百二十余个实证坑位与配套的方法论(行为签名验证、坐标三体系、探针位置决定可证伪范围)是这条路线最重要的沉淀。

后续工作的重心有三线:GPU 渲染路径的实现,让笔画性能从软渲的 20-25fps 再上一个台阶;拖拽体验与剩余交互专项的收口;以及把平台层(窗口、输入、IME、选择器)整理为可被其他 Avalonia 应用直接复用的独立层——当第二、第三个 Avalonia 应用走上这条路时,今天踩过的每一个坑都应该只需要被读取,而不需要被重新踩一遍。
楼主 8 天前
想参与讨论?登录 或 30 秒注册。