鸿蒙PC迁移_LocalSend 迁移到鸿蒙 PC:一次 Flutter + Rust + 三方库适配的完整记录
文章目录
欢迎加入鸿蒙 PC 开发者社区,共同打造开发者工具生态:鸿蒙 PC 开发者社区:https://harmonypc.csdn.net/
LocalSend-ohos 开源项目地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_localsend
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
环境搭建文章:https://blog.csdn.net/lbcyllqj/article/details/142694676
这篇文章记录的是 LocalSend 在 OpenHarmony / HarmonyOS PC 环境中的一次迁移和适配过程。
LocalSend 是一个开源跨平台文件传输工具,定位接近局域网内的 AirDrop。它本身不是一个简单 Flutter UI 项目,而是一个比较典型的跨端工程:
- 前端界面基于 Flutter。
- 核心通信用 Dart、Rust 和 HTTP API 共同完成。
- Rust 侧通过
flutter_rust_bridge接入 Flutter。 - 网络请求依赖
rhttp,运行时需要对应平台的动态库。 - 文件选择、偏好设置、路径、权限、URL 打开等能力依赖 Flutter 插件。
- 设备发现依赖 UDP multicast、HTTP register、TCP 扫描等网络能力。
所以这次适配不是“把 Flutter 工程跑起来”这么简单。真正的难点在于:Flutter OHOS 工程能构建、Rust 动态库能产出、三方插件能注册、运行时不会因为平台判断失败而崩溃、文件选择能拿到真实可读内容、局域网发现链路能明确区分代码问题和模拟器网络限制。
最终当前状态是:
- LocalSend 主应用可以构建 OHOS HAP。
rhttp已经适配为项目内本地三方库,并能产出 OHOS.so。- LocalSend 自身 Rust FFI 动态库可以产出并随 HAP 打包。
shared_preferences_ohos的 Pigeon 返回值问题已修复。uri_content、network_info_plus在 OHOS 上的启动阻塞已规避。- OHOS 文件选择器返回
file://docs/...后导致“没有权限”的问题已定位并修复。 - 模拟器上“Mac 搜不到 OHOS 设备”的现象已确认主要来自模拟器 NAT 和 UDP 组播限制,而不是服务没启动。
先放一张最终运行效果,给后面的技术细节做一个参照。

一、适配环境与项目基线
本次使用的工程目录是:
/localsend-main
LocalSend 仓库本身包含多个模块,核心目录大致如下:
localsend-main/
├── app/ # Flutter 主应用
│ ├── lib/ # Dart 业务代码
│ ├── ohos/ # OHOS 工程
│ ├── rust/ # LocalSend 自身 Rust 代码
│ └── rust_builder/ # flutter_rust_bridge/cargokit 构建入口
├── common/ # 通用协议、DTO、网络任务
├── core/ # Rust core
└── third_party/ # 本次新增/固化的三方库适配
本次使用的 Flutter OHOS 环境为:
Flutter 3.35.8-ohos-0.0.3
测试设备为:
127.0.0.1:5555
OpenHarmony 5.0.5.316
API 17
常用构建和安装命令如下:
cd ~/XM/localsend-main/app
~/flutter_flutter/bin/flutter build hap --debug --target-platform ohos-arm64
~/flutter_flutter/bin/flutter install -d 127.0.0.1:5555 --debug --device-timeout=10
启动应用和查看日志使用 hdc:
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc \
-t 127.0.0.1:5555 shell aa start \
-a EntryAbility \
-b org.localsend.localsend_app \
-m entry
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc \
-t 127.0.0.1:5555 shell hilog -x

