📦 Flutter 三方库 flutter_native_splash_ohos 是用于配置鸿蒙启动图并控制 Flutter 首帧的 package。在撤下已有启动图时,原有鸿蒙实现会删除仍被宿主引用的图片,导致 HAP 编译失败。我在原库基础上补齐了透明资源保留与自定义模块路径透传。
完整代码已提交 PR 至官方仓库 CPF-Flutter/fluttertpc_flutter_native_splash,有问题可以到仓库提 Issue,也可以来 鸿蒙系统Flutter开源库社区 交流。

前言

我在检查 Flutter 鸿蒙启动图的撤销流程时,发现命令提示成功,接着打包却报了资源缺失。为了弄清楚是哪一步出了问题,我用一个普通待办页面保留了原版与修复版两套工程,顺着错误检查图片和配置,最后在撤销函数里补上了透明图片与模块路径的处理。

修复后,工程能正常打包,待办页面在模拟器和真机上都能打开、勾选和筛选。补丁完成后,我又把原版源码和报错交给 DevEco Code CLI,请它复核调用关系和修复思路。下面记录我怎样找到缺失的资源,以及为什么最后保留了透明图片。

一、使用场景与问题复现

1.1 撤掉启动图,不该把工程改成不可编译

我复现的是开发者撤掉已有开屏图案的操作。比如改版时先下掉旧活动图,或者排查首屏过渡时暂时使用纯色背景,都需要生成器把工程恢复到可构建的状态。这里使用本地待办示例检查库的行为,场景来自配置与撤销启动图的实际操作。

开发者对 remove 子命令的预期很直接:命令跑完后图案撤下,工程依然能正常编译运行。实际表现却是终端打印了 Native splash complete、进程退出码为 0,紧接着执行 flutter build hap 时直接失败。问题发生在安装包生成之前,与运行期启动画面停留多久无关。

排查前提:CLI 退出码为 0 只说明 Dart 脚本走完了当前分支,宿主资源引用是否仍然闭合,最终要由鸿蒙构建期的 restool 资源编译器裁定。

在鸿蒙工程里,启动图展示横跨三个层次,各自职责不同:

  • 系统启动窗口:系统拉起 UIAbility 时读取 module.json5 中的 startWindowIcon 与 startWindowBackground,在引擎就绪前先绘制原生启动窗口。
  • 构建前 CLI 脚本:读取 flutter_native_splash.yaml,文件不存在时再读取 pubspec.yaml 中的配置,向鸿蒙 resources 目录写入亮暗色图片与背景色,并在 ArkTS 首页 Index.ets 中挂载引用该图片的 splashComponentBuilder()。
  • Flutter 运行期控制:页面在 Dart 层调用 FlutterNativeSplash.preserve 与 FlutterNativeSplash.remove,借助 deferFirstFrame / allowFirstFrame 控制首帧何时放行。

撤销命令可以替换掉前景图案的内容,但只要模块配置和 ArkTS 首页仍然保留着资源引用,就不能直接把磁盘上的同名媒体文件删空。图 1 把开发机撤销启动图、鸿蒙资源构建和应用首帧放行分开画出:先保留有效资源,再构建安装,最后进入待办页面。具体断链位置留到第三节结合源码展开。

开发机撤销启动图后构建安装并进入待办页面的分层流程

图1 撤销与启动

社区仓库已经具备启动图生成与运行期首帧控制能力,不需要另起炉灶重写新插件。我只补齐原包撤销链路上的缺口:保留同名有效透明资源,并沿用生成阶段配置的模块路径。应用侧继续使用原有的包名、flutter_native_splash 配置块以及 FlutterNativeSplash.preserve / FlutterNativeSplash.remove 接口。

核对情景执行操作原版表现补丁与验证
撤下已有开屏图案默认目录执行 create → removeCLI 返回 0,随后构建报 11211120写入 1×1 透明 PNG 保留引用,重新构建通过
主模块不叫 entry配置自定义模块与首页路径标签版拒收配置键;撤销时也丢失路径回迁白名单,并在 removeSplash 透传两项路径
重复执行撤销命令在同一工程连续运行两次 remove缺失状态无法自愈对比两次撤销后的全量文件哈希,确认幂等

表1 核对情景

默认 entry 模块用于完整构建和设备运行。自定义 splashModule 路径及重复撤销由开发机上的隔离脚本检查,记录文件字节、退出码和两次撤销后的哈希;自定义模块用例尚未单独构建成应用运行。

1.2 查库摸底与版本分层

