Air 系列应用交流/AirFx(from Friction)|AirFx · 鸿蒙 PC 适配全记录

AirFx(from Friction)|AirFx · 鸿蒙 PC 适配全记录

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

■ 应用:AirFx(from Friction)(AirFx)
■ 定位:Friction 移植,2D 动画制作
■ 上游:Friction(2D 动画) | 路线:Qt5 5.15+QPA | 版本:1.2.0
■ 开源: https://gitcode.com/Andy88/Harmony-Friction
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917616600016095653
■ 官网(免费下载): https://www.shunidc.cn/

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

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

适配开源地址:https://gitcode.com/Andy88/Harmony-Friction

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

▍一、为什么要适配 AirFx(Friction)

Friction 是 friction2d 社区维护的开源 2D 动画软件(GPL-3.0),血统可以追溯到 enve——一个以"程序化矢量动画"为核心理念的 MG 动画工具。它用 Qt5 Widgets 做界面,画布由 Skia GPU 自绘,视频编解码交给 FFmpeg 4.x,表达式引擎依赖 QJSEngine。对动效设计师来说,这类工具的价值在于关键帧、缓动、矢量路径与表达式绑定可以精确到帧;它恰好也是移植难度最高的一类应用:GUI、GPU 渲染、音频、多媒体、脚本引擎全部叠在一个进程里。

AirFx 是 Friction 1.4.0 深度定制分支(上游源码 friction-src/,基于 2.5d-main)向 HarmonyOS 的原生移植工程。路线在动手前就写进了 PORTING.md 的选型结论:Qt5.15.16 + 自研 ohos QPA 插件,复用 AirDraw 工程已验证的 Qt 树与 QPA(libqohos.so),ArkTS 薄壳加 XComponent 承载窗口,libfriction_core.so 单库装载全部核心。移植全程按十八个会话推进并逐条留档在 PORTING.md 状态日志中,从首会话 M0→M2 主界面上屏,到第十八会话 MatePad Mini 全项回归,每一步都带 hilog 与截图证据。

工程概况:包名 com.airfx.app,版本 1.0.0,deviceTypes 为 tablet 与 2in1,目标 ABI arm64-v8a(Qt/FFmpeg/Skia 全套 .so 经 entry/libs/arm64-v8a 整目录通道随 HAP 打包)。module.json5 仅声明 INTERNET 权限,另注册了 .friction 与 .ae 两种文件类型的 FileOpen skills,并带 backup 扩展。验证设备覆盖 MateBook Pro、MatePad 11.5 与 MatePad Mini 三端。

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

Friction 原生面向 X11/Wayland 桌面:main() 里跑 Qt 事件循环,QMenuBar 挂原生窗口,QAudioOutput 出声,模态对话框靠 Qt 自己的窗口管理。把它"编成动态库"离可用的动画软件还差一个生态位的距离。AirFx 的边界声明很直接:能进入当前可运行产物的能力,必须在真机上以可交互行为验收(PORTING.md 第 5 节 S1~S14 对照表逐项打勾);进入不了的不伪装。

层次 上游 Friction(桌面 Linux) AirFx 鸿蒙侧当前方案
进程入口 main() + Qt 事件循环 Friction_Init(argc, argv) 库形态,RenderThread 驱动(补丁 P1)
窗口系统 X11/Wayland + xcb QPA 自研 ohos QPA(libqohos.so)+ XComponent + 对话框子窗化
菜单系统 QMenuBar 原生渲染 S1 菜单 ArkTS 化:FxMenuBar bindMenu + triggerMenuAction 回调
音频输出 Qt Multimedia QAudioOutput OHAudio shim:OH_AudioRenderer push 模型(补丁 P4)
MCP 服务 /tmp/friction_mcp.sock socket 移到 QStandardPaths CacheLocation 沙箱(补丁 P5)
外部进程 QProcess 拉起 agent OHOS 无 fork 通道,启动即拒绝并 UI 提示(补丁 P6)
内存分配器 Linux 无条件链接 gperftools/tcmalloc musl 不适用,memorychecker.cpp 编译期摘除(补丁 P3)
文件选择 QFileDialog 原生 S7 系统选择器桥接:打开/多选/保存加后缀切换

