ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

codemagic_app_preview鸿蒙适配实战:平台通道与CI/CD集成

codemagic_app_preview鸿蒙适配实战:平台通道与CI/CD集成 1. 为什么要给 codemagic_app_preview 做鸿蒙适配先交代下背景。我所在的小组一直用 Flutter 做跨端业务CI/CD 走的是 Codemagic产物自动化预览用的是 codemagic_app_preview 这个三方库。它的作用很直接每次构建跑完后自动生成 App 内各页面的截屏/录屏预览附带设备型号、系统版本、构建号、git commit 等信息团队成员不用装包、不用自己跑模拟器打开链接就能看这次改版到底变成什么样了。这个库在工作流里的地位很微妙平时没人夸它但一旦坏掉UI 还原度、样式回归、状态变更确认全都卡壳。所以当初团队决定探索 OpenHarmony 平台支持时我第一个想到的不是业务代码怎么跑而是这套预览链路能不能跟着一起迁过去。如果 CI/CD 构建跑完预览生成不了整个审查流程就断了一截。真正的难点在于codemagic_app_preview 的设计初衷是纯 Android/iOS 双端它内部通过平台通道拉起原生截图能力再结合 Codemagic 的环境变量和 API 完成上传。OpenHarmony 上 Flutter 的运行时、插件通道、文件存储路径、权限模型都跟 Android 有差异直接拿来用必然跑不通。而且三方库官方大概率不会主动做鸿蒙适配至少短期没有计划。这个活只能自己干。这篇文章我会按我实际操作的顺序来写先讲清楚 codemagic_app_preview 的工作原理再拆 OpenHarmony 与 Android 的工程差异然后给出可落地的适配方案和完整 CI/CD 集成步骤最后把适配过程中踩过的坑和排查思路一并列出来。内容偏向实战适合已经跑通 Flutter 鸿蒙构建、正准备把工程化能力补上的团队参考。2. 先拆库codemagic_app_preview 到底在做什么动手改代码之前必须先搞清楚这个库的运行机制。我建议读源码的时候不要一头扎进方法实现里先看它的 pubspec 声明和示例代码理解它在编译期和运行期各干了哪些事。2.1 编译期的元数据注入codemagic_app_preview 在编译阶段做了一件事把当前构建的元数据写入到工程里这些元数据包括构建号、git commit hash、分支名、Codemagic 构建页面链接等。它依赖 Codemagic 提供的一组环境变量比如CM_BUILD_ID、CM_COMMIT、CM_BRANCH、CM_BUILD_URL。如果你在本地直接跑这些环境变量不存在库会自动退回到测试模式不会真的执行截图上传逻辑。这块逻辑相对独立鸿蒙适配时不需要动。因为 OpenHarmony 的 Flutter 工程同样走 Dart 层编译环境变量是平台无关的只要在 CI 环境里把这些变量带进来元数据注入照常生效。2.2 运行期的自动预览执行运行期逻辑是重头戏。当 App 以 preview 模式启动时——一般通过--dart-defineCODEMAGIC_PREVIEWtrue之类的参数触发——库里会创建一条独立的预览执行链检测当前运行环境是否满足截图条件逐个打开配置好的页面或路由通过平台通道调用原生截图/录屏能力将生成的图片/视频暂存到临时目录通过 HTTP 上传到 Codemagic 的预览 API这里最关键的就是平台通道调用。在 Android 上它调用的是MediaProjection或PixelCopy在 iOS 上走的是UIWindow的截图 API。OpenHarmony 上这些 API 一个都不存在所以适配工作的核心就是重写这条原生链路。2.3 为什么说它的架构其实不难适配说实话这个库的代码结构算清晰的。它把策展逻辑放在 Dart 层资产采集放在原生层交付上传放在 Dart 层。这种分层方式对鸿蒙适配非常友好Dart 层几乎所有代码可以原样保留真正需要动的只有原生端的那层 Channel Handler。你的任务可以理解成把原来监听 MethodChannel 的两个原生实现Android 一个、iOS 一个换成 OpenHarmony 上一个然后把临时文件目录路径、权限申请方式、截图 API 适配一下。工作量没有想象中那么大难的地方在于别把链路整体打破。提示如果你只想要最小可用版本可以先不实现录屏只做截屏因为 MediaProjection 的录屏在鸿蒙上的权限和后台限制比 Android 更严格先跑通截屏后续再补录屏迭代成本更低。3. OpenHarmony 上 Flutter 工程的三个致命差异在没有真正跑起来之前我一度觉得这种适配就是换几个 API 的事。实际踩下来发现至少有三个差异会影响整个技术方案需要提前想清楚。3.1 引擎与插件通道的差异OpenHarmony 的 Flutter 引擎目前是社区方案它跟 Android 的引擎实现是两条线。最直接的体现就是MethodChannel的注册方式Android 上写在MainActivity.kt里OpenHarmony 上则要写在 ArkTS 侧的EntryAbility或自定义的FlutterAbility子类里。也就是说你没法直接把项目的android目录下的代码搬过来需要在ohos目录下新建一套 Channel Handler。这个逻辑对做过 Flutter 双端插件开发的同学来说很好理解本来就有一个抽象 Channel 名称现在只需要保证平台侧实现换了Channel 名称和参数协议保持不变Dart 代码一行都不用改。// 在 ArkTS 侧注册自定义 Channel let channel MethodChannel(codemagic_app_preview, StandardMethodCodec.INSTANCE) channel.setMethodCallHandler((call, result) { if (call.method captureScreenshot) { // 调用鸿蒙截图能力 capturePage(result) } else if (call.method saveToTemporaryFile) { // 写入缓存目录 saveTempFile(call.arguments as string, result) } })3.2 存储路径与沙箱限制Android 的临时文件目录大家都很熟悉context.cacheDir随便用。OpenHarmony 的沙箱机制更接近 iOSApp 只能在自己分配到的沙箱目录里自由读写。这个差异在适配时很容易踩坑。因为 codemagic_app_preview 在 Dart 层会通过path_provider获取getTemporaryDirectory()然后往这个目录里写截图文件。如果你在鸿蒙上没做任何处理path_provider是可以正常拿到一个沙箱路径的但如果你的原生截图代码用的是另一个目录——比如appContext.cacheDir的实际值跟 Dart 层拿到的不一致——上传时就会报文件不存在。解决办法统一以 Dart 层拿到的路径为准原生层不要自行拼接。最稳妥的做法是原生截图完成之后把二进制字节返回给 Dart 层由 Dart 层负责写入临时目录。这样路径问题就从根上消失了。3.3 权限模型差异Android 的截屏权限在 API 29 之后基本靠MediaProjection配合用户弹窗授权iOS 则直接不能用公共 API 截取非自身 App 的内容。OpenHarmony 的截屏 API 走的是screenshot模块它同样需要用户授权和系统能力声明。这里最需要注意的是OpenHarmony 对截屏时机的管控相对严格如果页面还没渲染完成就去截很可能拿到一张黑屏或者白屏。所以适配时建议在截图前做一次主动延迟或者等首帧回调完成后再触发。我这里调试时踩过一次黑屏排了半天不是权限问题而是截图调用时机太早最后加了一个帧回调等待问题就消失了。4. 鸿蒙适配的完整落地步骤下面这部分是全文的核心我把适配过程拆成几个阶段来说明每个阶段都给出了我实际上用的做法以及为什么这么做的原因。4.1 环境准备与基线确认在做任何代码改动前先把环境基线锁住。不同版本的 Flutter 鸿蒙 SDK、不同版本的 OpenHarmony API代码写法会有细微差异基线不定后面改代码就是猜谜游戏。我用的环境参考如下按我实际操作时的版本你的环境可能有更新组件版本/说明Flutter SDKflutter 3.7.12 或更高版本OpenHarmony Flutter SDK社区维护的 flutter_ohos 分支HarmonyOS SDK / DevEco StudioAPI 9 及以上Codemagic标准 macOS 构建集群codemagic_app_preview1.x 版本注意codemagic_app_preview 这个库本身是一个 Dart 包对鸿蒙并不感知你在pubspec.yaml里正常声明就行。它依赖的path_provider、http这些包鸿蒙平台下都有对应的实现暂时不需要额外替换。关键是原生层的适配这一步需要把android目录下的MainActivity里的 Channel Handler 逻辑用 ArkTS 重写一遍放到ohos目录对应的入口类里。4.2 采集层的鸿蒙原生实现采集层主要负责两个能力页面截图和文件保存。我建议分两个 Channel 方法实现不要混在一起。这样后面如果单独调试生成逻辑可以只调截图不碰文件保存。截图这块OpenHarmony 提供了ohos.screenshot模块可以实现屏捕获。实现思路参考下面的伪代码import screenshot from ohos.screenshot async function captureScreenshot(): PromiseArrayBuffer { // 申请截屏能力 let options new screenshot.ScreenshotOptions() options.width 1080 options.height 2400 let result await screenshot.takeScreenshot(options) return result.pixelMap ? await result.pixelMap.getImageData(0, 0, 1080, 2400) : new ArrayBuffer(0) }实际编码的时候有几个细节要确认一是ScreenshotOptions的属性命名和 Android 的MediaProjection完全不同不要照着 Android 的代码硬翻译二是截图返回的PixelMap需要转成 JPEG/PNG 字节流Dart 层才方便保存三是权限声明要在module.json5里加上对应权限条目否则运行期直接报错。文件保存这一步我推荐走 Dart 层。原生截图拿到字节流后直接把Uint8List返回给 DartDart 再用File.writeAsBytes写入getTemporaryDirectory()拿到的路径。这样可以完全避开沙箱路径不一致的问题。// 在 Dart 层封装截图逻辑 class PreviewCapture { static const _channel MethodChannel(codemagic_app_preview); static FutureFile capture(BuildContext context) async { final bytes await _channel.invokeMethodUint8List(captureScreenshot); final dir await getTemporaryDirectory(); final file File(${dir.path}/preview_${DateTime.now().millisecondsSinceEpoch}.png); await file.writeAsBytes(bytes!); return file; } }4.3 上传逻辑的保持与变更上传逻辑在 codemagic_app_preview 中已经封装得很好了核心是通过 Codemagic 的 API 上传截图文件并关联到对应的构建记录。鸿蒙适配不需要修改上传协议也不需要改 API 地址。唯一要确认的是上传时机和网络权限。OpenHarmony 的网络权限需要在module.json5里显式声明ohos.permission.INTERNET。很多第一次接触鸿蒙开发的同事经常漏掉这个导致 Dart 层 HTTP 请求一直失败还以为是适配代码写错了。另外一点上传的时候建议增加失败重试。OpenHarmony 的 network 栈在某些模拟器或开发板上表现不太稳定超时概率比 Android 高。原库可能只做了一次重试你可以根据需要改成三次指数退避。# module.json5 中声明网络权限节选 requestPermissions: [ { name: ohos.permission.INTERNET } ]4.4 触发条件的本地模拟验证适配完成后不要直接推到 CI 去验证先本地跑一遍。codemagic_app_preview 本地跑的时候不会进入正式模式所以你需要手动模拟环境变量和触发条件。最简单的方式写一个内部入口页面开发模式下点一个按钮直接调用刚才封装的PreviewCapture.capture()看截图生成和临时文件写入是否正常。确认这个链路通了再考虑环境变量映射和 CI 集成。我本地验证时用的命令大致长这样flutter run --dart-defineCODEMAGIC_PREVIEWtrue \ --dart-defineCM_BUILD_IDlocal-test \ --dart-defineCM_COMMITabc123 \ --dart-defineCM_BRANCHdevelop \ --dart-defineCM_BUILD_URLhttp://local.test/build/1跑起来之后用 DevEco Studio 的日志过滤器盯着 Channel 的调用记录确认captureScreenshot被正确触发然后看临时目录里是不是真的生成了 PNG 文件。5. CI/CD 集成把预览链路接到 Codemagic 全流程本地验证通过后剩下的就是把这条链路接到真正的 CI 流程里。这一步比纯代码适配更考验对构建系统的理解因为你要同时考虑构建触发时机、产物传递、元数据映射等多方面因素。5.1 构建脚本里的环境变量映射Codemagic 原生提供了一组环境变量供 codemagic_app_preview 使用。OpenHarmony 构建流程中这些变量同样存在但有些变量名可能因为构建机镜像差异而不完整。我建议在构建脚本里做一个显式校验缺失的先给默认值避免空指针。下面是我实际用的脚本片段放在 Codemagic 的scripts阶段#!/bin/bash export CM_BUILD_ID${CM_BUILD_ID:-unknown} export CM_COMMIT${CM_COMMIT:-unknown} export CM_BRANCH${CM_BRANCH:-unknown} export CM_BUILD_URL${CM_BUILD_URL:-https://codemagic.io/} flutter build hap --release \ --dart-defineCODEMAGIC_PREVIEWtrue \ --dart-defineCM_BUILD_ID$CM_BUILD_ID \ --dart-defineCM_COMMIT$CM_COMMIT \ --dart-defineCM_BRANCH$CM_BRANCH \ --dart-defineCM_BUILD_URL$CM_BUILD_URL注意编译 targetOpenHarmony 下的产物是.hap包而不是 Android 的.apk。Codemagic 的构建机默认没有flutter build hap这个 target需要先确认构建机的 Flutter SDK 是否切换到了 OpenHarmony fork 版本。5.2 预览产物的留存与生命周期管理codemagic_app_preview 生成的截图最终会显示在 Codemagic 构建详情页的 Preview 标签里。这个图只反映构建产物实际运行时的 UI 状态所以它天然带有版本管理价值。我建议在保存截图时文件名里带上构建号和 commit hash比如preview_${CM_BUILD_ID}_${shortCommit}.png。这样即使构建详情页的关联信息丢失你也能从文件名反推出来源。另外OpenHarmony 设备上跑预览截图需要一台常驻的模拟器或真机。Codemagic 默认的 macOS 集群无法直接跑 HarmonyOS 模拟器所以有两种路径自建 Harbor 或使用华为云构建服务单独挂载鸿蒙模拟器在 Codemagic 构建机里用 Docker 驱动 OpenHarmony 容器化的模拟方案路径的选择取决于团队现有基础设施我这边选的是第二种容器方案的好处是构建节点可以复用 Codemagic 的编排逻辑不是从零搭一套。5.3 触发策略与失败重试机制预览截图这个环节对时间敏感。构建完成后的第一张截图最有价值因为这时候页面处于初始状态能直接反映冷启动后的 UI。如果产品复杂建议让预览流程跑的页面控制在三到五个核心页不要把所有路由都铺进去不然构建时间会拉长很多。我之前试过贪多把几十个页面全配进预览链路结果单次构建多了快六分钟团队怨声载道。后来改成只截核心转化路径的页面时间压缩到一分钟以内信息量反而更集中。失败重试也要设计好。OpenHarmony 设备偶尔会出现截图服务无响应的情况此时直接退出预览模式、让整个构建失败其实是合理的。因为预览链路失败说明构建产物有问题宁可失败重跑也不要让坏产物混进版本管理。6. 踩坑记录适配过程中最折磨人的五个问题适配过程中我遇到的问题远不止上面提到的那些这一节挑五个最有代表性的记录下来方便后来者参考。6.1path_provider在鸿蒙上获取到了空目录这个是我遇到的第一个问题。path_provider在 Android 上轻松返回 cache 路径但在 OpenHarmony 上某些版本拿到的是空字符串导致所有写入操作失败。排查链路是这样的先看 Dart 层的getTemporaryDirectory()返回值发现是null用MethodChannel手动调PathProviderPlugin的getStoragePath方法发现 ArkTS 侧的原生实现没有正确初始化导致 channel 返回 null解决方法有两种我最后选了第二种// 回退方案手动拼接沙箱路径 final dir Platform.isOpenHarmony ? Directory(/data/app/el2/100/base/${packageName}/cache) : await getTemporaryDirectory();这里不推荐手写绝对路径因为沙箱路径规则可能随系统版本变化。更好的做法是在原生侧通过自己的方法返回真实路径然后 Dart 层缓存起来。6.2 截屏黑屏问题黑屏的原因是 OpenHarmony 的截屏服务需要等待界面帧绘制完成如果你在onPageShow或路由跳转完成后立即截图大概率拿到的是一张还没渲染好的图。我的解决办法是加一个渲染完成的回调延时。不要用简单的Future.delayed因为低端设备上渲染时间不可控最好用 Flutter 的WidgetsBinding.instance.endOfFrame。Futurevoid waitForRaster() async { Completervoid completer Completer(); WidgetsBinding.instance.addPostFrameCallback((_) { completer.complete(); }); // 再额外等待一帧确保原生图层更新 await Future.delayed(const Duration(milliseconds: 200)); return completer.future; }6.3 平台通道注册冲突如果你的 App 里已经有一个插件注册了同名 Channel适配时会相互覆盖导致行为异常。排查方法很简单打开 DevEco Studio 的 Log过滤MethodChannel关键词看是否有两个注册记录指向同一个名称。解决方式是给 codemagic_app_preview 的鸿蒙适配单独起一个 Channel 名称比如codemagic_app_preview_ohos在 Dart 层做一层兼容转发。这样不会干扰其他插件。6.4 上传超时且无重试OpenHarmony 设备的网络栈在弱网环境下表现不太稳定。codemagic_app_preview 默认的 HTTP 超时时间是 30 秒但上传大图时偶尔会超。建议在 CI 环境里手动把上传超时调到 60 秒同时确认设备可以访问 Codemagic API 域名。final client http.Client(); final response await client .post(uri, body: bytes, headers: headers) .timeout(const Duration(seconds: 60));6.5 构建机找不到flutter build hapCodemagic 的镜像默认不带 OpenHarmony 的工具链需要你手动在构建脚本里拉取并切换到 Flutter 的 OpenHarmony fork 版本。这一步如果漏了构建会在很早的阶段就挂掉而且报错信息非常隐晦类似Target name hap not found。我的建议是把 SDK 切换动作放到单独的脚本步骤里并输出当前 Flutter 版本和分支方便定位问题。cd $FLUTTER_ROOT git clone https://gitee.com/openharmony-sig/flutter_flutter.git -b master . flutter doctor flutter --version7. 适配之后版本管理工作流有什么变化代码适配完成之后整个 CI/CD 预览链条最直接的变化是OpenHarmony 构建物也和 Android/iOS 一样具备可视化的验收抓手了。这对版本管理的影响是深远的但很多人只看表面没意识到更深层的价值。以前hap产物只有安装包大小、崩溃率这类冷数据UI 层面的改动全靠开发自测 截图手工贴到群里。现在每次构建都自带一组结构化预览图版本评审会议上可以直接对着图过需求开发、产品、测试三方看到的是同一份产物状态。另一个变化是构建历史变成了可视化档案。你翻半年前某个版本的预览图能直接知道当时 Tab 结构长什么样、主题色是什么、首页卡片排布如何。这个东西在快速迭代期价值巨大——它帮你把当时的 UI 到底什么样这个问题变成了秒查状态而不是翻代码历史。从工程化角度讲codemagic_app_preview 的鸿蒙适配真正补齐的是 OpenHarmony 在交付链路的可视性这一环上的短板。它不改变产物内容但它改变了团队感知产物状态的方式。8. 最佳实践组合与后续扩展方向适配工作告一段落之后我复盘了一下整个方案提炼出几个最佳实践组合分享给大家。8.1 最少改动原则能不改三个平台共用的 Dart 代码就不要改。我建议把鸿蒙适配集中在原生层和构建脚本层Dart 层只增加一个平台判断的 fallback。这样后续原库更新时可以很方便地合并上游改动。原库如果发布了新版本我的升级流程一般是先看 changelog确认 Dart 层是否有破坏性改动在分支上合并新版本跑本地验证确认无误后再把 ohos 原生层的代码同步到新版本对应的构建配置8.2 配置化驱动预览页面列表不建议把截图页面列表写死在代码里。更好的方式是通过--dart-define传一个路由名单进来这样不同团队可以各配各的预览范围不需要改公共代码。--dart-definePREVIEW_ROUTEShome,profile,cart,detailDart 层解析这个参数后动态构造路由列表逐页跳转截图。这比写死在main.dart里灵活得多也方便后续接入自动化 UI 巡检。8.3 持续监控与稳定性看板预览链路也会挂。我建议把截图成功率作为一个独立指标接入监控体系CI 构建完成后上报一项数据preview_success真/假。连续几次失败就触发告警避免预览链路长时间在无声无息中坏掉。这个动作看起来简单但价值很高。预览链路挂掉不会影响构建成功所以在没有监控的情况下极难察觉等团队发现的时候可能已经错过半个月的视觉回归数据了。8.4 后续扩展方向如果你想要更完整的鸿蒙工程化能力可以考虑在预览链路的基础上做两件事一是把截图对比接入自动化视觉回归。既然每次构建都有同一组页面的截图拿前后两次构建的截图做像素对比就能实现基本的 UI 回归检测。OpenHarmony 生态里这个能力相对空白做出来就是亮点。二是把预览元数据接入版本管理系统的 API。你可以在版本发布时自动把预览截图链接附到 release note 里让版本描述从一开始就是带图的而不是事后补文档。这两件事都是在现成链路上做的增量优化投入不大但对工程管理的帮助非常明显。9. 写在最后适配这件事给了我几个启示从决定适配 codemagic_app_preview 到完全跑通 OpenHarmony 预览链路整个周期大概花了两周其中正经写代码的时间只有三四天剩下的全在排查环境问题和理解系统差异。我个人最大的体会是鸿蒙适配的核心困难不在代码量而在思维转换。Android 上很多想当然的 API 和机制在 OpenHarmony 上都需要重新审视一遍。你用 Android 的惯性去写鸿蒙代码一定会有各种隐形问题反过来把它当成一个全新的平台用最笨的办法一点点验证反而顺利得多。另外一个体会是这种三方库的适配工作最怕的不是技术难点而是不知道边界在哪里。你要很清楚地知道哪些代码能复用、哪些必须重写、哪些可以通过环境配置绕过去。codemagic_app_preview 算是一个架构友好的例子因为它的 Dart 层和原生层分得很清楚给了我足够的操作空间。如果你的项目里要适配的库不是这种分层结构恐怕就得考虑在 Dart 层做整体模拟了。最后再分享一个小技巧适配过程中多做中间态验证。不要等所有代码写完了才跑测试每完成一个环节——比如截图原生层跑通、文件写入跑通、上传跑通——就单独验证一次。这样即使后面出了问题排查范围也极其有限。我这次能两周搞定一半功劳要记在这个习惯上。
返回列表