我检查了 oh-Flutter 与 CPF-Flutter 两个组织的公开仓库和相关源码,选用已有的 CPF-Flutter/fluttertpc_flutter_native_splash 适配仓。上游 flutter_native_splash 由 Jon Hanson 开发,用来生成原生启动图和控制 Flutter 首帧;CPF-Flutter 的社区仓库在此基础上提供鸿蒙资源生成、首页组件配置及鸿蒙插件工程。这个包已经覆盖了我要用的生成能力,我继续检查它的撤销入口。

图 2 是 jonbhanson/flutter_native_splash 上游源码仓库的实际页面,包含包用途、lib、example 和 test 等目录。上游负责通用生成逻辑与公开接口,本文核对的鸿蒙资源处理来自前面的社区适配仓。故障基线使用社区已发布标签 2.4.7-ohos-1.0.0,另用 br_v2.4.8_ohos_dev 开发分支对照实现。

Jon Hanson维护的flutter_native_splash上游源码仓库

图2 上游源码

核对源码树时,包结构有一处容易踩坑的细节:该标签把鸿蒙端实现放在仓库子目录 ohos/ 下,内部 pubspec.yaml 声明的包名是 flutter_native_splash_ohos,版本号为 1.0.0。也就是说,仓库 Git 标签、Dart 子包名、子包版本号三者并不同名。在鸿蒙子包上执行撤销命令时,不能直接照搬上游主包写法 dart run flutter_native_splash:remove,而必须指定子包名运行 dart run flutter_native_splash_ohos:remove。

我同时拉取并比对了社区开发分支 br_v2.4.8_ohos_dev,主要看了两个地方:

  • 社区开发分支已解决的部分:已合并为单包目录结构,并把 harmony_module_json、harmony_index_ets 两个键补进了 _Parameter.all 参数白名单。
  • 社区开发分支仍存在的问题:removeSplash 构造 removeConfig 时依然漏传上述两个自定义路径键,且空图片配置进入 _applyImageHarmony 后依然会走到 deleteSync 删除分支。

我按依赖顺序处理了两项改动:

  1. 先回迁社区已有的白名单修正,让配置解析接受自定义模块与首页路径。
  2. 再修改撤销入口,将这两项路径继续传给生成流程,并提供透明图片保留资源引用。

🔍 我还检查了原生目录里的 FlutterNativeSplashPlugin.ets。这个原生类很短,里面只有平台版本查询。原因是鸿蒙启动图的主体逻辑并不在运行期 MethodChannel 插件类里——构建前由 Dart CLI 直接读写宿主资源与 Index.ets,运行期则直接走 Flutter 引擎内置的首帧延迟接口。本次编译中断出在构建前 CLI,去改 .ets 插件类补不回磁盘上被删掉的图片。

1.3 用同款日常页面保留原版故障

我准备了 baseline 与 patched 两套对照宿主,分别通过相对路径 path 依赖原版子包和补丁子包。两组工程的 Dart 页面、锁文件与构建参数保持一致,仅用不同的 bundleName 和桌面名称区分安装目标。示例页面做成日常可用的“日常待办清单”,支持新增事项、勾选完成以及按“全部 / 待处理 / 已完成”分段筛选,用来核验修复后应用能正常冷启动、放行首帧并响应交互。

页面内置五条内存初始待办,不发起网络请求或异步文件读写:main() 入口先调用 FlutterNativeSplash.preserve 留住首帧,页面在 initState() 中调用 FlutterNativeSplash.remove() 放行渲染。两组宿主使用同一张脚本生成的 32×32 纯色 logo.png 和完全一致的 flutter_native_splash.yaml 配置:

flutter_native_splash:
  android: false
  ios: false
  web: false
  color: "#e1f5fe"
  color_dark: "#042a49"
  image: logo.png
  image_dark: logo.png