补丁清单 P1~P12 与风险登记册 R1~R10 在 PORTING.md 中逐条登记,每个补丁都指向明确的上游文件与目的,这张边界表是它们的执行结果。

▍三、鸿蒙版的整体架构

整体是"ArkTS 薄壳 — NAPI 桥 — dlopen 重型栈"三层。libfxbridge.so 只链系统库;首个触碰 Qt 的后台线程成为 Qt 主线程,按拓扑序 dlopen 整个依赖树,再 dlsym 取用 Friction_Init/Step/OnPointerEvent 等 C API。这一线程模型同时是 appfreeze 看门狗的防线——JS 线程永不承载重型初始化。

text
EntryAbility(Stage 模型)
├── want 参数/文件 URI 捕获 → GlobalState → Index 消费
├── SubWindowManager:多实例对话框子窗化(NativeWindow 直投通道)
├── FxMenuBar:菜单 ArkTS 化(L1/L2/L2Flat 三级,triggerMenuAction 下发)
├── DialogHost / DialogPage:对话框 Overlay 承载与几何去抖
└── Index.ets + XComponent
└── napi_init.cpp(libfxbridge.so,只链系统库)
├── RenderThreadMain:dlopen 拓扑序清单 → Qt 主线程
├── libfriction_core.so(27.4 MB 单库:core+ui+app 静态并入)
│ ├── Qt Widgets 全套界面(zh_CN 汉化)
│ ├── Skia m84 GL 画布(libskia.a,GLES/EGL)
│ ├── QJSEngine 表达式(Qt5Qml,V4 禁 JIT 走 interpreter)
│ └── FFmpeg 4.4.5 编解码(checkFFmpeg 闸门 avutil 56 通过)
├── OHAudio shim(audiohandler 接口替身)
└── 输入注入:QMouseEvent/QTabletEvent/QTouchEvent/IME 桥


主要目录如下(与 PORTING.md 第 6 节及仓库真实结构一致):

text
AirFx/
├── PORTING.md # 唯一权威计划与状态档案(18 个会话日志)
├── friction-src/ # Friction 上游源码(GPL-3.0)
├── thirdparty_src/ # 第三方依赖源码(不入库项见 README)
└── entry/
├── libs/arm64-v8a/ # Qt5 全家/FFmpeg/fontconfig 等整目录打包通道
└── src/main/
├── module.json5 # INTERNET、.ae/.friction FileOpen skills
├── ets/
│ ├── pages/Index.ets # XComponent 宿主与 want 消费
│ ├── components/FxMenuBar.ets # 菜单 ArkTS 化
│ ├── components/DialogHost.ets # 对话框 Overlay 与几何去抖
│ ├── utils/SubWindowManager.ets # 对话框子窗管理
│ └── common/GlobalState.ets
├── resources/rawfile/friction/ # share 数据(主题/图标/预设)
└── cpp/
├── napi_init.cpp # 薄 NAPI 桥(libfxbridge.so)
├── friction_patches/ # 上游补丁 + friction_ohos_entry.cpp
├── friction_build/ # CMake 交叉构建树(libfriction_core.so)
├── ohos_deps/ # Qt5/QPA 依赖、ffmpeg、预编译库
├── qt_platform/ohos/ # 自研 QPA(libqohos.so)
├── skia-ohos/ # Skia m84 交叉构建
├── build_friction_ohos.sh # 主构建脚本
└── deploy_friction_core.sh # 增量部署管线


▍四、核心适配过程

### 1. 依赖矩阵:复用四件、新建三件、摘除一件