二、第一个阻塞点:rhttp 没有 OHOS 动态库加载路径
LocalSend 的网络请求依赖 rhttp。在普通 Android、iOS、macOS、Windows、Linux 平台上,rhttp 有对应平台的构建和动态库加载逻辑。但迁移到 OHOS 后,最先遇到的问题就是运行时无法识别平台:
loadExternalLibrary failed: Unknown platform=ohos
这类问题的本质不是 Dart 代码写错,而是三方库还没有把 OHOS 当作一个受支持的平台处理。LocalSend 虽然是 Flutter 应用,但 rhttp 底层包含 Rust 产物,运行时必须能找到并加载对应的动态库。
本次处理方式是把 rhttp 固化为项目内的本地三方库:
third_party/rust/rhttp_ohos
然后在 app/pubspec.yaml 中把依赖切到本地路径:
rhttp:
path: ../third_party/rust/rhttp_ohos
简单说,固化 rhttp 的操作可以分成几步:
- 先把当前能用的
rhttp源码从 pub 缓存或上游仓库拷贝到项目内,比如放到third_party/rust/rhttp_ohos。这样后续 OHOS 适配改动就跟随项目一起管理,不再依赖机器本地的.pub-cache。 - 在
rhttp_ohos/pubspec.yaml里补齐 Flutter 插件平台声明,核心是给plugin.platforms增加ohos,并声明ffiPlugin: true。这样 Flutter OHOS 构建时才会把它当成需要 native 构建和打包的 FFI 插件。 - 在
rhttp_ohos/ohos/下补一个 OHOS HAR 插件工程,至少包含oh-package.json5、build-profile.json5、hvigorfile.ts、src/main/module.json5和src/CMakeLists.txt。其中CMakeLists.txt里通过 cargokit 去编译rust/目录下的 Rust crate。 - 给
rhttp自带的 cargokit 增加 OHOS 目标处理,让它能识别ohos-arm64,并转换到 Rust 目标aarch64-unknown-linux-ohos,同时配置 OHOS Native SDK 的clang、sysroot和必要的RUSTFLAGS。 - 修改 Dart 侧动态库加载逻辑。原来的平台判断不认识
ohos,所以会抛Unknown platform=ohos。适配后在 OHOS 分支显式加载librhttp.so。 - 最后回到主应用
app/pubspec.yaml,把rhttp: 0.x.x改成本地path依赖,然后执行flutter pub get和flutter build hap --debug --target-platform ohos-arm64验证。
这里有两个检查点很关键:一个是 .flutter-plugins-dependencies 里能看到 rhttp 的 ohos 插件记录;另一个是最终 HAP 解包或构建中间产物里能看到 librhttp.so。如果这两个点缺一个,运行时大概率还是会加载失败。
适配完成后,HAP 中可以包含:
librhttp.so
同时运行日志里不再出现:
Unknown platform=ohos
这里有一个很重要的经验:跨平台 Flutter 项目迁移鸿蒙时,不能只看 pubspec.yaml 有没有声明依赖,还要看依赖背后有没有 native 产物。只要插件或 package 底层有 Rust、C/C++、FFI、动态库加载,就必须检查它是否真的支持 OHOS。
三、第二个阻塞点:LocalSend 自身 Rust FFI 动态库
LocalSend 自身也包含 Rust 代码,并通过 flutter_rust_bridge 暴露给 Dart 层。对应 Dart 侧可以看到生成代码:
app/lib/rust/frb_generated.dart
app/lib/rust/frb_generated.io.dart
运行时需要加载的库是:
librust_lib_localsend_app.so
适配重点包括:
- 让 Rust crate 能够针对 OHOS arm64 编译。
- 让
rust_builder/ cargokit 能参与 OHOS HAP 构建。 - 确认生成的
.so被打进 HAP。 - 确认 Dart 侧能通过
ExternalLibrary.open('librust_lib_localsend_app.so')加载。
librust_lib_localsend_app.so 的生成链路也可以简单理解成这样:
- LocalSend 的 Rust 代码在
app/rust/,crate 名称是rust_lib_localsend_app。Cargo.toml里把库类型声明为cdylib/staticlib,所以它可以被编译成动态库给 Dart FFI 加载。 flutter_rust_bridge.yaml指定 Rust 输入和 Dart 输出,比如rust_input: crate::api、rust_root: rust/、dart_output: lib/rust。生成后的 Dart 绑定就在app/lib/rust/frb_generated*.dart。app/pubspec.yaml里声明了一个本地插件依赖:
rust_lib_localsend_app:
path: rust_builder
app/rust_builder/pubspec.yaml把这个本地插件声明为 FFI 插件,并补了ohos: ffiPlugin: true。这样主应用构建 HAP 时,Flutter 工具会把rust_builder/ohos当作一个 OHOS native 插件模块参与构建。rust_builder/ohos/src/CMakeLists.txt里通过 cargokit 调用 Cargo,入口大致是apply_cargokit(... ../../../rust rust_lib_localsend_app ...)。也就是说,OHOS 的 CMake/Hvigor 构建过程会反向触发 Rust crate 编译。- cargokit 根据目标平台把 OHOS arm64 映射为
aarch64-unknown-linux-ohos,再用 OHOS Native SDK 里的clang和sysroot做链接,最终生成librust_lib_localsend_app.so,并放到 OHOS 插件的libs/arm64-v8a/这类 native 库目录下。 - HAP 打包时,这个
.so会随rust_lib_localsend_app插件一起进入包内。应用启动后,frb_generated.io.dart里的加载逻辑通过ExternalLibrary.open('librust_lib_localsend_app.so')找到它,Dart 层才能正常调用 Rust 侧能力。
所以这个 .so 不是手工写出来的,也不是 Dart 编译出来的,而是 Flutter OHOS 构建过程中,由 rust_builder 插件、cargokit、Cargo 和 OHOS Native SDK 串起来自动产出的 Rust FFI 动态库。调试这类问题时,可以优先看三处:app/rust/Cargo.toml 的 crate 配置、app/rust_builder/ohos/src/CMakeLists.txt 的 cargokit 入口,以及最终 HAP 里有没有打进 librust_lib_localsend_app.so。
这一步还遇到了 WebRTC 相关依赖问题。LocalSend 的部分 WebRTC 功能依赖 Rust 生态里的网络和系统调用库,其中 nix 0.26.4 对 OHOS 支持不足,导致这条链路无法直接完整启用。
这次采取的是“先保证主传输链路可用”的策略:
- HTTP 发送、接收、发现优先适配。
- WebRTC 相关能力在 OHOS 下先 stub / 禁用。
- 保留后续继续适配 WebRTC 的空间。
这样做是为了避免一个低优先级能力阻塞整个应用迁移。对于 LocalSend 来说,局域网 HTTP 传输是更核心的基础能力,优先让它跑通更符合迁移节奏。
四、第三个阻塞点:shared_preferences_ohos 的 Pigeon 返回值不匹配
应用启动后,另一个比较隐蔽的问题出现在偏好设置插件上:
RangeError (length): Valid value range is empty: 0
从现象上看,它像是 Dart 侧访问数组越界;但根因在 OHOS 插件的 ArkTS 返回值。
shared_preferences_ohos 的 Dart 侧通过 Pigeon 生成的通道调用 OHOS 侧方法。Dart 侧期望写入成功后能收到一个包含布尔值的返回数组,例如:
[true]
但 OHOS ETS 侧部分成功写入路径返回了空数组:
[]
于是 Dart 侧解析返回值时访问第 0 项,直接触发 RangeError。
本次做法是把 shared_preferences_ohos 复制到项目内:
third_party/flutter/shared_preferences_ohos
然后在 app/pubspec.yaml 中使用本地版本:
shared_preferences_ohos:
path: ../third_party/flutter/shared_preferences_ohos
修复点主要集中在:
third_party/flutter/shared_preferences_ohos/ohos/src/main/ets/shared_preferences/SharedPreferencesOhosPlugin.ets
third_party/flutter/shared_preferences_ohos/ohos/src/main/ets/shared_preferences/Messages.ets
除了成功返回值,还同步修复了 StringList 相关通道名:
setEncodedStringList
setDeprecatedStringList
这个问题说明,Flutter 插件迁移时要同时检查三层:
Dart API
Pigeon 生成代码
ArkTS / native 实现
只要任意一层的消息格式不一致,应用就可能在启动早期崩溃,而且错误看起来不一定像插件问题。
五、第四个阻塞点:Android 插件逻辑不能直接套到 OHOS
LocalSend 原有代码里有一些平台判断是面向 Android 的。迁移到 OHOS 后,如果简单把 OHOS 当成 Android,短期可能能绕过编译问题,但运行时会出现更难排查的问题。
典型例子有两个。
第一个是 uri_content。原代码会在 Android 下传入:
AndroidUriContentStreamResolver()
但 OHOS 不是 Android,这个 resolver 在 OHOS 下没有意义。最终调整为只在 Android 下使用:
uriContentStreamResolver: checkPlatform([TargetPlatform.android])
? AndroidUriContentStreamResolver()
: null,
第二个是 network_info_plus。这个插件在 OHOS 下没有 wifiIPAddress 实现,启动时会出现 MissingPlugin 相关问题。最终处理方式是在 OHOS 下跳过 NetworkInfo().getWifiIP(),直接使用 Dart native 的 NetworkInterface.list() 枚举网络地址。
对应日志中可以看到:
Network state: [10.0.2.15]
这说明 OHOS 侧已经能拿到本机地址,后续设备发现、HTTP 服务绑定都可以基于这个地址继续排查。
六、第五个阻塞点:文件选择器“没有权限”其实不是权限问题
这个问题非常典型。用户在 OHOS 模拟器中选择文件后,界面弹出类似“没有权限”的提示。第一反应很容易是去查 module.json5 权限、申请读写权限、签名权限等。
但日志揭示了真实原因:
FileSelectorApiImpl --> documentPickerSelect select files successfully,
documentPicker uris: file://docs/storage/Users/currentUser/Download/img3.png
PathNotFoundException: Cannot retrieve modification time,
path = 'file://docs/storage/Users/currentUser/Download/img3.png'
文件选择器其实已经成功返回了文件 URI。问题在于 LocalSend 后续把这个 URI 当成普通本地文件路径处理了。
原来的通用转换逻辑类似这样:
static Future<CrossFile> convertXFile(XFile file) async {
return CrossFile(
name: file.name,
fileType: file.name.guessFileType(),
size: await file.length(),
path: kIsWeb ? null : file.path,
bytes: kIsWeb ? await file.readAsBytes() : null,
lastModified: kIsWeb ? null : await file.lastModified(),
lastAccessed: null,
);
}
在桌面端,file.path 通常是 /Users/.../xxx.png 这种真实路径;但 OHOS 文件选择器返回的是:
file://docs/storage/Users/currentUser/Download/img3.png
Dart 的 File API 不能把它当作普通路径读取,因此 length()、lastModified() 就会失败。外层 catch 到异常后统一弹出 NoPermissionDialog,于是用户看到的就是“没有权限”。
修复方式是新增 OHOS 专用转换逻辑:先把选择器返回的内容保存到 App 自己的缓存目录,再把缓存中的真实路径交给 LocalSend 后续发送流程。
新增文件:
app/lib/util/native/ohos_file_cache.dart
核心逻辑:
Future<Directory> getOhosSelectedFileCacheDirectory() async {
final directory = Directory(
'${await getCacheDirectory()}/localsend_ohos_selected_files',
);
if (!await directory.exists()) {
await directory.create(recursive: true);
}
return directory;
}
新增转换器:
static Future<CrossFile> convertOhosXFile(XFile file) async {
final displayName = file.name.isEmpty ? 'selected_file' : file.name;
final cachedFile = await createOhosSelectedFileCacheFile(displayName);
await file.saveTo(cachedFile.path);
final size = await cachedFile.length();
return CrossFile(
name: displayName,
fileType: displayName.guessFileType(),
size: size,
thumbnail: null,
asset: null,
path: cachedFile.path,
bytes: null,
lastModified: null,
lastAccessed: null,
);
}
然后在文件选择入口中做平台分发:
converter: checkPlatformIsOhos()
? CrossFileConverters.convertOhosXFile
: CrossFileConverters.convertXFile,
同时清缓存时也清理:
localsend_ohos_selected_files
这个修复完成后,OHOS 选择文件不再把 file://docs/... 直接交给 Dart File,也就不会再误报“没有权限”。

