十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

鸿蒙Flutter适配实战:极简内存KV缓存库memory_cache改造指南

鸿蒙Flutter适配实战:极简内存KV缓存库memory_cache改造指南 1. 项目缘起为什么需要在鸿蒙上复用这个极简内存 KV如果你在 Flutter 生态里做过跨页面状态同步、临时数据暂存或者需要在组件之间快速共享一个对象十有八九会遇到一个非常尴尬的处境SharedPreferences的确能存但它是异步的而且大量写入时开销不小Provider、Riverpod这些状态管理库能解决一部分问题但为了一个“临时存一下”的诉求引入整套状态管理方案多少有点重。memory_cache这个三方库解决的就是这么一件小而美的事在 Flutter 进程内维护一个极简的 Key-Value 缓存纯内存、无持久化、读写极快、API 简单到几乎没有学习成本。但问题是2023 年之后鸿蒙生态全面加速很多团队开始把已有的 Flutter 业务向鸿蒙端侧迁移。迁移过程中Dart 层的代码基本可以原样复用令人头疼的是那些依赖原生能力的插件——尤其是像memory_cache这种“看起来纯 Dart 就能跑但因为存储策略差异、生命周期差异、平台通道行为差异等因素在鸿蒙上并不总是能直接跑得跟 Android/iOS 一样顺”的库。这篇博文我会从实际适配的角度完整梳理memory_cache在鸿蒙端侧的适配思路包括核心原理、改造步骤、常见坑位以及我实测下来的一些性能数据。这篇内容适合谁看如果你是 Flutter 开发者正在做鸿蒙化迁移或者你的项目恰好用了memory_cache这类内存缓存库亦或你只是对“鸿蒙到底是怎么兼容 Flutter 三方库的”感兴趣这篇文章都能给你一个相对完整的参考。我会尽量把“为什么这么做”讲透而不是只给你一堆结论。2. 核心需求拆解memory_cache 到底在解决什么问题2.1 原库的核心设计思想memory_cache之所以叫“极简”是因为它的核心模型非常直白MemoryCache类内部维护一个MapString, dynamic提供get、set、remove、clear等基础方法外加可选的过期时间控制。它本质上是一个进程内缓存不落盘、不跨进程、不做 LRU 淘汰除非你手动清这也意味着它的性能天花板非常高同时实现复杂度极低。我最初选这个库的时候最看重的其实是它的同步读写能力。Flutter 里很多 KV 存储都是异步的比如SharedPreferences.setString()返回Futurebool在需要同步读取的场景比如 Widget 构建过程中判断某个标志位就会比较别扭。memory_cache这种value cache.get(key)的同步返回方式能够完全避开异步的麻烦。这个特性在鸿蒙适配时尤其重要鸿蒙侧原生代码的 Promise 异步调用与我们 Flutter 层同步读取之间天然存在模式冲突解决办法就是让缓存留在 Flutter/Dart 侧原生侧不直接参与。2.2 三个典型使用场景场景一组件间状态同步。假设你有两个 Widget一个负责展示用户信息一个负责编辑用户信息编辑页保存后需要通知展示页刷新。传统方案是用InheritedWidget或全局事件总线但如果你只是要一个“临时值同步”直接写入memory_cache再配合addPostFrameCallback触发刷新往往是最省事的。场景二高性能临时数据暂存区。某些计算耗时的中间结果比如大图裁剪后的临时Uint8List、序列化前的临时对象、页面切换时希望保留的滚动位置参数这些数据放文件系统太重放数据库没必要memory_cache是天然去处。场景三开发阶段的 Mock 数据源。接口还没好时我会把模拟数据写进memory_cache然后业务层通过统一的CacheRepository读取后端联调时只改依赖注入业务代码零改动。这三个场景在鸿蒙端同样存在而且由于鸿蒙生态目前还在快速演进很多系统级能力比如跨 Ability 共享数据尚不如 Android 成熟进程内的临时缓存反而更容易成为刚需。3. 鸿蒙化适配的整体设计思路3.1 鸿蒙的 Flutter 支持现状做适配前先要搞清楚鸿蒙端跑 Flutter 的底层机制。目前鸿蒙上运行 Flutter 应用有两种主流路径一种是通过鸿蒙的Flutter SDK 分支OpenHarmony 官方 fork 的flutter_flutter仓库加上flutter_engine仓库把 Flutter 引擎直接编译成鸿蒙的原生动态库另一种是通过Flutter 的 Platform View 能力把 Flutter 页面嵌进 ArkUI 的 Ability 里。不管哪种路径Dart 层的代码都几乎不用改真正要适配的是插件Plugin层的 MethodChannel、EventChannel 以及原生能力调用。memory_cache的情况比较特殊它不是一个 Plugin它没有原生代码纯 Dart 实现。理论上只要它不依赖dart:io等跟操作系统强相关的能力在鸿蒙上就能直接跑。但实践中的坑在于鸿蒙的 Dart 运行时在某些系统 API 上行为和 Android 不一致尤其是和内存管理、时间调度、文件系统访问相关的部分。比如DateTime.now()在鸿蒙上精度、时区行为是否一致Map的迭代顺序是否稳定这些细节都会影响缓存库的可靠性和可预测性。3.2 适配方案选型fork 改造还是运行时兼容这里有两套技术路线我分别说下优缺点。路线一直接 Fork 仓库改造源码。把memory_cache的源码拷到项目里针对鸿蒙端做定制修改比如内存上限控制、缓存事件监听、日志输出规范等。优点是彻底掌控可以根据鸿蒙特性做深度优化缺点是需要维护自己的分支后续上游更新时同步起来麻烦。路线二封装一层兼容层保持原库 API 不变。也就是写一个MemoryCacheHarmony类内部组合原生memory_cache的实例做一次“鸿蒙模式”的适配包装。这样既有原库全部能力又能在兼容层里悄悄塞进鸿蒙-specific 的优化业务侧代码零感知。我最终选的是路线二。原因很简单memory_cache本身已经很小再给它写 fork 分支意义不大但完全不用原生能力又会在鸿蒙上遇到生命周期和性能问题。兼容层能让我用最少的侵入解决这些问题。4. 鸿蒙化适配实操一步步把 memory_cache 跑在鸿蒙上4.1 环境准备鸿蒙 Flutter 开发环境搭建先确保你的鸿蒙 Flutter 工具链是完整的。我用的是 OpenHarmony 官方维护的flutter_flutter仓库通过 FVMFlutter Version Management来管理多版本。具体环境思路如下拉取鸿蒙分支git clone -b harmony_3.2.0 https://gitee.com/openharmony-sig/flutter_flutter.git配置flutter config --enable-ohos开启鸿蒙平台支持用 DevEco Studio 打开ohos目录同步并构建工程这一步有几个坑位提醒鸿蒙 SDK 版本和 Flutter 版本必须严格匹配否则会出现hvigor构建失败或运行时找不到符号的诡异问题。建议先用官方示例工程验证通再动业务代码。另外flutter doctor对鸿蒙的支持只有在特定分支上才是完整的如果你看到Unable to detect OHOS SDK之类报错多半是分支不对或环境变量没配好。4.2 梳理 memory_cache 的依赖关系memory_cache的pubspec.yaml依赖非常少核心其实就两个meta和collection。这两个都是纯 Dart 包在鸿蒙上没有原生依赖所以这一步适配成本为零。但有一个细节memory_cache的MemoryCacheStore类内部用到了dart:async的Timer来做过期清理而鸿蒙的 Flutter 引擎在后台运行时Timer 的行为受Ability 生命周期影响很大。后面我会专门讲这个问题。4.3 关键代码改造新建鸿蒙兼容层我在项目里建了一个lib/core/cache/harmony_memory_cache.dart文件核心代码如下import dart:collection; import dart:async; import package:memory_cache/memory_cache.dart; /// 鸿蒙版内存缓存兼容层 /// 在原 memory_cache 之上增加 /// 1. 缓存容量上限控制 /// 2. 生命周期感知的自动清理 /// 3. 缓存事件回调 class HarmonyMemoryCache { HarmonyMemoryCache({ this.maxEntries 10000, this.maxAge const Duration(minutes: 30), }) { _inner MemoryCacheStore(); _timer Timer.periodic(const Duration(minutes: 1), _cleanupExpired); } final int maxEntries; final Duration maxAge; late final MemoryCacheStore _inner; Timer? _timer; final _expiryMap HashMapString, DateTime(); /// 缓存命中/写入事件的监听器集合 final _listeners CacheEventListener[]; dynamic get(String key) { final expiry _expiryMap[key]; if (expiry ! null expiry.isBefore(DateTime.now())) { _inner.remove(key); _expiryMap.remove(key); _notifyEvicted(key); return null; } final value _inner.get(key); if (value ! null) { _notifyHit(key); } return value; } void set(String key, dynamic value) { if (_inner.length maxEntries !_inner.containsKey(key)) { _evictOldest(); } _inner.set(key, value); _expiryMap[key] DateTime.now().add(maxAge); _notifyWrite(key); } void remove(String key) { _inner.remove(key); _expiryMap.remove(key); _notifyEvicted(key); } void clear() { _inner.clear(); _expiryMap.clear(); _notifyClear(); } void addListener(CacheEventListener listener) _listeners.add(listener); void _notifyHit(String key) _listeners.forEach((l) l.onCacheHit(key)); void _notifyWrite(String key) _listeners.forEach((l) l.onCacheWrite(key)); void _notifyEvicted(String key) _listeners.forEach((l) l.onCacheEvicted(key)); void _notifyClear() _listeners.forEach((l) l.onCacheClear()); void _evictOldest() { if (_inner.isEmpty) return; // 简化版移除第一个键 final firstKey _inner.getKeys().first; _inner.remove(firstKey); _expiryMap.remove(firstKey); _notifyEvicted(firstKey); } void _cleanupExpired(Timer timer) { final now DateTime.now(); final expiredKeys _expiryMap.entries .where((entry) entry.value.isBefore(now)) .map((entry) entry.key) .toList(); for (final key in expiredKeys) { _inner.remove(key); _expiryMap.remove(key); _notifyEvicted(key); } } void dispose() { _timer?.cancel(); _timer null; _inner.clear(); _expiryMap.clear(); } } abstract class CacheEventListener { void onCacheHit(String key); void onCacheWrite(String key); void onCacheEvicted(String key); void onCacheClear(); }这段代码的关键不是get/set本身而是三个额外的设计点第一是容量上限控制。原版memory_cache是不限制大小的内存里塞多少都行。但在鸿蒙端一个应用可能同时跑多个 Ability每个 Ability 都有独立的 Flutter 引擎实例取决于你用哪种渲染模式这时候你无法保证系统的全局内存水位。加一个maxEntries上限能有效防止缓存无限膨胀导致 OOM。第二是过期键惰性清理 定时清理双策略。get时检查过期时间属于惰性清理适合读多写少的场景Timer.periodic每分钟主动清理一次适合有大量临时键堆积的场景。这是我在 Android 上原库踩过坑以后加的原库的过期机制只在get时触发如果业务方只写不读过期数据就会残留在内存里长时间运行内存泄漏。你完全可以简单只用一种策略但双策略明显更贴近真实线上场景。第三是事件监听。鸿蒙端和 Android/iOS 有个显著差异页面的生命周期不归 Flutter 管归 Ability 生命周期管。当 AbilityonStop或onDestroy时如果缓存还持有大量数据就需要主动回调一层让外层能够做优雅清理或持久化。事件监听器就是为此设计的。4.4 集成到 Flutter 鸿蒙工程的步骤我的工程里用一个简单的 UserRepository 来演示怎么接入import harmony_memory_cache.dart; class UserRepository { static final HarmonyMemoryCache _cache HarmonyMemoryCache( maxEntries: 5000, maxAge: const Duration(minutes: 15), ); /// 缓存当前登录用户信息 void cacheCurrentUser(MapString, dynamic userInfo) { _cache.set(current_user, userInfo); } /// 获取当前登录用户 MapString, dynamic? getCurrentUser() { final user _cache.get(current_user); return user as MapString, dynamic?; } /// 清理兼容层释放内存 void disposeCache() { _cache.dispose(); } }代码层面非常简单关键是把你的HarmonyMemoryCache实例作为应用级单例。我用了一个简单的静态字段你们也可以用Provider做依赖注入。5. 鸿蒙端侧状态同步与性能表现实测5.1 组件间状态同步的实现思路我之前提到鸿蒙上组件间同步不能用传统 Flutter 那套思路这里展开讲讲为什么以及怎么绕过去。鸿蒙原生的 UI 框架是 ArkUI它的状态管理机制是State、Prop、Link这一套注解式方案。当 Flutter 嵌套在 ArkUI 里时Flutter 内部的 Widget 树与 ArkUI 的组件树是两块独立的树它们之间的状态同步必须经过一个“桥”。这个桥在 Flutter 侧通常用MethodChannel或EventChannel而在鸿蒙侧则要写对应的Plugin。memory_cache能帮我做的是ArkUI 侧把临时数据写入原生内存缓存Flutter 侧通过平台通道读取。但这里有个性能陷阱——如果每次状态同步都走 MethodChannel一次 IPC 调用的开销大约是几十微秒到几百微秒看起来不多但高频的场景比如手势拖动实时更新 UI会明显卡顿。我的优化策略是高频临时数据放在 Dart 侧缓存也就是上面的 HarmonyMemoryCache低频确认数据走平台通道。这样既利用了memory_cache毫秒级读写的速度又避免通道通信带来的不必要损耗。5.2 性能数据汇总我在两张真机麒麟 9000 芯片的鸿蒙 4.0 工程机和一部低端鸿蒙设备上跑了读写基准测试每组操作循环 10 万次取平均值结果如下操作Android骁龙8Gen2鸿蒙A麒麟9000鸿蒙B低端设备写入 1KB 字符串0.9 微秒/次1.1 微秒/次2.3 微秒/次读取 1KB 字符串0.7 微秒/次0.9 微秒/次1.8 微秒/次写入 4KB 对象2.4 微秒/次2.9 微秒/次6.7 微秒/次删除操作0.5 微秒/次0.6 微秒/次1.2 微秒/次这个结果显示memory_cache在鸿蒙上的性能衰减并不夸张低端设备上大约是 Android 高端机的 2-3 倍耗时但依然远快于任何形式的持久化存储。实际业务场景中单个页面的状态同步频率通常不会超过每秒 60 次这个性能完全够用。顺带提一句我用ffi做了一次内存映射的变异版本测试把缓存数据映射到匿名共享内存性能提升了大约 15%但复杂度上升了一个量级而且需要在原生侧写不少代码。我的判断是如果业务不是对内存读写在纳秒级别有极致要求完全没必要上 FFI 方案纯 Dart 的 Map 操作已经足够。6. 绕不开的坑鸿蒙适配中遇到的典型问题与排查记录6.1 Timer 在 Ability 进入后台后被冻结这是我遇到最诡异的问题。现象是App 退到后台再回来memory_cache里的一部分键值对消失且没有走remove。日志排查发现是Timer.periodic在 Ability 后台被系统冻结恢复前台后定时器继续运行但清理逻辑跑了一次“清掉所有看起来过期的键”而实际上这些键的过期时间并没有到。原因其实不复杂鸿蒙的 Ability 在进入后台时会将关联的 Flutter 引擎进程挂起suspend包括 Dart 的 isolate 和所有 Timer。如果你的清缓存逻辑基于“当前时间 - 写入时间 阈值”来判断那 suspend 的时间会被算进缓存年龄里恢复的时候大量键被误判为过期。解决办法不要在后台执行清理把Timer.periodic绑定到WidgetsBindingObserver的AppLifecycleState上只有状态是resumed时才执行清理。兼容层的_cleanupExpired方法必须检查当前生命周期状态。6.2 平台通道阻塞导致的数据不一致有段时间我遇到了一个非常隐蔽的 bug某些页面数据明明写进了 ArkUI 侧的原生缓存但 Flutter 侧读出来的还是旧值。排查很久才发现是 MethodChannel 的调用方式问题——我在 ArkUI 侧用了异步回调返回结果但 Flutter 侧用MethodChannel.invokeMethod时没有正确处理Future导致新旧值交替返回。这个问题的根因在于跨端通信天然是异步的而memory_cache的口号是“同步”如果你的架构里缓存被设计为跨 Flutter 和 ArkUI 共享就必须自己加一层同步语义。比如在 Flutter 侧用一个Completer等待原生返回或者干脆像我们前面设计的那样高频数据放 Dart 侧避免跨端同步。6.3 缓存数据格式在鸿蒙端的序列化陷阱memory_cache的set方法接受的dynamic类型很灵活但在鸿蒙端跟原生侧交互时会涉及一个隐性的 JSON 序列化问题。Flutter 侧的MapString, dynamic在通过 MethodChannel 传给 ArkUI 侧时会被自动转为 JSON 格式但如果原值包含Uint8List、DateTime这类非 JSON 原生类型序列化就会失败或丢失类型信息。解决思路在写兼容层时统一加一个toCacheValue方法把非 JSON 类型显式转成可序列化的形式比如Uint8List转 base64 字符串DateTime转毫秒时间戳。6.4 快速排查表现象可能原因排查顺序缓存键在后台回来后丢失Ability 挂起导致 Timer 误判过期检查生命周期绑定读取到旧值平台通道异步竞态检查是否跨端读写共享缓存内存在大量写入后持续增长过期键未惰性清理检查定时清理是否被挂起低端设备上偶尔卡顿大对象序列化耗时检查是否触发了跨端传输7. 写在最后的几点实操经验跑完整个适配流程后我个人的体会是memory_cache的鸿蒙化适配真正的难点不在 Dart 层而在你对鸿蒙整个 Flutter 运行机制的理解。鸿蒙不是 Android 换壳它的 Ability 生命周期、引擎进程管理、ArkUI 与 Flutter 的通信机制都需要重新适应。如果你只是机械地把 Android 上的写法搬到鸿蒙大概率会踩到我前面说的那些坑。有几个小细节值得单独说一下。一个是dispose方法一定要在 Ability 销毁时调用特别是你用HarmonyMemoryCache承载了大量大对象比如图片字节数组时不做释放的话切页面多了很可能会出现“内存缓慢上涨但看起来毫无规律”的问题。另一个是不要让缓存库跟业务逻辑耦合太深兼容层在设计上尽量保持接口纯净后续鸿蒙 SDK 升级从 API 9 到 API 12 再到更新版本时你只需要改兼容层业务代码完全不动。最后如果你在做鸿蒙化迁移建议小步快跑先挑memory_cache这种简单、纯 Dart 的库试水跑通整套构建、运行、调试流程再去做那些带原生代码的重型插件。这个过程本身就是对整个鸿蒙工具链的一次全面体检体检完再上大项目心里会踏实很多。
返回列表