PORTING.md 依赖矩阵把依赖分成三类处理。复用 AirDraw 的 Qt5.15.16 树(linux-ohos-clang mkspecs 已配好)、ohos QPA、quazip 与 zlib/libpng/freetype 基础库;新建 Qt5Qml(qtdeclarative 5.15.16 交叉编译,V4 JIT 关闭走 interpreter,规避 arm64 JIT 风险)、FFmpeg 4.4.5 五库(--disable-everything 按需裁剪,avutil 56 落在 checkFFmpeg 闸门区间)与 Skia。Skia 是最硬的一仗:friction2d fork 锚定 m84(commit b551a388),GN 工具链对 OHOS 无现成 target 且慢网无法 git-sync-deps,工程自研 extract_skia_sources.py 从 gn/*.gni 提取源清单、按平台黑名单裁剪,最终纯 CMake 产出 21 MB libskia.a。gperftools 则在 OHOS 分支整体摘除(P2/P3/P8)。

### 2. 应用改库与 RenderThread 线程模型

补丁 P1 把 main() 改成 extern "C" int Friction_Init(int argc, const char** argv)(AIRFX_AS_LIB 守卫),删除 Windows VEH 等桌面专属尾段。napi_init.cpp 严格按 AirDraw 模型:dlopen 与 QApplication 构造全部发生在 native 后台线程(RenderThread 即 Qt 主线程),ArkTS 侧通过 Friction_Step 泵驱动事件循环。这条纪律直接对应风险 R6——重型初始化上 JS 线程必然触发 appfreeze 看门狗。十二个首会话根因档案里,"文件级静态 QPixmap 早于 QApplication 构造导致 dlopen 即 SIGABRT"与"模态 exec 在无内建事件循环线程死锁"两条教训,都写成了工程通则:Qt 值类型禁文件级静态,friction 代码里一切模态 exec 遇到即守卫。

### 3. ohos QPA 的 GL 与输入接线

QPA 层解决两件事。其一是渲染:首会话定位到整窗黑屏根因是 GL 消费者(QOpenGLWidget/Skia 的 GrContext)与 QPA compose blit 抢同一个 EGL 窗口 surface(EGL_BAD_ACCESS 0x3002 循环),根修为 QOhosGLContext 统一绑 16x16 独立 pbuffer、swapBuffers 置空,GL 消费者画 FBO、QPA raster compose 独占窗口 surface。其二是输入:触摸、鼠标、滚轮、手写笔经 XComponent 回调注入 QMouseEvent/QTabletEvent/QTouchEvent,IME 走 QPA inputContext 通路;第十七、十八会话又补上了命中预判与重臂机制,让系统键盘的弹出时机与 Qt 焦点控件严格对齐。

### 4. 菜单与对话框的 ArkTS 化

Qt 原生菜单在触屏上的可达性不达标,S1 把 QMenuBar 全量导出为 JSON,FxMenuBar.ets 用 bindMenu(CustomBuilder) 变体渲染三级菜单,点击经 fx.triggerMenuAction 回灌 Qt 侧。第十六会话根修了两个深层缺陷:部分设备上经 .enabled(item.e!==0) 标禁的菜单项不参与触摸测试,导致 fx.triggerMenuAction 从未执行;以及对话框"复位重开"状态跳变留下隐形 MenuWrapper 叠挂吞点击——最终以无 isShow 的 bindMenu 变体加 Builder 参数内同步取数根治,并沉淀了一条 ArkTS 硬规则:@Builder 体内只允许 UI 组件语法,普通语句直接编译错误。大对话框则经 SubWindowManager 子窗化,DialogHost 对 show 期几何连发做尾沿去抖,首帧不再丢失。

### 5. 部署管线与 ELF 合规

deploy_friction_core.sh 把"构建到真机"收敛成一条命令:ninja 增量构建 → cp -L 解引用拷贝 → patch_needed.py(必须显式传 libdir 参数)处理 NEEDED 依赖 → strip_libc_needed.py 等长替换 musl 禁忌符号(ffmpeg 的 libc.so NEEDED 换 libz.so)→ devecocli build → HAP md5 校验 → hdc 装机。签名环节的教训也留了档:自建 CA 材料配对验证全过仍被拒,正解是 devecocli signature generate --force 自动申请 IDE debug 材料。entry/libs/arm64-v8a 整目录通道配合 strip:false 保证 Qt/FFmpeg 动态库原样进 HAP。

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

### 难点1:GL 画布与窗口合成抢 surface

现象:主界面整窗黑屏,hilog 里 eglMakeCurrent 0x3002 EGL_BAD_ACCESS 循环。根因是 Qt 的 GL 消费者与 QPA 的 compose blit context 共用一个 EGL 窗口 surface,违反"单 surface 单 context"铁律。Krita 端口的既有经验是干脆关 GL 走 raster,但 Friction 画布必须 GPU。工程最终在 QPA 层翻案:QOhosGLContext 给所有 GL 消费者绑 16x16 独立 pbuffer,swapBuffers no-op,GL 内容画进 FBO,QPA raster compose 独占窗口 surface 负责最终上屏。修后 compose 流转正常,既保住 GPU 画布又不与合成器互踩。

### 难点2:resize 链上缺失的几何回报

现象:自由窗口拖小时内容跟随、拖大后内容停在旧宽 1429px,右侧整柱纯黑。MIN-GEO 探针抓到实锤:应然几何 963x560,QWindow/QWidget 全停在 711x495。根因在 qtbase:QWindow::setGeometry 在已有平台窗时只调平台层、不更新内部逻辑几何,全靠 QPA 回报 handleGeometryChange 驱动布局链——而这套 QPA 从未回报;Qt 对"窗口大于新 screen 自动夹小"解释了拖小时看似正常的假象。修复是 QOhosWindow::setGeometry 末尾补标准 QPA 义务 handleGeometryChange。同轮还把窗口最小尺寸用 setWindowLimits({minWidth:640, minHeight:460}) 钉住(API 名没有 setMinimumWindowSize,踩坑留档),小窗下 minimumSizeHint=589x430 的右缘控件不再被裁。

### 难点3:文本四联症——字体、IME 与键盘的连环坑

现象(第十七会话集中爆发):文字工具输入后画布无字形、填充面板滑条叠盖属性区、键盘收起后点击输入框不重弹、工具栏任意按钮误弹键盘。四案三根因:画布无字形是 Skia 走 fontconfig 而 OHOS 无 /etc/fonts/fonts.conf,沙箱零字体(Qt 控件走 QT_QPA_FONTDIR 不受影响,界面正常反而造成长期假象),修复在 dlopen 前写沙箱 fontconfig/fonts.conf 并设 FONTCONFIG_FILE;滑条叠盖是 isVisible() 不感知滚动视口裁剪,修复按 visibleRegion() 门控 GL 合成缓存;键盘误弹是命中预判沿父链递归爬进了共享容器的 SliderEdit,根修为严格匹配——只认编辑控件自身或一级视口,不做任何回溯。同轮沉淀的方法论(UIDUMP 探针、QTimer::singleShot(0) 在 Step 泵里从不执行)都进了 PORTING.md。

### 难点4:启动竞态的悬空 std::function

现象:冷启动或点新建偶发 SIGSEGV,processdump 栈恒定收敛在 RunOnCoreThread → DrainCoreTasks → Friction_Step,SEGV@0x2。根因是跨线程调用桥 RunOnCoreThread 的队列任务以 [&] 捕获函数栈局部变量(fn/done/doneMtx/doneCv)——150ms/800ms 超时返回后栈帧销毁,滞留任务被 Drain 执行即悬空。修复引入 CoreCallState,三个调用点(ImeCommitText/ImeKey/GetMenuJson)全部改 make_shared 全值捕获。修后六次冷启动零崩溃。这类竞态无法靠回归测试兜底,靠的是把 processdump 取证通道标准化后自然复现实锤。

### 难点5:菜单遍历通道与系统模态的冲突

现象:菜单全遍历自测通道跑到「文件/链接」必卡死。根因是该菜单项拉起系统文件选择器——系统级模态窗口,Qt 侧的 activeModalWidget/activePopupWidget 自愈清理管不到。修复三连:activePopupWidget close、6 秒看门狗 STUCK 自报告、"链接"进排除清单。修后带工程遍历 PC 端 210 action、mini 端 200 action 全程 0 崩溃 0 挂死;配合 24 变体 .ae 生成器(manifest 往返逐项比对),mini 240 文件批量 246/246 全过、单文件往返 mini avg 85ms / PC 75.4ms——这条自测通道后来成为每轮回归的标准动作。

▍六、编译、安装与启动

构建分两段:先交叉编译 native 栈,再打 HAP。仓库已内置全部脚本:

bash
cd AirFx/entry/src/main/cpp


./build_friction_ohos.sh


./deploy_friction_core.sh


随后是标准 hvigor 构建(可在工程根目录直接执行):

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


签名使用 devecocli signature generate --force 申请的 IDE debug 材料(自建 CA 材料实测不被接受,教训留档于 PORTING.md 首会话)。产物位于 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.airfx.app


PORTING.md 记载的真机验证覆盖 MateBook Pro(2in1)、MatePad 11.5 与 MatePad Mini 三端,含 .ae 批量往返、菜单全遍历、播放链与对话框链的量化回归数据,均可按状态日志中的证据目录复核。

▍七、当前功能边界

按 PORTING.md 验收对照表(S1~S14)与状态日志,当前已覆盖:

- M0~M2 全链:依赖七件套交叉编译、libfriction_core.so、NAPI 桥、ArkTS 壳、真机主界面(12 项中文菜单/双排工具栏/属性面板/画布/时间轴/状态栏);
- 菜单 ArkTS 化与汉化(S1/S2)、触控与鼠标双模(S3)、系统选择器桥接含保存后缀切换(S7)、暗黑主题(S11)、新建场景对话框全链与模态死锁根修(S12);
- 性能验收(S13):.ae 往返 mini avg 85ms / PC 75.4ms,240 文件批量零失败;
- 上游对齐(S14 部分):24 变体全特性 .ae 往返一致,空工程/带工程菜单遍历 199/210/200 action 三端零崩溃;
- 文本链(第十七/十八会话):画布字形(fontconfig 根修)、精确选中、IME 通路、键盘重弹与误弹根修;
- 自由窗口 resize 链(handleGeometryChange 根修)、拖拽接入、OHAudio 音频、对话框子窗化、MCP server 沙箱 socket。

尚未覆盖或规划中的能力(均有验收表或日志依据):S4 手写笔压感、S6 光标、S8 文件夹授权、S9 剪贴板验证、S10 碰一碰未过验;S5 三源捏合缩放的轴回调与捏合换算已编入代码,但 uinput 无法验轴,需 PC 真触控板确认;S14 中 shader 类效果仍跳过,v10 生成器 pos 键写侧污染在遗留清单待查;vtracer(位图转矢量)与 onnxruntime(AI 深度估计)两个可选 QLibrary 模块按计划延后。

因此,当前版本更准确的定位是"Friction 鸿蒙原生移植的可交互基础版":从构建体系到画布渲染、输入、音频、对话框的完整骨架已经过三端量化回归,笔压、光标、shader 对齐等深水区按验收表逐项推进。

▍八、总结

AirFx 的十八个会话给出了一条重型 Qt 应用的完整移植路径:先用依赖矩阵把复用与新建切开,再以"应用改库 + RenderThread 泵"规避看门狗,用自研 QPA 的 pbuffer 方案调和 GL 消费者与窗口合成,把菜单、对话框、选择器这些桌面交互逐一翻译成鸿蒙原语,最后用菜单遍历与 .ae 批量生成器把回归变成可量化的常规动作。PORTING.md 里每条根因、每个探针、每条方法论沉淀,本身就是这轮移植的第二产出。

后续重心在验收表的空格里:笔压与光标补齐输入深水区,shader 效果兑现 S14 的最后一格,文件夹授权与剪贴板打通文件与数据通道。工程已有的证据链纪律——每项改动真机部署、hilog 与截图双通道取证——会继续作为这些工作的验收底线。
楼主 8 天前
想参与讨论?登录 或 30 秒注册。