ARTICLE DETAIL

资讯详情

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

鸿蒙Flutter资源优化实战:asset_opt适配方案与踩坑记录

鸿蒙Flutter资源优化实战:asset_opt适配方案与踩坑记录 最近在给公司的鸿蒙版 Flutter 应用做包体治理本来以为资源优化这种事和 Android 差不多拿现成的工具链跑一遍就完事了。结果 asset_opt 这个在 Android 上跑得好好的三方库一碰到鸿蒙工程就各种别扭资源扫不到、压缩完的图不被原生侧识别、去重逻辑直接把我公共目录给误删了。断断续续踩了一周坑才把这条适配链路完全打通。这篇文章就把我的完整方案、改造细节和踩坑记录整理出来给正在搞鸿蒙 Flutter 资源优化的朋友一点实际参考。asset_opt 本身是个非常纯粹的 Flutter 资源优化工具核心做三件事图片压缩、重复资源去重、未引用资源清理。它的设计目标是让你在pubspec.yaml声明好资产清单后一条命令就能把工程里的图片资源统一压一遍顺便清掉那些早已不用的废资源最后再输出一份体积报告给你看效果。这套流程放在 Android 工程上很顺但放到鸿蒙上工程目录结构不同、资源落盘位置不同、图片解码兼容性也不同等于工具的一半逻辑都要重新对齐。1. 为什么非要在鸿蒙上复用 asset_opt先算清资源优化的账1.1 一个 HAP 里资源能占多狠先讲一个我实测的数字。当时我们那个业务型 AppRelease 模式打出来的 HAP 是 12.6MB其中纯资源文件图片、切图、图标、rawfile 里塞的配置文件占了 8.3MB超过 65%。而且这里面绝大多数是 PNG是设计稿直接导出的原图一张 512 的启动图就 1.2MB一套 tab 栏切图五个加起来也快 1.5MB。你可能会说8MB 也不大啊。但在鸿蒙应用分发场景里这个体积的代价是实打实的各大应用市场对 HAP 上传大小有硬性限制超过阈值要走特殊审核通道流程直接变长。用户下载时包体大小和转化率是强相关的每大 1MB下载转化率都肉眼可见地往下掉。渠道包、压测包、预置包分发时每多 1MB 都是白花花的带宽和存储成本。所以包体治理这件事不是“有空再优化”而是你上架前必须过的一道关。而资源文件作为体积大头自然是压缩的第一优先级。1.2 asset_opt 原本能干什么asset_opt 之所以让我愿意花时间去适配是因为它把资源优化这件事做得足够“自动化”。它的核心能力基本涵盖了日常资源治理的痛点能力说明对应价值图片压缩支持 PNG/JPEG/WebP 的体积优化可配置质量参数直接降低资源体积重复资源去重扫描工程中内容相同的图片合并为同一份引用清理设计交付时的重复切图未引用资源清理分析资产声明与实际引用关系找出无用资源及时清理历史遗留废图体积报告优化前后对比、各类资源占比统计量化治理效果便于汇报和持续跟踪构建前钩子可以挂到构建流程中自动执行保证每次出包都是瘦身后的状态这套能力放在 Android 上已经算成熟我的目标就是让它在鸿蒙上也能达到同样的自动化程度而不是每次发版前人工开一个脚本手动压一轮图。1.3 所谓“鸿蒙化”到底在改什么很多人一听到“鸿蒙化”就觉得是要把整个工具重写一遍或者去对接什么鸿蒙原生 SDK。其实 asset_opt 这种工具型 Flutter 库鸿蒙化适配的本质就一件事让工具链对齐鸿蒙工程的资源管理方式。具体拆解下来其实就三个层面的改造识别层让工具能判断出当前工程是鸿蒙工程并理解鸿蒙的目录结构。扫描层知道资源在鸿蒙工程里放在哪、以什么形式存在、哪些该扫哪些不该扫。输出层压缩和清理之后的结果要能被鸿蒙的构建系统正确识别并打进 HAP。只要这三层对齐了asset_opt 的底层压缩逻辑完全不用动。它本来就是个纯 Dart 实现的工具库压缩用系统命令行或自带编解码器处理逻辑和平台无关只要喂给对的路径它就能干活。2. 鸿蒙的资源体系与 Flutter assets 的落盘路径适配前必须搞清的三个位置2.1 HarmonyOS 工程的资源目录到底长什么样做适配的第一步是彻底搞懂鸿蒙工程的资源布局。HarmonyOS 的工程结构和 Android 完全是两套思路Android 的 res 目录按mipmap、drawable、values分目录而 HarmonyOS 是统一在模块下的resources目录里按类型再分 base 和 qualifiers比如base/、dark/、zh_CN/这种限定词目录。一个标准的鸿蒙工程资源相关的核心目录大概是这样的entry/src/main/ ├── module.json5 └── resources/ ├── base/ │ ├── element/ # 字符串、颜色、样式等值资源 │ ├── media/ # 图片、音频等媒体资源 │ └── rawfile/ # 原始文件打包时不处理直接放进 HAP ├── dark/ # 深色模式资源限定目录 ├── en_US/ # 英文语言限定目录 └── zh_CN/ # 中文语言限定目录这里面前三个目录是我们适配的重心base/media所有通过$media.xxx引用的图片资源都放这里。base/rawfile通过 rawfile 接口按文件名访问的原始文件Flutter 的 assets 最终会以这个形式进到 HAP。module.json5模块级配置文件里面会声明资源相关的能力。2.2 Flutter assets 在鸿蒙构建产物里的去向搞懂了鸿蒙工程的资源布局接下来要搞懂 Flutter assets 在鸿蒙构建流程中的落盘位置。在 Android 上Flutter 项目里assets/目录下的资源经过构建后会打包进 APK 的assets/flutter_assets/目录。而在鸿蒙上Flutter 引擎通过flutter_flutter适配层运行在 HarmonyOS 系统之上打包进 HAP 时Flutter 的资产会被放到resources/base/rawfile/flutter_assets/目录下。这里有个非常关键的细节如果你的 Flutter 工程里声明了 assets 目录构建后不会以单独文件的形式平铺在资源目录里而是整体放进flutter_assets/assets/子路径下。举个例子你在pubspec.yaml里这么声明flutter: assets: - assets/icons/ - assets/images/startup.png打完鸿蒙包之后在 HAP 解包后的文件系统里你会看到这样的落盘结果resources/base/rawfile/flutter_assets/ ├── asset_manifest.json ├── assets/ │ ├── icons/ │ │ ├── home.png │ │ └── mine.png │ └── images/ │ └── startup.png ├── fonts/ └── kernel_blob.bin这意味着 asset_opt 的适配逻辑里扫描 Flutter 声明的资产是一层扫描鸿蒙构建产物是另一层。你要优化的是 Flutter 工程里assets/下的源文件你要保证的是优化后的文件能在下一次构建时被正确带进rawfile。2.3 三个位置之间的关系一张对照表我在实际适配时把这三者的关系整理成了一张表写进了团队内部文档里也分享出来给你位置路径示例作用适配关注点Flutter 源资产assets/icons/home.png开发者维护的原始资源asset_opt 的主要扫描和优化对象鸿蒙工程资源entry/src/main/resources/base/media/鸿蒙原生侧使用的图片、媒体资源优化后如需同步原生资源要写回这里构建产物落盘resources/base/rawfile/flutter_assets/assets/HAP 包里 Flutter 资产的最终位置验证优化结果是否真正进入安装包搞清楚这三层适配思路就清晰了asset_opt 的入口路径要指向 Flutter 源资产目录扫描规则要排除构建产物目录否则会把上一轮的构建残留当成源资源同时输出机制要保证优化后的文件仍然保持在 Flutter 工程声明的 assets 路径下让鸿蒙构建流程能按原路径拾取。3. 核心改造从 Android 思维切到鸿蒙思维的四处代码3.1 第一处平台与工程类型识别asset_opt 原本判断工程类型的逻辑是检测是否存在android/或ios/目录这在跨端工程里很常见。放到鸿蒙工程里既没有android/也没有ios/判断逻辑直接走入了“未知平台”分支然后报错退出。适配的第一步就是让工具能识别鸿蒙工程。鸿蒙 Flutter 工程的标志性特征是工程根目录下存在ohos/目录或者由 DevEco Studio 生成的entry/目录。我的改造方案是加一个平台探测器enum FlutterPlatform { android, ios, ohos, unknown } FlutterPlatform detectPlatform(Directory projectRoot) { final entries projectRoot.listSync().map((e) e.path).join(\n); if (entries.contains(${Platform.pathSeparator}android)) { return FlutterPlatform.android; } if (entries.contains(${Platform.pathSeparator}ios)) { return FlutterPlatform.ios; } if (entries.contains(${Platform.pathSeparator}ohos)) { return FlutterPlatform.ohos; } return FlutterPlatform.unknown; }这里有一个很容易踩的细节不要用Platform.operatingSystem ohos这种运行时判断。因为 asset_opt 是跑在开发机上的工具开发机可能是 Windows、macOS 或 Linux运行时平台信息和目标工程平台没有任何关系。正确的做法是基于工程目录结构做静态探测和你在 Android 上判断有没有android/目录是同一个思路。3.2 第二处资源扫描规则切换asset_opt 原本的扫描逻辑是这样的读取pubspec.yaml里的flutter.assets声明然后逐一扫描声明的目录和文件收集汇总后进入压缩流程。这个逻辑在鸿蒙上基本可以复用但有三个需要调整的地方排除构建产物目录。很多 Flutter 工程会把build/、.dart_tool/这些目录放在工程根下如果 assets 声明涉及目录扫描注意把build/里鸿蒙构建的中间产物排除掉否则会把上一轮构建生成的压缩图片当成源文件反复压缩导致质量下降。增加对 rawfile 的只读扫描。为了生成未引用资源清理建议工具还需要知道鸿蒙原生侧resources/base/media和rawfile里有哪些文件在被使用。这里只需要做“读取”和“分析”不需要修改。路径分隔符处理。鸿蒙构建系统在 Windows 和 macOS 上表现一致但在 DevEco Studio 的某些版本里rawfile的静态资源路径可能用反斜杠记录。扫描时做一次\到/的归一化会少一些奇怪的问题。改造后的扫描结构大概是class AssetScanner { final FlutterPlatform targetPlatform; ListFile collectAssets(Directory flutterAssetsDir) { if (targetPlatform FlutterPlatform.ohos) { // 鸿蒙模式下额外识别 rawfile/flutter_assets 目录避免重复扫描 return _scanWithFlutterAssetDir(flutterAssetsDir); } return _scanLegacy(flutterAssetsDir); } }3.3 第三处图片编码器选型的取舍asset_opt 的图片压缩在 Android 上默认走两条路一是 PNG 无损/近无损压缩二是转 WebP 有损压缩。到了鸿蒙上WebP这个格式的处理要格外谨慎。鸿蒙系统自带的图片解码能力对 WebP 的兼容性经历过一段“坑多”的时期。早期版本的 OpenHarmony 对 webp 的动图支持不完整部分设备上静态 WebP 解出来会出现花屏或透明通道丢失。我当时在适配时做了这样一个编码器选择策略原格式Android 默认处理鸿蒙推荐处理原因PNG压缩 PNG 或转 WebP优先压缩 PNG保留格式避免透明通道兼容问题JPEG有损重压缩有损重压缩格式本身稳定放心处理GIF转 WebP 动图不处理或仅压缩静态帧动图转码风险高WebP重压缩仅调整质量参数已是 WebP 的保持原格式避免二次转码这里我踩过一个大坑后面第 4 节详细展开。总之记住一个原则上线前的资源优化稳定优先于极限压缩率。少压 5% 的体积换一个在所有鸿蒙设备上都不会解码出错的资源产物这笔账是划算的。3.4 第四处输出回写机制的修正asset_opt 原本压缩完后会把新文件直接覆盖回原路径。这在 Android/iOS Flutter 工程里没问题因为源资产路径就是构建时的输入路径。但鸿蒙工程有个特殊情况如果这个工程的历史版本已经把某些资源手动放进了resources/base/media给原生侧用那么 asset_opt 压缩完 Flutter 源资产后还要同步一次resources/base/media下对应文件。否则会出现同一个图标Flutter 侧已经压到 20KB原生侧还留着 80KB 的旧文件HAP 里两个版本都存在体积白省。同步逻辑很简单就是对比文件名把优化后的文件拷贝过去void syncOptimizedMedia(File optimizedFile, Directory ohosMediaDir) { final relative p.relative(optimizedFile.path, from: flutterAssetsRoot); final target File(p.join(ohosMediaDir.path, relative)); if (!target.existsSync()) return; optimizedFile.copySync(target.path); // 注意这里是覆盖式同步务必保证格式兼容否则原生侧会无法解码 }这里要特别提醒同步到resources/base/media的操作默认可以不开。如果你们的鸿蒙原生侧完全没有直接引用$media资源那这一步可以跳过减少不必要的风险。我在配置文件里给这个行为加了一个syncOhosMedia的开关默认关需要时手动打开。4. 实测中踩过的三个深坑分别来自扫描、编码、打包三个环节4.1 坑一扫描到构建中间产物统计虚高反复转码先说现象。我第一版适配跑完之后生成的报告显示有一个 8MB 的图片资源怎么压都在 7.8MB 左右下不去。查了半天才发现扫描器把鸿蒙构建生成的build/ohos/.../rawfile/flutter_assets/assets/里的产物也当成了源资产。这个中间产物里图片已经被上一轮构建处理过再压不仅压不动多少反而可能因为重复转码让图片出现肉眼可见的质量损失。根因Flutter 工程assets/声明的是目录而鸿蒙构建产物和源资产目录结构高度相似扫描器没做排除。解决扫描时强制排除两类路径const excludedSegments [ build, .dart_tool, ohos/flutter_assets, ];所有扫描到的文件只要路径中包含这些分段一律跳过。这样既不会漏扫描也不会把构建中间产物卷进来。这个坑给到的教训是工具适配特别是扫描类工具第一步永远是把“输入边界”划清楚。不划边界后面所有数据都可能是脏的。4.2 坑二WebP 图片在部分鸿蒙设备上解码失败直接白屏这是我最头疼的一个坑而且是在真机上才暴露的。当时为了追求更狠的体积下降我在适配参数里把“PNG 转 WebP”这个开关默认打开了。本机模拟器上测一切正常图片显示完美。结果灰度测试时一台老款鸿蒙设备的页面图片直接显示不出来页面大片空白日志里报的是解码异常。排查过程先怀疑是工具压缩参数问题把 WebP 质量从 75 调到 90问题依旧。再用排除法把转码开关关掉只做 PNG 压缩问题消失。最终确认是 WebP 编码兼容性问题集中出现在部分低版本系统的鸿蒙设备上。解决采用第 3.3 节里的策略表PNG 一律不转 WebP只做 PNG 无损压缩。JPEG 走有损重压缩因为是存量 JPEG格式本身没有解码风险。同时把工具里所有转 WebP 的逻辑全部改为默认关闭只在显式配置了--enable-webp时才启用。这个坑给我的经验是跨端适配中“格式兼容性”是最容易被低估的隐性风险点。模拟器、真机、出厂系统版本三级差异可能都会不一样。在做格式转换类优化时务必给自己的工具加一个“最低兼容模式”保命用的。4.3 坑三自动去重误删公共 rawfile 资源差点打乱整个构建asset_opt 的去重逻辑是通过比对文件内容哈希把内容完全相同的文件合并成一个然后把重复引用全部指向保留的那个。听起来没毛病。但在鸿蒙工程里我碰到了这样一个情况rawfile目录下有两个名字不同、内容一模一样的配置文件一个被module.json5里的某个字段引用另一个是为兼容旧版接口保留下来的。去重逻辑以为发现了“重复资源”直接删掉了一个。结果就是HAP 构建时找不到那个旧接口引用的文件直接构建失败。根因asset_opt 的去重是针对 Flutter assets 设计的它的语义是“只要 Flutter 侧引用一致就把文件合并”。但鸿蒙 rawfile 的引用方式走的是原生侧接口文件名本身就是引用标识同名或改名都可能破坏引用关系。解决给去重功能加了一个“受保护资源列表”。在配置文件里声明哪些路径是只读的asset_opt: protect_list: - resources/base/rawfile/** - resources/base/media/**凡是在保护列表里的路径去重逻辑只做检测和报告不做删除操作。这样 Flutter 侧的 assets 可以放心去重原生侧的资源保持不动。这个坑的教训很直接自动化工具的“破坏性操作”必须从第一天就设计好安全边界。去重、清理、删除这类逻辑宁可保守一点也不要让工具自作主张去动那些它不完全理解的资源。5. 把适配做成自动化工作流命令行、配置与 CI 流水线打通代码适配之后剩下的问题就一个怎么让这条链路在团队里稳定复用。asset_opt 本身支持命令行调用我的工作就是把鸿蒙适配参数固化下来然后接入 CI让每次构建前自动跑一遍。5.1 一个命令完成优化的设计思路在团队里推广一个工具最简单的形态就是“一条命令搞定”。我把鸿蒙适配的所有参数固化进了一个配置文件然后暴露一个统一命令dart run asset_opt -p . --platform ohos --config asset_opt_ohos.yaml命令本身不复杂核心是这个配置文件的设计# asset_opt_ohos.yaml project_type: ohos scan: assets: assets/ exclude: - build/** - .dart_tool/** - ohos/flutter_assets/** compress: keep_original_format: true enable_webp: false png_quality: 85 jpeg_quality: 80 deduplicate: enabled: true protect_list: - resources/base/rawfile/** - resources/base/media/** clean_unused: enabled: true sync_ohos_media: false report: output: build/asset_opt_report.html这个配置的每项都有明确含义团队里谁都能看懂。关键是compress.keep_original_format这个设计默认保持原格式压缩不轻易做格式转换这是从坑二里总结出来的保守策略安全优先。5.2 CI 里怎么接每次构建前自动瘦身命令行方案最爽的一点就是能无痛接进 CI。我们在 GitLab CI 里的流水线大致是这样的optimize-assets: stage: prepare script: - dart pub get - dart run asset_opt -p . --platform ohos --config asset_opt_ohos.yaml artifacts: paths: - build/asset_opt_report.html expire_in: 7 days这个阶段放在prepare里在所有构建和测试之前执行。这样每次构建的输入天然就是瘦身后的资源后端再也不用担心会有人忘记跑优化。这里有个 CI 执行顺序的细节resource 优化一定要发生在 Flutter 构建打包之前而不是之后。如果放在之后跑你优化的是已经打好的 HAP得重新解包、重新打包操作复杂还容易破坏 HAP 签名。放在构建前让构建流程直接消费优化后的成果是最干净的方案。5.3 体积数据的持续跟踪asset_opt 优化完之后生成的 HTML 报告可以额外接一个解析脚本把关键数据同步到 CI 的评论里。比如本次优化前总体积、优化后总体积节省的字节数最大的 5 个资源文件列表未引用资源和重复资源统计这样每次构建的 MR 评论里都会自动出现一条体积跟踪记录团队可以直观看到资源优化的效果在持续累计。我自己的建议是第一个里程碑先保证 HAP 总体积下降 20% 以上然后再追求 30%、40% 的增量。这个目标非常适合作为团队资源治理的北极星指标。最后说一点个人感受。asset_opt 的鸿蒙化适配真正难的不是把代码跑通而是理解两套资源体系之间的差异并且愿意为稳定性牺牲那一点极限压缩率。我在这轮适配里改的代码其实不多但花的 80% 时间都在确认边界和验证兼容性。自动化工具正是有了这些朴素的边界才真正变得可靠可用。如果你也正在做类似的鸿蒙 Flutter 工具链适配希望这份记录能帮你少走几个弯路。
返回列表