七、第六个阻塞点:模拟器里能看到 Mac,但 Mac 搜不到模拟器
LocalSend 的设备发现依赖 UDP multicast 和 HTTP register。迁移完成后,模拟器上的 LocalSend 可以看到 Mac 端 LocalSend,但 Mac 端 LocalSend 搜不到模拟器。
这个现象很容易被误判为:
- OHOS 端服务没启动。
- UDP 绑定失败。
- rhttp 仍然没适配好。
- 防火墙或权限没有打开。
但实际日志显示,OHOS 端服务是正常启动的:
Network state: [10.0.2.15]
Bind UDP multicast port (ip: [10.0.2.15], group: 224.0.0.167, port: 53317)
Server started. (Port: 53317, HTTPS only)
在模拟器内还可以看到端口监听:
tcp 0 0 0.0.0.0:53317 0.0.0.0:* LISTEN
udp 0 0 0.0.0.0:53317 0.0.0.0:*
进一步通过 hdc fport 转发端口:
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc \
-t 127.0.0.1:5555 fport tcp:53318 tcp:53317
然后在 Mac 上访问:
curl -k https://127.0.0.1:53318/api/localsend/v2/info
可以得到类似返回:
{
"alias": "新鲜的桃子",
"version": "2.1",
"deviceModel": "HarmonyOS",
"deviceType": "desktop",
"download": false
}
这说明 OHOS 端 LocalSend 服务本身是可用的。Mac 搜不到模拟器,核心原因是模拟器网络是 NAT。OHOS 模拟器看到自己的地址是:
10.0.2.15
这个地址对 Mac 所在的真实 Wi-Fi 局域网通常不可达。UDP multicast 也不能像真实局域网设备那样双向工作。因此会出现“模拟器能看到 Mac,但 Mac 看不到模拟器”的单向发现现象。
这不是 rhttp 或 Rust 动态库问题,也不是 LocalSend 服务没有暴露。更准确的结论是:
八、构建、签名与安装
本次用户已经手动完成签名配置,因此适配过程中没有覆盖签名文件。构建使用 Flutter OHOS 命令:
cd ~/XM/localsend-main/app
~/flutter_flutter/bin/flutter build hap --debug --target-platform ohos-arm64
成功后产物为:
app/build/ohos/hap/entry-default-signed.hap
本次构建产物大小约为:
173M
安装命令:
~/flutter_flutter/bin/flutter install \
-d 127.0.0.1:5555 \
--debug \
--device-timeout=10
安装成功日志:
Installing entry-default-signed.hap to 127.0.0.1:5555...
Uninstalling old version...
installing hap. bundleName: org.localsend.localsend_app
验证命令:
~/flutter_flutter/bin/flutter analyze lib
结果:
No issues found!

