Flutter鸿蒙化:Rust FFI集成fast_blurhash实现图片极速占位
Flutter 做一个带大量网络图片的内容页最让人崩溃的不是图片本身而是那种“白屏等待→突然弹图→布局跳动”的体验。尤其是监控告警、巡检记录、商品大图这种场景用户第一诉求是“先看到大致内容”而不是盯着转圈图标干等。BlurHash 就是干这个的把一张图压缩成几十个字符的短哈希客户端先用它渲染一张模糊但轮廓可辨的预览图等原图到位后再替换。而标题里的 fast_blurhash则是这个思路在 Rust 生态里的性能版本再配合 FFI 把它接到鸿蒙的 Flutter 上等于给 HarmonyOS 端补上了一条纯原生级别的极速预览链路。这篇文章我就按自己的实操过程来写从“为什么要绕一圈用 Rust FFI”到交叉编译、so 库落地、Dart 侧绑定再到加载管线接入和排坑全程给出可直接抄的步骤。适合正在做 Flutter 鸿蒙化适配的团队也适合对 Rust 跨平台编译感兴趣、想看看 FFI 在移动端怎么玩的朋友。1. 先讲清楚为什么是 blurhash、为什么是 Rust、为什么是鸿蒙1.1 blurhash 解决的到底是什么焦虑很多人把图片占位简单理解为“先塞一个灰色块”。实际上真正的体验问题在于用户看到一张模糊但结构清晰的预览图时大脑会快速判断“这个信息是否需要我等待”从而极大降低焦虑。BlurHash 的思路是提取图像的低频信息空间上的颜色分布用 DCT 变换的一小撮系数来表征整张图的“色块轮廓”。解码时反向生成一张指定尺寸的模糊图细节全部丢失但构图、色调、明暗都在。具体到实现Wolt 开源的 BlurHash 有两种编码encode和解码decode。编码通常在后端完成把图片变成像LEHV6nWB2yk8pyo0adR*.7kCdn5j这样的短字符串客户端拿到字符串后解码成占位图。整个链路里最需要性能的是解码——因为它在用户设备上高频发生。列表滚动时每张图都要快速产出一帧占位图如果解码要几十毫秒甚至上百毫秒那还不如用纯色占位。这也就解释了为什么“告警图片加载焦虑”特别适合用 BlurHash。值班人员看监控截图、巡检照片他关心的不是像素细节而是“这图拍到的是什么”。先用模糊轮廓把信息量给足再平滑过渡到原图整个交互就顺了。1.2 现有库一大堆为什么偏偏是 fast_blurhashBlurHash 的客户端实现很多Dart 版、C 版、Swift 版都有。我在 Flutter 里最早用的是纯 Dart 实现功能没问题但在中低端设备上批量解码时有可感知的掉帧。后来换成 fast_blurhash性能提升非常明显。这库是 Rust 写的核心逻辑尽量上了 SIMD官方 benchmark 对比原版 C 实现有数倍差距我在 arm64 真机上实测解码一张 32x32 的占位图耗时在微秒级编码一张 1080P 的图也只有几毫秒。除了快选它还有个现实原因Flutter 插件的原生侧往往已经有 C/C 代码如果再引入一个 Dart 实现就等于维护两套逻辑。Rust 编出来的 cdylib 暴露一层 C ABIFlutter 的dart:ffi可以直接调不需要额外的 bridge 框架。更重要的是一旦编译产物能放进鸿蒙的 so 目录它同时也能保持 Android/iOS 的复用——一套 Rust 源码多端共用这才是适配鸿蒙时最划算的投入方式。1.3 鸿蒙 Flutter 的现状以及“适配”到底意味着什么HarmonyOS 的 Flutter 支持现在已经不是“能不能跑”的阶段了而是“插件生态跟不跟得上”。鸿蒙端跑 Flutter 时android/和ios/目录下的原生代码都不能直接用需要ohos平台目录来承载 ArkTS 和原生 native 库。常规操作是把原生能力写成 NAPI 模块或直接塞 so 库再通过 MethodChannel/EventChannel 和 Dart 通信。这里有个关键区别像图片占位这种纯计算任务根本不需要走通道用 FFI 直连 so 里的导出函数就行。通道适合“UI 事件触发原生能力再回传结果”的场景而 blurhash 解码是典型的 CPU 密集计算中间层越少越好。所以针对 fast_blurhash 的鸿蒙化核心工作就是三件事Rust 交叉编译出鸿蒙可加载的 so、Dart 侧通过 FFI 绑定函数、把解码结果接进 Flutter 的图片管线。下面逐个拆。2. 鸿蒙化适配的准备工具链、目标三元组与 C ABI 设计2.1 工具链清单与关键环境变量先说结论适配鸿蒙的 Rust 交叉编译并不需要什么独家神器核心是准备好 OpenHarmony SDK 里的 native 工具链也就是 clang 和 sysroot。我说的“鸿蒙”都指 HarmonyOS NEXT 及对应的 OpenHarmony 底座应用沙箱和 so 加载机制与传统 Android 有差异但 Rust 产物思路一致。我的工作环境是这样一套组合DevEco Studio 最新稳定版、OpenHarmony SDK包含 native 目录、Rust 用 rustup 管理的较新 stable。检查 Rust target 支持时直接跑rustup target list | grep ohos如果看不到aarch64-unknown-linux-ohos说明你的 Rust 版本太老先升级到近一年的 stable 版。这个 target 在 Rust 里已经是内置定义但它不会自动带 std需要rustup target add aarch64-unknown-linux-ohos然后找到 OpenHarmony SDK 的 native 目录一般是$OHOS_SDK_HOME/native里面有llvm、sysroot、build-tools等子目录。后面编译时只需要两样东西llvm/bin/clang和sysroot。注意这里绝对不能拿 Android NDK 的 clang 去编鸿蒙 so虽然都是 aarch64 Linux但 sysroot 里的 libc 实现和依赖库细节不一样编出来的 so 可能在真机上加载失败。2.2 交叉编译的三个关键点target triple、sysroot、链接器Rust 的 target 叫aarch64-unknown-linux-ohosclang 的--target参数则通常是aarch64-linux-ohos。这两者的关系很多人搞混Rust target 是 rustc 的编译目标定义clang 的 target 是底层 C 编译器实际产出代码的目标。配置 C 编译器时需要通过环境变量告诉 cargo 用哪个 clang、带哪些参数。我在项目根目录写了一个.cargo/config.toml内容如下[target.aarch64-unknown-linux-ohos] linker clang ar llvm-ar rustflags [ -C, link-arg--targetaarch64-linux-ohos, -C, link-arg--sysroot/path/to/ohos-sdk/linux/native/sysroot, ]但我不建议在 config.toml 里写死/path/to这种绝对路径更稳的做法是通过环境变量传入。编译脚本里这样写export OHOS_SDK/path/to/ohos-sdk/linux/native export TOOLCHAIN$OHOS_SDK/llvm export SYSROOT$OHOS_SDK/sysroot export CC_aarch64_unknown_linux_ohos$TOOLCHAIN/bin/clang --sysroot$SYSROOT --targetaarch64-linux-ohos export AR_aarch64_unknown_linux_ohos$TOOLCHAIN/bin/llvm-ar export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_LINKER$TOOLCHAIN/bin/clang原理不复杂cargo 在编译带 C 依赖的 crate 时会找CC_target对应的编译器而 Rust 自身代码的链接交给了CARGO_TARGET_target_LINKER。把两个都指向同一个 clang并让 clang 知道 sysroot 在哪就能同时搞定 Rust 代码和 C 依赖的编译。如果还编了涉及 C 代码的 crate要确保 C 依赖也拿到同样的--target和 sysroot否则 ODR 问题不明显但链接阶段大概率会报一堆未定义符号。2.3 想清楚对外暴露哪些 C ABIfast_blurhash 本身是纯 Rust crate默认没有 C 导出。我们要写一层薄薄的 FFI 封装。设计导出函数时我的原则是“只暴露编码、解码、释放内存三个函数”不暴露任何 Rust 结构体。这样 Dart 侧用DynamicLibrary.open拿到函数指针后就非常简单。编码函数输入原始 RGBA 像素、图片宽高、模糊分量数返回一个 C 字符串指针解码函数输入 hash 字符串、目标宽高、punch 值把像素填到 Dart 传进来的缓冲区里。这里有两个容易踩的坑第一编码返回的字符串是 Rust 侧CString分配的Dart 用完必须调用释放函数否则每编一张图泄漏一块内存第二解码是写入调用方提供的缓冲区缓冲区大小必须由 Dart 侧算准Rust 侧无法从裸指针知道长度所以约定为width * height * 4字节RGBA 顺序。C ABI 的打印和日志也值得考虑。鸿蒙端的 hilog 体系跟 Android logcat 不通用Rust 侧如果 panic默认是 abort不会输出友好信息。所以 FFI 封装建议用兜底逻辑所有边界条件都返回错误码不要让 Rust 的 panic 穿过 FFI 边界。我的每个导出函数都做了空指针判断和参数校验宁可返回错误码也不把异常丢给 Dart 层去猜。3. 实操把 Rust 编译成鸿蒙能用的 .so3.1 手写 FFI 封装层在 Cargo.toml 里先声明好 crate 类型[package] name fast_blurhash_ffi version 0.1.0 edition 2021 [lib] crate-type [cdylib, staticlib] [dependencies] fast_blurhash 2.0cdylib是产出动态库的关键。封装代码我放在src/lib.rs完整结构如下use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_float, c_int}; #[no_mangle] pub extern C fn fbh_encode( width: c_int, height: c_int, components_x: c_int, components_y: c_int, pixels: *const u8, ) - *mut c_char { if pixels.is_null() || width 0 || height 0 { return std::ptr::null_mut(); } let w width as u32; let h height as u32; let len (w * h * 4) as usize; let data unsafe { std::slice::from_raw_parts(pixels, len) }; let hash fast_blurhash::encode(components_x as u32, components_y as u32, w, h, data); match CString::new(hash) { Ok(s) s.into_raw(), Err(_) std::ptr::null_mut(), } } #[no_mangle] pub unsafe extern C fn fbh_encode_free(ptr: *mut c_char) { if !ptr.is_null() { drop(CString::from_raw(ptr)); } } #[no_mangle] pub extern C fn fbh_decode( hash: *const c_char, width: c_int, height: c_int, punch: c_float, pixels: *mut u8, ) - c_int { if hash.is_null() || pixels.is_null() || width 0 || height 0 { return -1; } let hash_str match unsafe { CStr::from_ptr(hash) }.to_str() { Ok(s) s, Err(_) return -2, }; let len (width as usize) * (height as usize) * 4; let out unsafe { std::slice::from_raw_parts_mut(pixels, len) }; match fast_blurhash::decode(hash_str, width as u32, height as u32, punch) { Some(rgba) { out.copy_from_slice(rgba); 0 } None -3, } }几个细节值得说明。第一fast_blurhash::encode的components_x和components_y通常取 4 到 6取太大字符串会变长但清晰度提升有限取太小会糊成一团。我默认在调用方传 4/4效果比较稳。第二from_raw_parts基于调用方保证缓冲区长度所以 Dart 侧必须严格按width * height * 4分配不能想当然。第三fbh_decode返回 0 表示成功负数表示不同错误这样 Debug 时看错误码就能定位问题。3.2 写编译脚本并验证产物我习惯把编译脚本放在项目根目录的scripts/build_ohos.sh#!/usr/bin/env bash set -euo pipefail OHOS_SDK${OHOS_SDK:-/path/to/ohos-sdk/linux/native} TOOLCHAIN$OHOS_SDK/llvm SYSROOT$OHOS_SDK/sysroot TARGETaarch64-unknown-linux-ohos export PATH$TOOLCHAIN/bin:$PATH export CC_${TARGET//-/_}$TOOLCHAIN/bin/clang --sysroot$SYSROOT --targetaarch64-linux-ohos export AR_${TARGET//-/_}$TOOLCHAIN/bin/llvm-ar export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_LINKER$TOOLCHAIN/bin/clang cargo build --release --target $TARGET编译完成后产物在target/aarch64-unknown-linux-ohos/release/libfast_blurhash_ffi.so。注意crate 名是fast_blurhash_ffi所以 so 文件不叫 libfast_blurhash.so这一点很多人会忽略。如果想要指定导出库名可以在 Cargo.toml 里加[lib] name fast_blurhash产物就统一了。验证 so 架构非常关键别等真机加载失败才回头看。用 OpenHarmony SDK 自带的 llvm-readelf 确认$TOOLCHAIN/bin/llvm-readelf -h target/aarch64-unknown-linux-ohos/release/libfast_blurhash.so | grep Machine输出应当是AArch64。如果想顺手看一下导出的符号表用llvm-nm -D找fbh_encode、fbh_decode是否存在。这一步能提前确认#[no_mangle]没写错以及导出链路上没有意外改名。3.3 把 so 库放进 DevEco 工程的正确姿势鸿蒙 Flutter 工程的ohos模块本质是一个 DevEco 工程。第三方 so 最省事的放置位置是模块的libs目录。比如ohos/ entry/ src/ main/ ets/... cpp/... libs/ arm64-v8a/ libfast_blurhash.solibs/arm64-v8a下的 so 会在打包时自动带进 HAP。如果直接放src/main/cpp/libs或者别的地方有可能打不进去加载时就会报dlopen failed。我实测下来在 Flutter 插件工程里还要留意一点插件发布时ohos目录是整个打进 pub 包的so 要放进插件的ohos/entry/libs/arm64-v8a。这样 Flutter 工程集成插件后鸿蒙构建会自动把这部分 so 资源带上不需要宿主工程手动拷贝。另外如果项目里有 C 原生代码走 CMake 方式也能引入 Rust 产物先把 libfast_blurhash.so 放到src/main/cpp/thirdparty/然后在 CMakeLists 里用add_library(fast_blurhash SHARED IMPORTED)声明。不过既然 fast_blurhash 是纯 FFI 调用我建议走 libs 自动打包路径少一层 CMake 维护成本。4. 实操Flutter 插件侧打通 FFI 与图片加载管线4.1 插件工程结构与依赖引入鸿蒙化插件和普通 Flutter 插件的差异主要集中在目录上。标准结构里lib/是 Dart API 层ohos/是鸿蒙原生模块。如果读者是从零开始可以这样创建flutter create --templateplugin --platformsohos fast_blurhash_ohos如果你的 Flutter 版本还没把ohos列为内置 platform就手动创建ohos/目录并在pubspec.yaml里声明flutter:下的 plugin platform 为 ohos。核心要保证的是Dart 代码加载 so 时能正确找到库名构建时 so 能进入 HAP。Dart 侧我们需要dart:ffi和package:ffi两个依赖。package:ffi不是 Flutter 自带库要在pubspec.yaml加dependencies: ffi: ^2.1.0这个库提供了PointerUtf8、toDartString()等便捷方法没有的话字符串转换要自己数长度很痛苦。4.2 Dart FFI 绑定代码import dart:ffi; import dart:typed_data; import package:ffi/ffi.dart; typedef _FbhEncodeNative PointerUtf8 Function( Int32 width, Int32 height, Int32 componentsX, Int32 componentsY, PointerUint8 pixels, ); typedef _FbhEncodeDart PointerUtf8 Function( int width, int height, int componentsX, int componentsY, PointerUint8 pixels, ); typedef _FbhDecodeNative Int32 Function( PointerUtf8 hash, Int32 width, Int32 height, Float punch, PointerUint8 pixels, ); typedef _FbhDecodeDart int Function( PointerUtf8 hash, int width, int height, double punch, PointerUint8 pixels, ); late final DynamicLibrary _lib DynamicLibrary.open(libfast_blurhash.so); late final _FbhEncodeDart _encode _lib .lookupFunction_FbhEncodeNative, _FbhEncodeDart(fbh_encode); late final _FbhDecodeDart _decode _lib .lookupFunction_FbhDecodeNative, _FbhDecodeDart(fbh_decode); late final void Function(PointerUtf8) _encodeFree _lib .lookupFunctionPointerUtf8 Function(PointerUtf8), void Function(PointerUtf8)(fbh_encode_free);Dart 侧包装函数要注意一个细节lookupFunction的泛型参数前一个是 Native 签名后一个是 Dart 签名。原生函数返回Int32时 Dart 侧对应int返回PointerUtf8时 Dart 侧还是PointerUtf8这个对应关系写错会在运行时抛ArgumentError不会在编译期暴露。解码流程是这样Uint8List decodeBlurHash(String hash, int width, int height, double punch) { final output Uint8List(width * height * 4); final hashC hash.toNativeUtf8(); try { final ret _decode( hashC.cast(), width, height, punch, output.buffer.asPointer(), ); if (ret ! 0) { throw Exception(fbh_decode failed: $ret); } return output; } finally { malloc.free(hashC); } }Uint8List.buffer.asPointer()拿到的指针指向 Dart 内存区域Rust 侧往里写 RGBA 数据完成后 Dart 拿这份Uint8List去构建ui.Image全程只有一次 native 调用和一次内存拷贝非常干净。4.3 接入 ImageProvider从 hash 到占位图到真实图上面拿到的是 RGBA 字节流Flutter 里要把它变成图像才能交给Image组件。最直接的办法是实现一个自定义ImageProvider。我写的BlurHashProvider包含四个字段hash、宽、高、punch。核心重写两个方法class BlurHashImage extends ImageProviderBlurHashImage { final String hash; final int width; final int height; final double punch; const BlurHashImage(this.hash, {this.width 32, this.height 32, this.punch 1.0}); override FutureBlurHashImage obtainKey(ImageConfiguration configuration) async this; override ImageStreamCompleter loadImage(BlurHashImage key, ImageDecoderCallback decode) { return OneFrameImageStreamCompleter(_loadFrame(decode)); } FutureImageInfo _loadFrame(ImageDecoderCallback decode) async { final rgba await compute( decodeBlurHash, (_BlurArgs(hash, width, height, punch)), ); final image await decodeImageFromPixels( rgba, width, height, PixelFormat.rgba8888, ); return ImageInfo(image: image); } }有三个地方要重点解释。第一compute把解码任务放到后台 isolate避免 RGB 解码阻塞 UI 线程。实测在低端机上 32x32 的解码虽然只有微秒级但列表里同时出现十几张图时后台 isolate 能显著减少掉帧。第二obtainKey里直接返回this等于每个不同的 hash 都是独立缓存 keyFlutter 默认的ImageCache会自动缓存解码结果当网络图替换完成后占位图的ImageInfo会被正常释放不会常驻内存。第三decodeImageFromPixels的宽高是占位图尺寸不是原图尺寸。占位图本身只需要 32x32 甚至更小因为后续显示会由BoxFit放大模糊图的特性决定了放大后观感依然成立。真实图片加载完成后替换动画我习惯用AnimatedSwitcher淡入淡出 300ms 左右AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: imageLoaded ? Image.network(url, key: ValueKey(url)) : Image(image: BlurHashImage(hash), key: const ValueKey(blur)), )AnimatedSwitcher在 key 变化时会先展示旧 child 再淡入新 child。这里有个深坑如果网络图 URL 和 hash 都没有变化AnimatedSwitcher认为 child 没变不会触发动画。所以在图片组件层级上给两个 child 设置不同的 key 是必须的否则替换会变成瞬间跳变。4.4 与平台通道、路由共存的一点经验这个方案完全没有用到 MethodChannel/NAPI所以不会和 EventChannel、Navigation 冲突。之前有同事问blurhash 图片会不会在页面切换后重新解码其实不会因为 ImageCache 的 key 是BlurHashImage对象页面 A 和页面 B 只要构造参数一致内存里就是同一份缓存路由切换不会触发重新 loadImage。这一点在带 tab 的多页面场景里很香等于白拿了 Flutter 默认缓存机制的红利。5. 常见问题与排查技巧实录5.1 一张问题速查表现象可能原因排查/解决dlopen failed: library libfast_blurhash.so not foundso 没进 HAP或路径不在默认目录确认 so 在ohos/entry/libs/arm64-v8a/下重新 build用hdc shell查看应用 lib 路径dlopen failed: cannot locate symbolRust 侧依赖了目标 sysroot 没有的符号检查是否用了错误的 NDK 编 C 依赖确认 sysroot 指向 OpenHarmony 的 nativefbh_decode返回-1传入的 hash 字符串为空或缓冲区为空检查 Dart 侧是否把空串传过来了增加判空保护返回-2hash 不是合法 UTF-8 或格式非法检查后端下发的 hash 是否完整有没有被截断返回-3解码器无法从 hash 生成像素确认 hash 字符串长度和分量数匹配不要手改 hash解码结果偏色/是乱码像素格式顺序不对确认 Dart 侧PixelFormat.rgba8888且 Rust 输出顺序为 R,G,B,A模拟器能跑、真机崩溃x86_64 和 arm64 产物混用分别为模拟器与真机编译并放置对应架构的 so模拟器放 x86_64真机放 arm64-v8a内存持续增长编码返回的字符串没有释放调_encodeFree释放PointerUtf8解码场景检查是否有循环调用没走缓存hilog 里看不到 Rust 日志Rust 灭 log 默认不输出到 hilogRust 侧不依赖println!通过返回值/错误码调试或用 hilog 手动封装 FFI 日志函数5.2 最容易被坑的几个细节第一坑是 so 库名。Cargo 的[lib] name决定产物文件名但很多 FFI 封装例子没有显式设置name默认是 crate 名加下划线。Dart 侧DynamicLibrary.open写错了文件名还经常不是报“文件不存在”而是报“找不到符号”或者“library not found”反复横跳。我的建议是显式把库名设成简洁的单词并且每次改名后同时更新 Dart 侧的 open 参数别留两套记忆。第二坑是并发问题。FFI 本身没有 isolate 限制但DynamicLibrary.open的句柄是整个进程共享的。多个 background isolate 同时调用decodeBlurHash时Rust 侧会并行执行fast_blurhash 内部是只读操作线程安全没问题。但要注意一点同一个 isolate 里连续调用大量解码时Dart 的compute会创建新 isolate不必自己管理线程池Flutter 会复用 root isolate 的 worker。如果有极端高频的列表滚动场景建议自己加一层节流不要每次图片出现都立刻 decode以“占位图按需生成”为原则。第三坑是punch参数对观感的影响。punch大于 1 时模糊图的对比度会增强某些颜色会显得发暗偏色小于 0.5 会糊成一片。我在不同设备上对比后默认值 1.0 最安全但暗色系 UI 里把 punch 调到 0.8 左右观感更柔和。这个参数完全可以做成后端下发客户端不用内置太多风格选择。第四坑是发布时 so 的压缩和 strip。Rust release 产物默认带调试符号so 体积可能有几 MB。发布前建议用llvm-strip去掉符号表$TOOLCHAIN/bin/llvm-strip -s target/aarch64-unknown-linux-ohos/release/libfast_blurhash.sostrip 后 so 通常只有几十 KB 到一两百 KB对 HAP 体积影响很小。这个优化要放在构建脚本里别在 release 分支忘记执行。第五坑是 CI 和本地的 Rust target 不一致。如果团队里有人本机能编、CI 上编不过优先检查 rustup target 列表和 SDK 路径是否注入。CI 环境建议显式安装指定 Rust 版本并缓存target/目录否则每次重新编 Rust 依赖会相当耗时。5.3 我自己的调试路径参考真机调试时最有效的组合是先用hdc查看应用是否真的打包了 so再用 log 观察错误码。hdc 查看 HAP 内文件的命令是hdc shell bm dump -n bundleName如果嫌 log 太碎我通常会在 Dart 侧加一个 Debug 开关把decodeBlurHash的输入输出参数打印到控制台。因为 FFI 层没有异常堆栈错误码本身就能快速定位问题在哪一层-1是调用方参数问题-2是字符串类问题-3是 fast_blurhash 内部解码失败。真机上跑不起来时先跑一个最小 demo只调fbh_decode把结果直接渲染到CustomPaint以此确认 FFI 链路通不通。6. 后续还能怎么扩展如果你的团队正在往鸿蒙迁移一批 Flutter 应用fast_blurhash 的适配模式是可以复制的。所有 CPU 密集型的 Rust crate——图像处理、缩略图生成、编解码、哈希计算——都可以用同一套 X 交叉编译 cdylib Flutter FFI 的框架接进鸿蒙。你不需要为每个库写 NAPI 模块只需要保证暴露的 C ABI 是纯函数式的输入输出都是基础类型或缓冲区指针就能一劳永逸。另外一个方向是把编码逻辑也搬到端上。后端不便下发 blurhash 时客户端可以把本地图片比如拍照后的缩略图转成 hash再上传给服务端便于其他端复用。fast_blurhash 的 encode 性能足够支撑这类需求只是要注意大图的 RGBA 数据量如果原图是 4000 万像素一次编码的内存峰值就会比较大建议先压缩成小图再喂给 encode。至于 EventChannel、PlatformView 这些鸿蒙 Flutter 适配里的老话题和 blurhash 没有直接耦合。FFI 方案的好处就是独立于平台通道哪怕原生侧有自定义的图片加载器通过通道回调也不影响 blurhash 占位图的解码与展示。这种“轻耦合”在插件适配时是很大的优点意味着你可以先行把图片占位能力落地再去处理更复杂的混合渲染问题。做鸿蒙适配时我最大的体会就是遇到三方库先别急着找现成的 ohos 版包很多底层纯计算库通过 Rust FFI 反而能更快落地还能顺手把 iOS/Android 三端统一起来。fast_blurhash 只是一个很小的缩影但它的适配路径完整展现了“工具链准备 → 交叉编译 → 内存契约设计 → Dart 绑定 → 图片管线接入”的闭环。下次再遇到类似库按这个节奏走一遍适配速度会比想象中快很多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →