1. 出发之前先讲清楚:为什么一个 CLI 组件要去适配鸿蒙
去年年底,我们团队开始把整套开发工具链往鸿蒙(HarmonyOS)生态迁移。最开始的几天,所有人都以为最大的难点在 UI 渲染层,毕竟不同的运行时、不同的组件模型,一套 Flutter 界面要跑通新系统总归要费点功夫。结果真正卡了我们三周的,不是界面,而是一条命令都跑不起来——原来所有围绕终端交互的脚本、工具、命令资产,全都建立在传统操作系统的进程模型和 Shell 语义之上,到了鸿蒙这套全新的运行时环境,基本等于作废。
那时候团队里正好有一个从 Flutter 组件演化出来的内部工具,代号dolphin_cli。它本质是一个高性能终端交互引擎:负责命令解析、参数校验、任务编排、终端输出渲染,并且沉淀了一批自动化流水线能力和命令资产索引。原本在 PC 端和移动端开发调试时跑得好好的,工程师们每天都要敲上百次。可一旦放到鸿蒙侧,连最基本的ls、cat、管道语义都变了,更别提它还依赖一大堆系统级 API。
这篇就是那段时间的完整复盘。我会从架构设计、鸿蒙适配链路、性能优化、工具链集成、资产治理几个维度展开,把踩过的坑和最终的方案都写出来。如果你也正在做跨端 CLI 工具,或者打算把现有 Dart/Flutter 工具链移植到鸿蒙,这篇文章应该能帮你省下至少两周的弯路。
先说结论:dolphin_cli 并不是简单改改配置就能跑在鸿蒙上,它需要一次真正的"架构再适配"——核心交互引擎保留,但平台层全部抽象重写。下面我从为什么要这么设计开始讲。
1.1 旧工具链在鸿蒙上到底断在了哪里
先说一个很多人容易忽略的事实:鸿蒙的应用生态和传统 Linux/Android 的进程模型有本质区别。它是一个组件化的、基于能力和权限管理的系统,应用之间不能随意 fork 子进程,也没有传统意义上全局可写的文件系统视图。这直接干掉了三类最常用的 CLI 依赖:
第一类是裸进程调用。过去我们靠Process.run()去调用系统工具,比如ffmpeg、adb、git,到了鸿蒙的应用沙箱环境里,这种调用路径要么被权限拦截,要么根本找不到对应的可执行文件。
第二类是终端 IO 语义。标准输入、标准输出、管道、重定向,这些概念在传统终端里天经地义,但在鸿蒙的应用模型里,开发者往往面对的是日志回调、事件回调这类异步通道,没有天然的 stdin/stdout 概念。
第三类是文件系统布局。原来我们读写用户的.dolphin/配置目录、临时缓存、日志文件,路径都是固定的,但在鸿蒙沙箱里,不同模块能访问的目录和权限完全不一样,路径映射规则也需要重新设计。
你可以把这个问题想成:原来 CLI 工具是"站在一个开放广场上吆喝",到了鸿蒙,你只能"在自家房间里,通过一扇限定大小的窗户和别人交换东西"。该有的能力都得自己再造一套。
1.2 为什么依然选择 Flutter/Dart 作为底座
团队里也争论过要不要直接用 Rust 或者 Go 重写一版 CLI。讨论了很久,最后还是决定保留 Flutter/Dart 底座,原因有三个:
第一,团队现有资产。我们已经有大量用 Dart 写的业务逻辑、AI 模型调用、数据处理脚本,如果重写,这些都要推倒重来。Dart 的 AOT 编译能力在性能上并不输给 Go,日常 CLI 场景绰绰有余。
第二,跨端一致性。dolphin_cli 不只是跑在鸿蒙上,它在 Windows/macOS/Linux 上也要继续使用。用 Flutter/Dart 可以保证同一套命令解析、任务编排、渲染逻辑在五个端上行为一致,只替换平台层的桥接实现。
第三,交互引擎的渲染优势。传统 CLI 大多只是文本流,但我们要做的是"终端交互引擎",需要支持实时进度条、表格化输出、模糊搜索交互、彩色高亮。Dart 的异步模型和流式处理在这个场景下非常顺手,事件循环天然适合处理击键响应和输出刷新的调度。
选型定了之后,剩下的问题就变成了:怎么让这套 Dart 生态的工具,在鸿蒙的运行时里跑起来、跑得稳、跑得快。
2. dolphin_cli 的模块拆解:把"引擎"这件事拆清楚
在讲鸿蒙适配之前,必须先看清楚 dolphin_cli 的内部结构,因为后面几乎所有适配工作都围绕着"哪些层可以跨端复用、哪些层必须重写"这条线展开。这里我直接按模块给大家拆一遍。
2.1 命令注册与解析层:参数模型是一切的基础
dolphin_cli 的入口是一个"命令树"设计。每个命令就是一个节点,节点上挂着这个命令的名字、描述、参数定义、子命令列表。参数定义采用类似标准库args包的风格,但我们在它之上加了一层元数据注解,这样可以让参数声明同时服务于三个目标:运行时解析、帮助文本生成、以及自动补全。
abstract class DolphinCommand { String get name; String get description; /// 返回该命令的参数规格 List<CommandParam> get params; /// 执行入口 Future<int> execute(CommandContext ctx); } class CommandParam { final String flag; final String abbr; final String help; final bool required; final bool multiple; const CommandParam({ required this.flag, this.abbr = '', this.help = '', this.required = false, this.multiple = false, }); }实际使用的时候,我们秉承"git 风格"的子命令嵌套,例如dolphin asset scan --depth=3 --format=json,解析层会先把输入拆成 token 序列,再沿着命令树逐层匹配。这里有个容易被忽视的细节:参数解析必须支持--key=value和--key value两种写法,同时还要兼容短参数合并,比如-abc等价于-a -b -c。这些看似基础的功能,在鸿蒙的适配过程中反而没出问题,因为解析层是纯 Dart 实现,完全不依赖系统能力。
2.2 执行调度层:异步任务队列与状态机
命令真正运行的部分,我们没有采用"一个命令跑一个 Future"这种朴素做法,而是封装了一套执行调度器。它内部维护一个任务队列,每个命令在运行过程中会不断上报状态(开始、进行中、成功、失败、重试中),上层 UI 根据状态变化实时刷新进度条。
这套调度器有几个关键设计:
- 支持并发上限控制,默认同时最多跑 4 个任务,防止大量小文件操作打满系统 IO。
- 支持超时与重试,每条命令可以声明自己的超时时间和重试次数。
- 支持取消与中断,用户按
Ctrl+C时,调度器会先发取消信号给所有子任务,等它们主动退出后再结束进程。
在 Dart 里,我们大量使用了Stream来传递任务状态事件。比如有一个TaskStatusStream,UI 层只需要监听这个流,就能无脑刷新各种进度组件。这套抽象在后面适配鸿蒙时帮了大忙,因为鸿蒙侧的运行环境和桌面端不一样,我们只需要替换"状态事件如何变成屏幕上的像素"这一层,不需要改动调度逻辑。
2.3 输出渲染层:从 JSON 到交互式 TUI
dolphin_cli 的输出不是简单的print,而是走了一层渲染管道。每条命令执行完会产生一个结构化的结果对象,然后按用户指定的格式输出:默认是人类可读的彩色文本;--format=json输出结构化数据方便流水线消费;--format=table输出对齐表格;还有--interactive模式,会启动一个 TUI 界面做交互式选择。
这层设计的一个关键点是我们把"业务返回值"和"渲染逻辑"彻底分离。命令层永远只返回DolphinResult,渲染层根据模式去决定把它画成什么样。这样一来,自动化流水线只需要盯 JSON 输出,人类开发者用默认文本模式,两边各取所需,不会互相干扰。
到了鸿蒙侧,这层渲染逻辑大部分可以直接复用——因为我们采用了自绘 ANSI 转义的方案,而不是依赖系统终端。鸿蒙没有天然的标准终端模拟器,我们就自己在界面上画了一个输出面板,把 ANSI 转义序列解析成富文本控件来渲染。后面第五节我会详细讲这个方案的取舍。
3. 鸿蒙适配的全过程复盘:环境、编译、桥接三层同步推进
这一节是整篇文章的干货核心。说实话,看再多的移植文档,都不如自己完整走一遍鸿蒙的适配流程来得深刻。我按"环境准备 → 编译链路 → 平台桥接"三个阶段来还原我们的实际操作。
3.1 环境准备:开发工具链与鸿蒙 SDK 的版本配对
鸿蒙应用开发的标准工具链是 DevEco Studio,它内置了鸿蒙 SDK 以及配套的模拟器和构建工具。dolphin_cli 要跑进鸿蒙生态,第一件要做的是确认 Dart 侧的能力边界。
我们当时的做法是先把 Flutter 工程切到鸿蒙适配分支,然后单独把命令行程序编译为鸿蒙的原生动态库,再通过一个轻量插件工程把它包装成鸿蒙应用模块。也就是说,dolphin_cli 本体不会被打成一个独立应用,而是作为一个"能力组件"嵌入到宿主应用里,对外暴露命令执行接口。
工程结构调整如下:
dolphin_cli/ ├── core/ # 纯 Dart 实现,跨端复用 │ ├── command/ # 命令树、参数解析 │ ├── executor/ # 任务调度器、状态机 │ ├── render/ # 输出渲染管道 │ └── asset_index/ # 资产索引与扫描 ├── platform/ # 平台适配层 │ ├── desktop/ # Windows/macOS/Linux 实现 │ ├── android/ # Android 实现 │ └── harmony/ # 鸿蒙实现(本次新增) └── bridge/ ├── harmony_bridge.ets # 鸿蒙侧桥接入口 └── method_channel.dart这里最关键的决策是:核心层完全不 import 任何 dart:io 的 API。一开始我们觉得这不可能,毕竟 CLI 工具离不开文件读写。但后来做了个强制约束:所有File、Directory、Process、Socket的使用都必须走PlatformFileSystem这种抽象接口,由平台层提供实现。这个约束一开始让我们多写了很多胶水代码,但到了鸿蒙适配的时候,发现工作量一下子减少了一大半——因为只需要为鸿蒙写一套平台实现,核心调度逻辑一行都不用动。
3.2 编译链路:从 Dart AOT 到鸿蒙 hap 包
鸿蒙应用最终是通过 DevEco 构建出.hap格式的安装包。我们的编译链路分三步:
第一步,用 Dart 的 AOT 编译能力把核心层编译成原生库。这里注意不能用dart compile exe,因为鸿蒙的加载器和标准库路径不一样。我们实际用的是 Flutter 鸿蒙适配分支提供的一套编译脚本,把 Dart 代码编译成libdolphin_core.so,再链接到鸿蒙的 native 工程里。
第二步,编写鸿蒙侧的桥接层(.ets 文件)。这层的职责有两个:接收来自 Dart 侧的命令执行请求,以及把鸿蒙的系统能力(文件读写、网络、日志)包装成 Dart 侧PlatformFileSystem接口的实现。
第三步,通过 Hvigor 构建脚本把.so、资源文件、桥接层代码打成一个统一的 hap 包。这里有一个坑:鸿蒙对动态库的加载路径有规范要求,必须放在libs/arm64-v8a这种目录层级下,Hvigor 的打包配置里如果不显式声明,很容易出现"so 文件在包内却加载不到"的诡异问题。
3.3 平台桥接:一套接口八种实现
平台桥接层是整个适配的核心工作量所在。我列一张对比表,大家感受一下差异面有多宽:
| 能力项 | 桌面端实现 | 鸿蒙端实现 |
|---|---|---|
| 文件读写 | dart:io 直接访问 | 通过 ArkTS 的沙箱文件接口,路径需映射 |
| 进程调用 | Process.start() | 受限,改为任务内轻量执行 + 调度器管理 |
| 标准输出 | stdout 管道 | 回调到 UI 层日志面板 |
| 环境变量 | 系统 env | 自定义配置注入,无系统级 env |
| 命令补全索引 | 本地目录扫描 | 资产索引库 + 沙箱白名单 |
这背后最核心的思想是能力等价替换,而不是能力模拟。鸿蒙不提供某个能力,就不要去 hack 一个假的来,而是换一种方式达成同样的业务目标。比如没有独立的子进程,我们就把所有命令的执行放到同一个 isolate 里,用调度器的并发控制来模拟进程隔离的效果;没有环境变量,就提供统一的配置源,让每条命令从同一个配置对象里读取环境参数。这套思路后面在资产治理模块里也沿用上了。
4. 性能专项:交互引擎怎么从"能跑"做到"灵敏"
适配完成之后,dolphin_cli 在鸿蒙上是能跑了,但体验远谈不上好。首次启动要 800 多毫秒,磁盘扫描 1000 个资产文件要十几秒,命令补全经常给人"卡了一下"的感觉。对于一个天天敲的交互工具来说,这种体验是不能接受的。于是我们又做了四轮专项优化。
4.1 启动耗时:从 850ms 压到 210ms
CLI 工具启动慢,在传统桌面平台很难想象,但在鸿蒙上非常典型——因为应用框架本身的初始化链路更长。
我们做了三件事:
第一,AOT 快照。Dart 代码编译成 AOT 产物之后,启动时不再需要加载整棵 VM,而是直接读快照。这一项优化直接砍掉了约 300ms。
第二,懒加载帮助文本。过去所有命令的帮助文档在启动时就全部注册进内存,我们的命令树有 200 多个节点,帮助文本加起来不小。后来改成只在用户输入help或参数解析失败时才加载对应文档,又省下约 150ms。
第三,渲染层分离。启动时只初始化文本输出所需要的最小渲染管线,交互式 TUI 组件全部推迟到真正进入交互模式才初始化。
优化后启动耗时的分布大概是:Dart VM 初始化 80ms、核心命令树注册 45ms、渲染层初始化 50ms、其他 35ms,合计 210ms 左右,体感上基本是"秒开"。
4.2 命令补全的响应延迟:从 48ms 降到 9ms
命令补全是交互引擎的门面功能。我们实现了一套基于 Trie 树的命令前缀匹配,配合参数定义的元数据生成候选列表。最初的实现每次按键都会从根节点重新扫描,导致敲击d时延迟 10ms,敲到dolphin a时延迟飙到 48ms,因为候选太多、排序逻辑又重。
优化手段主要两个:
一是给 Trie 树的每个节点加上候选结果缓存。一个节点对应的候选列表在命令树注册时就算好,之后只会在参数描述变化时失效,平时直接命缓存。二是给排序函数加了一个"最近使用优先"的参数,在内存里维护一个 LRU 列表,用户常用的命令永远排在前面。现在最复杂的模糊补全场景(比如搜dolphin asset index rebuild),Response 时间也控制在 10ms 以内,几乎感觉不到延迟。
4.3 大目录扫描:用流式 IO 替代一次性加载
资产索引功能要扫描项目目录下所有文件,识别哪些是可执行脚本、哪些是命令配置文件。最初实现是把所有文件路径一次性读进内存再逐个处理,1000 个文件就吃了 300MB 内存,GC 频繁导致界面卡顿。
后来改写成了流式处理:用Directory.list()的懒加载流,每拿到一个文件实体就立刻处理并丢弃原始路径,同时把索引写入内存中的紧凑哈希表。整个扫描过程内存占用控制在 30MB 以内,1000 个文件的扫描时间从 12 秒降到 1.8 秒,GC 停顿也几乎消失了。
这里我踩过一个很有意思的坑:在鸿蒙上Directory.list()对流式处理的支持和桌面端不完全一致,有时候流会提前关闭导致只扫了一半文件。排查到最后发现是平台层的桥接实现没有正确处理好流结束事件,后来在桥接层加了一个显式的stream.done回调才解决。这个问题第四节的踩坑清单里会单独讲。
5. 自动化工具链集成:让 dolphin_cli 成为流水线里的一等公民
CLI 工具只有被自动化流水线消费,才能发挥最大价值。dolphin_cli 在鸿蒙上的定位不仅仅是一个交互式命令工具,它同时承担着自动化构建、检查、发布环节里的门禁角色。这一节我们聊工具链的集成设计。
5.1 结构化输出:让机器能读懂每一次执行
为了对接流水线,dolphin_cli 的每条命令都实现了--format=json输出模式。输出结构是固定的,类似下面这样:
{ "command": "asset scan", "request_id": "a3f9c1", "status": "success", "duration_ms": 1805, "result": { "total": 1000, "indexed": 998, "skipped": 2, "items": [] } }关键点是任何失败都必须有非零退出码。dolphin_cli 定义了一套退出码规范:0 表示成功、1 表示参数错误、2 表示业务校验失败、3 表示超时、4 表示权限不足。流水线只需要根据退出码和 JSON 里的status字段做判断,不需要解析人类可读的文本。
自动化集成这块我们还做了一个很实用的功能:执行计划文件。用户可以在项目里放一个dolphin_job.yaml,定义一系列命令的编排关系,比如"先扫描资产、再检查依赖、最后构建签名"。dolphin_cli 会按拓扑序执行这些命令,任何一个失败都会中断后续任务:
jobs: - name: scan_assets command: asset scan --depth=2 - name: check_deps command: dep verify --strict depends_on: scan_assets - name: build_hap command: build hap --release depends_on: check_deps在鸿蒙侧,执行计划文件被放在沙箱的配置目录里,流水线每次执行前会拉取最新版本。这样团队内部的构建流程就变成了一组可审计的、声明式的命令编排,而不是一堆散落在 CI 脚本里的临时命令。
5.2 与构建门禁的集成:每次提交都被检查
另一个高频场景是代码提交前的门禁检查。我们把dolphin ci-check挂到了团队的代码提交钩子里,每次提交自动执行格式检查、资产索引一致性校验、依赖安全扫描。由于鸿蒙侧的应用沙箱无法直接触发系统级钩子,我们的方案是让 DevEco 的构建任务在编译前调用 dolphin_cli 的ci-check子命令,再把结果以 JSON 形式汇报给构建面板。
这条链路跑通之后,团队内部出现了一个明显的变化:大量重复性的检查工作从"人肉执行"变成了"自动执行",而且因为输出结构统一,任何一次失败都能在流水线里定位到具体的命令和参数。自动化工具链的价值在这里体现得最为直接。
5.3 审计日志:每次执行都留痕
最后是审计。所有通过 dolphin_cli 执行过的命令,无论成功失败,都会记录一条结构化日志:执行时间、命令名、参数、退出码、耗时、请求来源。这些日志默认存在本地沙箱里,也可以按需导出到远程日志系统。
这么做最大的好处是出了问题时可以回溯。有一次团队排查一个构建失败,就是因为某位同学的机器上运行了旧版本的资产索引命令,导致上游产物版本不一致。通过审计日志一查,直接定位到几秒前的那条命令,省去了大量猜测时间。
6. 命令资产治理架构:从"脚本堆"到"资产库"
最后说说资产治理。这可能是整个项目里概念上最抽象、但实际收益最明显的一部分。所谓"命令行资产",我的定义是:一切可以被命令行消费的可执行实体和配置实体——包括 shell 脚本、二进制工具、Python/Node 脚本、命令配置文件、hook 规则、模板文件。
6.1 资产模型与索引结构
我们把每条命令资产抽象成一个统一的数据模型:
class CommandAsset { final String id; // 资产唯一ID,由路径哈希生成 final String name; // 资产名称 final AssetType type; // executable / script / config / hook final String version; // 语义化版本 final String checksum; // 内容 SHA-256 final List<String> deps; // 依赖的其他资产ID final String sourcePath; // 原始路径 }资产目录采用内容寻址存储:每个资产的内容会被计算出一个唯一的哈希值,作为它在资产库中的存放名称。这样同一份脚本无论被复制到多少个项目里,在资产库中都只有一份实体,可以天然的做去重和版本追踪。
索引结构上,我们用了一个精简的倒排表:每个资产 ID 对应一份元数据 JSON,目录里再放一个总索引文件,对着名称和别名做快速模糊查找。扫描器每天定时增量更新,只处理修改时间有变化的文件,因此不会对系统性能造成压力。
6.2 依赖管理与安全校验
命令资产之间经常存在隐式依赖。比如一个构建脚本,可能要求系统里有某个特定版本的工具链;一个 Python 工具,可能依赖某个第三方库。传统做法是把这些依赖写进 README,靠人肉保证。我们的做法是把deps字段显式声明在资产元数据里,扫描器会构建一张依赖图,每次执行前自动检测前置条件是否满足。
安全校验也不可忽略。团队遇到过一个问题:一个第三方脚本被悄悄替换成了恶意版本,导致构建产物异常。从那以后,dolphin_cli 对每个新增资产都强制做内容哈希校验,凡是哈希与元数据声明不一致的,一律拒绝执行并弹出告警。审计日志里也会记录每次资产加载时的哈希值,做到可追溯。
6.3 生命周期管理:从收录到废弃
资产治理不只是收录和索引,还要考虑生命周期的每个阶段。我们为每条资产规定了四种状态:active(正常使用)、deprecated(已不推荐)、retired(已停用)、archived(已归档)。命令行执行时,deprecated状态的资产会给出警告提示,retired状态的默认拒绝执行,archived状态的直接不出现在补全列表里。
这套状态机的价值在于:随着团队规模扩大,命令行资产会像滚雪球一样膨胀。如果没有一套生命周期机制,最终会变成一堆"不知道还该不该用"的脚本集合。通过显式管理状态,每个资产都很清楚自己的定位和寿命,治理成本大幅度下降。
7. 踩坑实录:几个差点让我们返工的真实问题
最后分享几个排查过程比较曲折的坑。这些问题在官方文档里基本找不到现成答案,都是靠实际调试和读源码才定位到的。
7.1 坑一:鸿蒙沙箱路径映射导致索引缓存全部失效
现象:迁移到鸿蒙后,dolphin_cli 的资产索引第一次扫描正常,但第二次运行时索引变成了空,所有资产的元数据都读不出来了。
排查过程:我们一开始以为是代码逻辑问题,反复检查索引写入和读取的代码。后来发现,第一次扫描时写入的"缓存路径"是绝对路径,比如/data/storage/el2/base/haps/entry/files/assets.json,但第二次运行时,鸿蒙的沙箱路径前缀发生了变化,同样的逻辑拿到的是/data/storage/el2/base/haps/entry/files2/assets.json或者干脆是另一个挂载点下的路径。
根因:鸿蒙的文件路径在应用更新或重启后可能存在动态映射,不能把绝对路径硬编码进缓存索引。正确做法是每次启动时重新获取沙箱根路径,或者使用系统提供的路径抽象接口,而不是自己拼路径。
解决:我们在平台层封装了SandboxPathResolver,所有文件访问必须通过它拿到当前会话的真实路径。此后索引缓存问题彻底消失。
7.2 坑二:ANSI 颜色在部分模拟器上变成乱码
现象:同样的渲染代码,在真机上颜色正常,在某个型号的鸿蒙模拟器上,彩色文本变成了一堆ESC[31m之类的乱码。
排查过程:一开始怀疑是编码问题,但纯文本输出正常,只有带转义序列的文本异常。后来在模拟器上安装了另一个终端类应用测试,发现它输出的颜色也是乱码。
根因:部分模拟器的终端组件没有实现 ANSI 转义序列解析能力,它直接把ESC[当作普通字符显示。这其实是模拟器渲染层的缺失,而不是 dolphin_cli 的问题。
解决:我们在渲染层做了一个能力探测:首次启动时输出一段颜色测试序列,如果检测到输出里包含未被解析的ESC字符,就自动降级为纯文本模式。这个降级开关也支持用户手动指定--color=never。
7.3 坑三:合并流式扫描时偶发提前结束
现象:用Directory.list()扫描目录,1000 个文件偶尔只扫到 600 个就提前结束,且不报任何错误。
排查过程:这个问题在桌面端从未出现过,鸿蒙平台第一次发现。我们加了逐条日志,发现流在某个特定文件之后就开始产生done事件。怀疑是不是该文件的元数据读取异常导致平台层主动关了流。
根因:查到最后发现是桥接层对list方法的实现里,错误地把"某个目录项访问失败"当作"整个流结束",向 Dart 侧发送了done信号。这是一个典型的局部错误被冒泡为全局终止的问题。
解决:在桥接层做了修改:单个目录项失败时记录日志并跳过,继续保持流打开。同时在 Dart 侧加了兜底——扫描结束后对比"已处理数量"和"目录列表数量",如果不一致就标记本次扫描为"不完整"并触发一次重试。
写在最后
整套鸿蒙适配做下来,我最深的体会是:跨端 CLI 工具真正的难点从来不是语法、不是框架,而是平台能力边界的重新认知。桌面端习以为常的进程、管道、全局文件系统,在鸿蒙的世界里都被重新定义了。dolphin_cli 之所以能顺利落地,靠的是从一开始就坚持"核心层纯逻辑、平台层全抽象"的架构纪律——这句话听起来像正确的废话,但真正被现实毒打一遍之后,才会知道它有多重要。
如果你们团队也在做类似的移植,我给你一个很实际的建议:动手之前,先把所有dart:io的依赖全部列出来,一个一个问自己"这个能力在目标平台上真的存在吗"。不要抱有侥幸心理,不要觉得"应该可以跑"。把不确定性提前暴露在架构设计阶段,远比后面在集成测试里排查要省时得多。
最后再分享一个小细节:鸿蒙侧的编译产物体积一开始比桌面端大了一倍,后来发现是因为 AOT 快照里包含了大量无用符号。在编译参数里开启--strip和树摇优化后,体积直接砍掉 60%。这种参数级的优化往往不需要改代码,但对安装包体积敏感的场景来说,效果立竿见影。后续如果还有机会,我会再写一篇关于鸿蒙侧增量构建和热更新的实践,这里先留个悬念。