九、最终验证链路
完成上述适配后,建议按下面顺序验证:
- 启动应用,确认没有
Unknown platform=ohos、RangeError、MissingPlugin wifiIPAddress等启动错误。 - 查看日志,确认
HttpUploadIsolate已就绪。 - 查看日志,确认
Network state能输出 OHOS 本机 IP。 - 在 OHOS 端选择文件,确认不再弹“没有权限”。
- 从 OHOS 端选择 Mac 设备并发送文件,确认 Mac 端能收到请求。
- 打开 OHOS 端接收开关,确认服务端口启动。
- 如果 Mac 搜不到 OHOS 模拟器,用
hdc fport和/api/localsend/v2/info验证服务是否可访问。
启动成功日志中比较关键的几行:
Provider initialized: [Provider<PersistenceService>]
Child isolate is ready: HttpUploadIsolate
Network state: [10.0.2.15]
Server started. (Port: 53317, HTTPS only)
文件选择成功后,关键日志类似:
Cached OHOS selected file:
file://docs/storage/Users/currentUser/Download/img3.png
-> .../localsend_ohos_selected_files/..._img3.png

十、迁移过程中的经验总结
这次 LocalSend 迁移最重要的经验,是不要把所有问题都归类成“鸿蒙不兼容”。跨平台项目迁移时,问题通常分布在不同层级。
第一类是三方库平台适配问题。比如 rhttp,Dart 层依赖看起来正常,但 native 动态库加载逻辑不认识 OHOS,就会在运行时失败。解决这类问题,需要把三方库本地化,补齐 OHOS 构建和加载路径。
第二类是 Flutter 插件通道问题。比如 shared_preferences_ohos,表面是 Dart RangeError,根因却是 ArkTS 侧 Pigeon 返回值不符合 Dart 侧预期。解决这类问题,要沿着 Dart API、Pigeon 生成代码、ArkTS 实现一路查。
第三类是平台语义差异问题。OHOS 不是 Android,不能简单复用 Android 的 content resolver、Wi-Fi IP 插件、权限模型和文件路径假设。尤其是文件选择器返回的 file://docs/...,它不是普通磁盘路径,需要通过选择器提供的能力读出来,或者复制到 App 可控缓存目录。
第四类是模拟器网络限制问题。LocalSend 这种局域网工具高度依赖 UDP multicast 和真实局域网地址。模拟器 NAT 环境下,服务可以启动,HTTP 也可以通过端口转发验证,但 Mac 不一定能自动发现模拟器。这类问题要通过日志和端口验证区分,不能误判成业务代码失败。
第五类是能力取舍问题。WebRTC 相关 Rust 依赖在 OHOS 下还存在系统调用兼容问题,这次没有强行一次性适配所有能力,而是优先保证 HTTP 文件传输链路可用。迁移复杂项目时,先建立可运行、可验证的最小闭环,比一开始追求全能力更稳。
十一、结论
LocalSend 迁移鸿蒙 PC 的难点,不在 Flutter 页面本身,而在跨平台底层能力的完整闭环:
Flutter UI
↓
Flutter 插件
↓
Dart 网络与文件 API
↓
Rust FFI / flutter_rust_bridge
↓
三方 Rust 动态库
↓
OHOS HAP 打包、签名、运行时加载
↓
局域网发现和文件传输
本次适配后,LocalSend 已经具备在 OHOS 环境中启动、加载 Rust 动态库、保存偏好设置、枚举本机 IP、选择文件、启动 HTTP 服务和向 Mac 发送文件的基础能力。
仍然需要明确的边界是:在 OpenHarmony 模拟器 NAT 网络中,Mac 官方 LocalSend 搜不到模拟器并不等价于应用适配失败。日志和 hdc fport 已经证明 OHOS 端服务在 53317 端口正常工作;要验证双向自动发现,应该放到真实鸿蒙 PC 设备或桥接网络环境中继续测试。
这也是迁移局域网工具时最容易踩的坑:有些问题是代码问题,有些问题是插件问题,有些问题是三方 native 库问题,还有一些只是模拟器网络拓扑的问题。只有把日志、动态库、插件通道、文件路径和网络链路分开验证,才能真正把应用从“能构建”推进到“能使用”。
更多推荐



所有评论(0)