复现原版故障时,我按固定顺序执行了四步操作:

  1. 将宿主 module.json5 的 startWindowIcon 预先指向专用资源 $media:splash,桌面应用图标保留为独立的 $media:icon,互不混用。
  2. 运行 dart run flutter_native_splash_ohos:create,确认亮色与暗色媒体目录均生成 splash.png,Index.ets 写入启动组件。
  3. 运行 dart run flutter_native_splash_ohos:remove,观察到命令以退出码 0 结束,终端输出 [Harmony] Deleting splash.png。
  4. 在不改动任何生成文件的前提下,直接核查 module.json5、Index.ets 与 resources/*/media/ 目录的实际内容。

终端核查到的现场状态如图 3 所示:

撤销后宿主资源引用与媒体目录的终端核查

图3 配置对照

可以看到,module.json5 第 21 行依然写着 "startWindowIcon": "$media:splash",Index.ets 里也依然保留着 Image($r("app.media.splash")) 与 FlutterPage 的 splashScreenView 参数;唯独 base/media 和 dark/media 下的 splash.png 已经双双消失。我保留了 CLI 运行后的这些文件,直接用它们继续构建。

二、修复前后的构建与运行结果

2.1 构建结果与设备运行

🐛 保持上述 CLI 产物不变,我先对两组工程分别执行 flutter build hap --debug --no-codesign。原版在 :entry:default@CompileResource 阶段中断,补丁版生成无签名 HAP,用于模拟器运行。真机使用另外配置调试签名后构建的安装包,具体过程放在本节后半部分。

原版撤销启动图后在终端构建HAP失败

图4 原版中断

补丁版应用在模拟器冷启动后勾选待办并切换筛选

图5 修后运行

图 4 是早期取证时的原版构建失败过程,图 3、图 6 来自同一批终端记录。在待办宿主复验中,我用当前页面代码重新执行两组构建,原版仍报同一错误,补丁版生成的新 HAP 用于 Pura X View 上的冷启动、勾选与筛选录制(图 5、图 7)。原版没有可安装包,因此这里比较资源产物与构建结果,没有启动耗时数据。

检查项原版 baseline补丁版 patched
默认路径 create / remove两条命令均退出 0两条命令均退出 0
撤销后的亮 / 暗 splash.png文件被直接删除生成 1×1 全透明 PNG
模块配置与首页组件引用仍指向已缺失的 splash同名资源文件保持有效
flutter build hap 构建结果CompileResource 报 11211120 退出成功生成可安装 HAP 包
Pura X View 模拟器实测无可安装 HAP冷启动进入待办页,勾选与筛选正常
畅享 100 Pro Max 真机实测无可安装 HAP调试签名安装后冷启动,勾选与筛选正常

表2 结果对照

原版构建失败的完整终端输出见图 6。鸿蒙资源编译工具 restool 给出了明确的拦截原因:

原版构建报错节选(出处:CompileResource 阶段 hvigor / restool 终端输出):
Error Code: 11211120
Error: Resource Pack Error
Error Message: The resource reference '$media:splash' is not defined.

原版撤销后CompileResource报告媒体资源未定义

图6 编译报错

Hvigor 随即以退出码 255 终止构建。错误发生在安装之前,排查要回到 CLI 改过的图片与资源引用。待办页面的 Dart 逻辑此时还没有机会运行。

✅ 补丁组按完全相同的 create → remove → build hap 顺序执行后产出安装包,安装到 Pura X View 模拟器并从干净桌面冷启动,直接进入“日常待办清单”。勾选第三条事项使完成进度变为 3 / 5 时的界面状态如图 7 所示(随后切换“待处理”筛选隐藏已完成项的连续过程见图 5 动图):

Pura X View模拟器中的修复后日常待办页面

图7 模拟界面

同一次冷启动与点击交互期间,通过 hdc hilog 采集到的 SPLASH_AUDIT 诊断日志如下:

11-11 19:19:49.299  3721  3721 W A00000/om.example.splashremovepatched/XComFlutterOHOS_Native: flutter settings log message: SPLASH_AUDIT dartStarted group=patched
11-11 19:19:49.927  3721  3721 W A00000/om.example.splashremovepatched/XComFlutterOHOS_Native: flutter settings log message: SPLASH_AUDIT splashRemoved group=patched count=5
11-11 19:19:50.638  3721  3721 W A00000/om.example.splashremovepatched/XComFlutterOHOS_Native: flutter settings log message: SPLASH_AUDIT firstFrameRasterized group=patched
11-11 19:19:51.041  3721  3721 W A00000/om.example.splashremovepatched/XComFlutterOHOS_Native: flutter settings log message: SPLASH_AUDIT toggleItem group=patched done=true

四行日志依次对应 Dart 入口执行、initState() 确认 5 条内存初始待办就绪后调用 FlutterNativeSplash.remove()、首帧光栅化完成以及用户勾选待办项。它们用来核对运行期生命周期顺序,不单独拆算系统启动窗口动画时长。

📱 接着,我把同一套补丁工程复制到纯英文目录,重新执行生成与撤销命令,在 DevEco Studio 中配置本地调试签名,构建 entry-default-signed.hap,再通过 USB 安装到华为畅享 100 Pro Max。

应用从真机桌面冷启动后进入“日常待办清单”。勾选第三条事项后,完成进度变为 3 / 5;切到“待处理”时,已完成项从列表中隐藏。图 8 是勾选后的 DevEco Studio Screen Mirroring 投屏画面。

DevEco投屏窗口中的华为畅享100ProMax真机实测

图8 真机实测

真机进程的 SPLASH_AUDIT 日志也记录了入口执行、启动遮罩释放、首帧光栅化和勾选操作:

11-12 11:29:30.979 60221 60221 W A00000/om.example.splashremovepatched/XComFlutterOHOS_Native: flutter settings log message: SPLASH_AUDIT dartStarted group=patched
11-12 11:29:31.502 60221 60221 W A00000/om.example.splashremovepatched/XComFlutterOHOS_Native: flutter settings log message: SPLASH_AUDIT splashRemoved group=patched count=5
11-12 11:29:32.193 60221 60221 W A00000/om.example.splashremovepatched/XComFlutterOHOS_Native: flutter settings log message: SPLASH_AUDIT firstFrameRasterized group=patched
11-12 11:29:33.948 60221 60221 W A00000/om.example.splashremovepatched/XComFlutterOHOS_Native: flutter settings log message: SPLASH_AUDIT toggleItem group=patched done=true

2.2 解码透明像素,再检查重复撤销

检查撤销产物时,我先确认图片存在,再解码读取像素。损坏的 PNG 会影响资源编译,残留的彩色 Logo 则意味着图案还在。我用原包已有的 image 依赖编写了 verify_png.dart,依次检查解码是否成功、宽高是否为 1×1,以及唯一像素的 alpha 是否为 0。任一条件不满足,脚本都会抛出异常。

默认 entry 与自定义 splashModule 两组隔离工程各含亮、暗两张 splash.png,四张文件解码输出全部为 width: 1, height: 1, alpha: 0, valid: true。与此同时,隔离回归脚本还重点核对了四类不应被误伤的宿主资产:

  • 桌面图标资源:media/icon.png 字节哈希在生成与撤销前后保持不变。
  • 无关业务媒体:同目录下的其他业务图片未被改动或误删。
  • 测试目录文件:ohosTest 等非目标目录未受波及。
  • 无关颜色配置:color.json 中除启动窗口底色外的其余颜色键值完整保留,且连续执行两次 remove 后全工程文件哈希完全一致。

四张图片都通过了解码检查。我尚未在设备上演示系统深浅色模式的实时切换,这里的亮暗结论只涉及两套资源文件。

2.3 验证环境

环境项实际版本与条件
社区 Flutter / Dart3.41.9+ohos-1.0.2 / 3.11.5
DevEco Studio26.0.0.865
HarmonyOS SDK / API26.0.0.105(API 26)
DevEco Code CLI0.3.0-stable(--agent ask)
复核诊断模型deepseek/deepseek-flash
实测模拟器 / 镜像Pura X View 模板 / 镜像 7.0.0.107(--no-codesign 免签)
实测真机 / 固件版本华为畅享 100 Pro Max(YEW-AL80)/ YEW-AL80 7.0.0.100(SP9C00E100R9P4)
真机连接与签名条件USB 直连(型号 YEW-AL80),Release 系统 + 本地调试签名
模拟器与真机系统基线OpenHarmony-7.0.0.105(API 26,arm64-v8a)

表3 验证环境

表中的两种签名条件对应各自的安装包:模拟器使用 --no-codesign 构建,真机使用本地调试签名。证书与私钥保存在开发机用户目录,不随公开示例提交。

DevEco Code CLI 在开发机上读取附件做静态分析,不参与应用运行。本文设备验证覆盖 HarmonyOS 端,Android、iOS、Web 尚未运行回归。

三、源码根因、CLI 复核与最小修复

3.1 删掉了图片,两处引用仍然保留

顺着 11211120 报错指向的 $media:splash,我沿着 lib/cli_commands.dart 与 lib/harmony.dart 梳理出原版的完整调用关系(见图 9)。

原版撤销的资源缺失与补丁处理对照

图9 撤销对照

原版 removeSplash 并没有为各平台单独编写一套逆向卸载逻辑,而是构造一份只含默认白底 #ffffff、黑底 #000000 和平台开关的 removeConfig Map,直接复用 _createSplashByConfig(removeConfig)。这套“用空图案配置重跑一遍生成流程”的思路在鸿蒙分支依次踩中了三个环节:

  1. 只读取名,不改模块配置:_generateHarmonyConfiguration 解析 module.json5,从首个 ability 的 startWindowIcon 提取出资源名 splash,但不会把该字段改回其他图标。
  2. 空路径直接触发删文件:亮色和暗色各调用一次 _applyImageHarmony;由于 removeConfig 中没有传图片路径,imagePath 为 null,代码直接进入删除循环把两套目录下的 splash.png 物理删除。
  3. 首页组件检测已有定义后跳过清理:末尾的 _configSplashPage 发现 Index.ets 中已经存在首次生成注入的 splashComponentBuilder(),便跳过修改并照常打印更新完成日志,页面里的 $r("app.media.splash") 原封不动留了下来。

原版 lib/harmony.dart 中把“未传前景图”直接当成“删除同名资源文件”的两个完整函数如下:

/// 按亮色或暗色模板更新前景图。
void _applyImageHarmony({String? imagePath, bool dark = false}) {
  String fileName = '${_flavorHelper._harmonySplashImageName}.png';
  final templates = _getAssociatedHarmonyTemplates(dark: dark);
  if (imagePath == null) {
    // 原版把缺省图片路径当作删除指令,宿主引用不会在这里同步撤回。
    for (final template in templates) {
      _deleteImageHarmony(template: template, fileName: fileName);
    }
  } else {
    print(
      '[Harmony] Creating ${dark ? 'dark mode ' : 'default '}'
      '${fileName.split('.')[0]} images',
    );

    final image = decodeImage(File(imagePath).readAsBytesSync());
    if (image == null) {
      // 解码失败时停止生成,避免把不可读文件继续写入宿主资源目录。
      print('The file $imagePath could not be read.');
      exit(1);
    }

    _saveImageHarmony(
        templates: templates,
        image: image,
        fileName: fileName,
        harmonyResFolder: _flavorHelper.harmonyResFolder);
  }
}

void _deleteImageHarmony({
  required _HarmonyIconTemplate template,
  required fileName,
}) {
  final file = File(
    '${_flavorHelper.harmonyResFolder}${template.directoryName}/$fileName',
  );
  if (file.existsSync()) {
    print('[Harmony] Deleting $fileName');
    file.deleteSync(recursive: true);
  }
}

我找到的原因是:模块与首页两处引用都还在,唯独被指向的媒体文件被删了。资源编译器找不到这些引用对应的文件,于是在打包阶段报错。

3.2 把原版材料交给 DevEco Code CLI 复核

在人工定位并验证补丁可正常打出 HAP 之后,我单独整理出一个干净的只读输入目录,向 DevEco Code CLI 提供了五份原版材料:

  • prompt.txt:限定只读分析边界、禁止修改文件或执行命令,并要求单列未提供源码与未验证项。
  • cli_commands.dart:标签 2.4.7-ohos-1.0.0 原版入口源码(不含任何人工补丁)。
  • harmony.dart:标签原版鸿蒙端资源与首页模板处理源码。
  • compiler-error.txt:CompileResource 阶段真实的 11211120 报错节选。
  • scenario.txt:工程目录条件与 create → remove → build hap 复现步骤记录。

我在提示词里区分了 CLI remove 与运行期 remove(),也给出了两项模块路径配置键,请工具沿这些线索检查调用关系。因此,这次使用的是给定范围的静态复核。工具是否读到了相关源码、建议能否对应实际报错,都需要再逐项核对。

第一次调用 CLI 时,我从外部工作目录传入跨目录附件路径,收到 File attachment path is outside the allowed roots,进程退出 1。于是我进入附件目录,通过 --dir "$PWD" 指定项目根,再传相对文件名。可变长参数 --file 放在命令最后,避免提问文本被当成路径。

当时没有逐字保存完整命令行参数,下面按已确认参数整理为等价复现命令。--pure 表示不启用外部插件,只读范围由 --agent ask 与提示词共同限定。

deveco run "请只读诊断附件中的故障,按 prompt.txt 的限制输出。" \
  --pure \
  --agent ask \
  --model deepseek/deepseek-flash \
  --format json \
  --dir "$PWD" \
  --file prompt.txt cli_commands.dart harmony.dart compiler-error.txt scenario.txt

第二次调用返回退出码 0,复核前后五份输入附件的 SHA-256 校验值一致(见图 10)。

终端核对DevEco Code版本、输入哈希与可见答复

图10 CLI诊断

模型给出的调用链梳理与人工判断一致:它准确串起了 module.json5 只读取名、imagePath == null 触发 deleteSync 以及 Index.ets 跳过清理仍打印更新日志这三步,同时把默认路径的资源缺失与自定义路径的参数遗漏拆成了两个独立问题,没有混为一谈。

3.3 为什么保留人工补丁

虽然根因判断一致,模型给出的参考修复代码与我预先写好的人工补丁在工程取舍上仍有明显差异(见表 4):

对比维度模型原始示例最终保留的人工补丁
占位图片内容4×4 不透明纯白 / 纯黑 PNG1×1 四通道全透明 PNG(alpha = 0)
源码改动范围同时修改两份文件,给生成函数新增 remove 参数仅修改 lib/cli_commands.dart 单文件,保持原函数签名
其他平台与日志省略了部分其他平台分支,建议顺手改日志文案完整保留其他平台代码与原有日志输出

表4 方案取舍

我没有直接照搬模型示例,主要基于下面三点具体考量。

3.3.1 图像内容:1×1 透明像素与密度缩放

模型示例生成的是不透明的白图与黑图——因为在 removeConfig 默认把亮暗底色重置为 #ffffff 和 #000000 的前提下,前景与背景恰好同色。我没有编译运行模型那套代码,因此不断言它会出现色块;但用全透明像素表达“无前景图案”更直接:底色完全交给 start_window_background 决定,前景图只负责维持资源名合法。

调用 Image(width: 1, height: 1, numChannels: 4) 创建四通道图像时,默认像素全为零(alpha = 0)。我还专门核对了 harmony.dart 里的密度缩放算式:pixelDensity 固定为 4,代入 image.width * template.pixelDensity ~/ 4 后结果为 1 × 4 ÷ 4 = 1,不会被整除截断成 0 宽高的非法图片。

3.3.2 注入位置:沿用鸿蒙图片键缩小改动面

模型方案为上层生成函数和鸿蒙生成函数都增加了撤销标记,需要沿调用链修改参数。

我利用原库已有的 image_harmony 与 image_dark_harmony 配置键,在撤销入口将它们指向临时透明 PNG。后面的鸿蒙生成流程就能沿原路径读取和写入图片,其他平台也不会读取这两个键,修改因此集中在 lib/cli_commands.dart 中。

3.3.3 清理时机:同步解码后再删临时目录

最终保留在 完整补丁文件 中的 removeSplash 实现如下,摘录只补充中文注释,执行逻辑与该版本一致:

/// 撤下前景图案,并恢复库约定的默认亮暗背景。
void removeSplash({
  required String? path,
  required String? flavor,
}) {
  print("Restoring Flutter's default native splash screen...");
  final config = getConfig(configFile: path, flavor: flavor);

  final removeConfig = <String, dynamic>{
    _Parameter.color: '#ffffff',
    _Parameter.darkColor: '#000000'
  };

  if (config.containsKey(_Parameter.android)) {
    removeConfig[_Parameter.android] = config[_Parameter.android];
  }

  if (config.containsKey(_Parameter.ios)) {
    removeConfig[_Parameter.ios] = config[_Parameter.ios];
  }

  if (config.containsKey(_Parameter.web)) {
    removeConfig[_Parameter.web] = config[_Parameter.web];
  }

  if (config.containsKey(_Parameter.harmony)) {
    removeConfig[_Parameter.harmony] = config[_Parameter.harmony];
  }

  // 撤销也必须使用生成时指定的鸿蒙工程路径。
  for (final parameter in [
    _Parameter.harmonyModuleJson,
    _Parameter.harmonyIndexEts,
  ]) {
    if (config.containsKey(parameter)) {
      removeConfig[parameter] = config[parameter];
    }
  }

  // 沿用 iOS plist 文件清单,保留其他平台已有的配置传递行为。
  if (config.containsKey(_Parameter.plistFiles)) {
    removeConfig[_Parameter.plistFiles] = config[_Parameter.plistFiles];
  }
  Directory? temporaryDirectory;
  try {
    if (config[_Parameter.harmony] != false && Directory('ohos').existsSync()) {
      // module 和 Index 仍引用启动图;透明占位保留资源有效性并移除图案。
      temporaryDirectory = Directory.systemTemp.createTempSync('native-splash-');
      final transparentImage = File('${temporaryDirectory.path}/transparent.png');
      transparentImage.writeAsBytesSync(
        encodePng(Image(width: 1, height: 1, numChannels: 4)),
      );
      removeConfig[_Parameter.imageHarmony] = transparentImage.path;
      removeConfig[_Parameter.darkImageHarmony] = transparentImage.path;
    }
    _createSplashByConfig(removeConfig);
  } finally {
    // 图像输入在生成函数中同步解码;异步写入使用已解码的像素,不再读该文件。
    temporaryDirectory?.deleteSync(recursive: true);
  }
}

这里有一个清理时机需要核对。底层保存图片时使用 Isolate.run 异步写目标文件,而撤销函数返回前就会删除临时目录。我检查了前面的图片处理函数:源图已通过同步读文件完成解码,传给后续任务的是内存中的图像对象。

因此,异步写目标文件时不再读取临时 PNG,清理这个临时文件不会中断对像素的处理。这个结论依赖当前实现的读入顺序;如果以后将解码也移入异步任务,就要一起调整临时文件的生命周期。

3.4 自定义模块路径的两处遗漏

当主模块不叫 entry 时,我会在启动图配置中填写 harmony_module_json 与 harmony_index_ets,分别指向模块配置和首页源码。生成与撤销都要使用这两项路径。源码解析时先检查参数白名单,撤销时还要将路径传给 removeConfig,两个位置缺一不可。

版本配置解析自定义目录撤销
2.4.7-ohos-1.0.0 标签白名单拒收两项路径键解析阶段退出
br_v2.4.8_ohos_dev 开发分支已接受路径键漏传路径,退回默认 entry 目录失败
fix/ohos-remove-splash-resource 修复分支回迁白名单修正透传路径,写入同名透明资源

表5 路径对照

主机隔离验证脚本分别在稳定标签、开发分支与补丁版上跑了这套自定义模块用例:稳定标签在第一关就被 _yamlToMap 拦下;开发分支过了第一关,但在执行 remove 时退回默认路径失败;合入两份补丁后,生成与撤销均准确落在 splashModule 目录并退出 0。

四、踩坑细节与适用边界

4.1 分清终端撤销与首帧放行

排查启动屏问题时,最容易混淆的是名字里都带 remove 的两个入口。表 6 把两者的阶段和现象拆开对照:

对比项构建前 CLI 撤销运行期首帧放行
调用方式dart run flutter_native_splash_ohos:removeFlutterNativeSplash.remove()
执行时机开发机打包前修改工程文件应用在设备上启动后完成初始化时
作用对象宿主 resources 下的图片、颜色与 Index.etsFlutter 引擎的首帧渲染门控
典型故障现象CompileResource 报 11211120 无法生成 HAP应用能安装打开,但画面一直停在开屏页进不了首页

表6 两类入口

本文补丁修复的是左侧的构建期资源缺失,不能替代右侧业务代码里的 FlutterNativeSplash.remove() 调用。此外,当前待办示例采用内存同步列表,没有模拟复杂的网络超时或异步初始化异常兜底。

4.2 不猜用户原来的模块配置

表面上看,另一种修法是在执行 remove 时把 module.json5 的 startWindowIcon 改回 $media:icon,再从 Index.ets 抠掉 splashComponentBuilder()。我在评估后放弃了这条路线,原因有三点:

  • 原始图标名不可知:原库在 create 阶段没有备份原始配置,假如项目原本用的是 $media:launch_logo,硬改回 $media:icon 反而会破坏原有配置。
  • 首页代码可能已被二次编辑:首次生成后开发者往往会在 Index.ets 里补充生命周期或状态代码,靠字符串正则反向删除组件极易误伤业务代码。
  • 共用资源名存在覆盖风险:如果业务页面也直接引用了 app.media.splash,create 会覆盖它,remove 后它也会变成透明图。

💡 配置建议:始终让 startWindowIcon 指向专用的 $media:splash,与桌面图标 $media:icon 和应用内业务图片严格分开;若需彻底回到首次运行 create 之前的文件状态,应通过 Git 版本控制还原。

另外,原库仅读取 module.json5 中首个 ability 配置,且要求鸿蒙亮暗背景色成对提供;这些属于原包既有的设计边界,多 Ability 工程接入前需单独核对。

4.3 进程退出与平台范围

⚠️ removeSplash 中的 try / finally 能覆盖正常返回和常规 Dart 异常,但原库内部有多处校验失败时直接调用 exit(1) 强退进程。一旦在创建临时目录之后触发这类强退,finally 块不会执行,系统临时目录下可能残留一个极小的 native-splash-* 文件夹。为保持最小改动面,本次未重构全库的退出机制。

模型答复把 _createBackground 等未提供的实现列为待核对项。我随后检查了完整源码中的背景生成,再用构建和设备运行结果确认补丁的效果。其他平台仍保留原有实现,适用范围以第二节的环境说明为准。

五、结论与可直接复用的建议

5.1 在现有项目中接入修复版

需要撤下已有鸿蒙启动图时,这个补丁让撤销后的模块配置、首页组件和媒体资源继续对应。应用可以恢复纯色启动背景,再按原来的方式构建 HAP。原有生成配置和运行期首帧接口都可以继续使用。

社区源码位于文首仓库。本文验证的补丁与同款页面放在个人 Fork 的 fix/ohos-remove-splash-resource 分支,鸿蒙子包在 ohos 目录。下面的依赖固定到这份已验证源码,适合需要立即接入本文修复的项目。在应用的 pubspec.yaml 中配置:

dependencies:
  flutter_native_splash_ohos:
    git:
      url: https://atomgit.com/abaoge/fluttertpc_flutter_native_splash.git
      ref: 34c91f8d0f492d06573d8b18398c4e46405bfde9
      path: ohos

如果项目已经声明了同名依赖,替换原条目即可。Git 依赖中的子目录决定读取哪份 pubspec.yaml,因此不要将此处的包名改成上游主包名。安装依赖后,在已有鸿蒙宿主的应用根目录执行撤销:

flutter pub get
dart run flutter_native_splash_ohos:remove

命令直接修改项目里的启动资源。保留现有 flutter_native_splash.yaml,生成器会从中读取平台开关与模块路径;没有独立配置文件时,才读取 pubspec.yaml 内的配置块。默认的 entry 模块沿用原配置即可,撤销后的亮、暗背景分别恢复为白色和黑色。

5.2 接入时需要确认的选项

如果主模块使用自定义目录,请在原来的启动图配置块中同时填写 harmony_module_json 和 harmony_index_ets,分别指向模块声明与 ArkTS 首页。路径相对于执行命令的应用根目录解析。补丁会把这两项配置从撤销入口继续传下去,防止生成时写进自定义目录、撤销时又返回 entry 目录。

我建议给启动前景图使用独立资源名。在 module.json5 中,让 startWindowIcon 指向专用的 $media:splash,桌面应用图标仍使用自己的资源。撤销后前景图会成为透明 PNG,业务页面若共用同名媒体资源,也会看到透明图。因此接入前要先确认资源的使用位置。

原配置中的平台开关仍然生效。若只处理鸿蒙启动图,可沿用第一节配置中的 Android、iOS 和 Web 关闭选项。将 harmony 设置为 false 会跳过鸿蒙生成分支,这种配置也不会应用本文的透明资源处理。

应用运行期继续使用 FlutterNativeSplash.preserve 和 FlutterNativeSplash.remove。本例在入口保留首帧,在待办数据就绪后放行。已有项目如果有网络或文件初始化,应按自己的完成、超时和失败处理路径释放首帧;命令行撤销启动图不会替应用执行这些调用。

5.3 使用范围与完整工程

我验证了 API 26 下的默认模块构建,以及 Pura X View 模拟器与华为畅享 100 Pro Max 真机交互,自定义路径和重复撤销通过开发机脚本检查。深浅色模式实时切换及其他平台还没有运行回归。这个修复保留了现有资源引用和首页组件,若要完整恢复首次生成前的文件,应使用项目自己的版本记录。

完整的依赖、工具链、原版故障复现与模拟器操作步骤放在 diagnostic_example/README.md。其中 diagnostic_example 是文章使用的待办页面,仓库原有 example 则保留上游示例。读者可以直接把库接进自己的应用,需要对照问题时再打开同款工程。

代码保留 Jon Hanson 的 MIT 许可证,以及鸿蒙贡献文件中的 Apache-2.0 声明。白名单修正来自社区已有实现,透明资源与撤销路径处理是本文新增部分,源码记录中分别保留了修改归属。

小结

撤下启动图后,工程能继续打包,待办页面也恢复了正常使用。排查这个问题让我更留意生成工具的收尾动作:删除一张图片前,还要看模块与页面是否继续引用它。DevEco Code 帮我重新梳理了调用链,而检查图片、构建和打开应用,让我确认保留透明资源足以解决这里的引用缺失。

参考资料

  1. CPF-Flutter/fluttertpc_flutter_native_splash 发布标签 2.4.7-ohos-1.0.0
  2. CPF-Flutter/fluttertpc_flutter_native_splash 开发分支 br_v2.4.8_ohos_dev
  3. HarmonyOS 资源编译工具 restool 指南
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