ARTICLE DETAIL

资讯详情

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

Flutter鸿蒙适配实战:分片上传与断点续传完整移植方案

Flutter鸿蒙适配实战:分片上传与断点续传完整移植方案 把 Flutter 项目搬到鸿蒙上大部分人第一反应是“页面能不能跑起来”。真正做完整适配的人才知道最磨人的从来不是 UI而是那些背后依赖原生能力的功能模块。我这次接到的任务就是把我维护了很久的一个大文件上传模块从基于 chunked_uploader 的实现完整移植到鸿蒙环境而且要保留分片传输和断点续传这两个核心能力。chunked_uploader 这个库本身不是新东西在 Android 和 iOS 上用得很成熟。它的核心价值很直接把一个大文件切成若干小块按需上传上传过程中即使网络断了、进程被杀也能从断点恢复不用从头再来。这在移动端上传视频、日志包、安装包这类场景里是刚需。但到了鸿蒙上问题就变成三件套Dart 层的分片逻辑能不能直接用鸿蒙原生侧的网络能力能不能无缝对接断点续传的状态记录能不能跨平台复用这篇文章我会把这次适配的完整过程拆开讲从环境准备、依赖盘点到分片策略、平台通道实现再到最后的性能调优和踩坑记录。适合正在做 Flutter 鸿蒙适配的开发者参考也适合打算把上传模块改成分片方案的团队用来做技术预演。1. 为什么是 chunked_uploader分片上传与断点续传的核心需求1.1 大文件上传的普遍痛点很多团队在早期做上传功能时都会走捷径读文件到内存一个 POST 请求直接丢给服务端。文件小的时候没问题一旦到了几百 MB 甚至几个 GB这条路就走不通了。我见过不少线上事故用户传一个 200MB 的视频进度走到 60% 网络一抖整单失败又要从头再来。这不仅仅是用户体验差的问题对服务器也是巨大的浪费——上传到一半的连接断掉服务端已经接收的流量全部作废。分片上传解决的就是这个核心矛盾把一个完整任务拆成多个可以独立完成的小任务。每片上传成功就记账失败只需要重传失败的那一片。断点续传则是分片方案的自然延伸既然任务被拆碎了那么理论上只要记录每一片的状态整个任务就能在任何位置恢复。chunked_uploader 的设计思路正好对应这个模型。它不是业界唯一的方案还有 tus protocol、云厂商各自的分段上传 SDK 等但它胜在接口简单、纯 Flutter 实现不依赖特定后端和自建服务端对接最方便。我选它还有一个原因它的协议层可以兼容常见的 Range 请求服务端只要支持 Content-Range 就能跑起来不需要引入一整套新协议栈。1.2 chunked_uploader 的工作机制拆解chunked_uploader 的工作流程可以拆成四个阶段切分、上传、记录、合并。切分阶段把文件按照预设的分片大小分成逻辑片段注意这里是“逻辑片段”而不是真正把文件切割成多个物理文件它只是在读取时按偏移量定位。上传阶段创建多个并发任务各自负责一部分分片的发送。记录阶段维护一个已上传分片的清单这个清单是断点续传的基础。合并阶段在所有分片上传完成后通知服务端把分片拼成完整文件。这个库在 Flutter 端的核心依赖是 dart:io 的 RandomAccessFile 和 HttpClient。RandomAccessFile 负责按偏移量读取指定长度的字节HttpClient 负责发送带 Range 头的请求。这种设计有一个好处整个上传逻辑都在 Dart 层完成不依赖 Android 或 iOS 的原生 SDK。理论上只要是 Flutter 支持的系统这套代码都能跑。但这里有个关键前提——平台必须提供 dart:io 的完整实现。Android 和 iOS 没问题但鸿蒙不同。鸿蒙的 Flutter SDK 是 OpenHarmony 生态那边的移植版本确实提供了一套 dart:io 兼容层但覆盖范围、性能表现和原生平台还有差异。这也是鸿蒙适配中最大的不确定性后面我会专门讲这部分实测结果。1.3 鸿蒙适配的本质Dart 逻辑复用 平台能力补齐分析完 chunked_uploader 的内部机制适配策略就很清楚了。Dart 层的分片调度、状态记录、进度计算这些纯逻辑代码理论上可以 100% 复用。真正需要验证和改造的是它依赖的那些平台能力文件系统的路径规则、网络请求的性能和稳定性、以及权限模型。我把整个适配工作拆成了三层验证层、补齐层、调优层。验证层做的是把原库在鸿蒙模拟器和真机上跑起来看哪些 API 能直接用哪些会抛异常。补齐层针对跑不通的部分用鸿蒙原生能力做桥接。调优层则是在功能跑通后针对鸿蒙的特性做性能调整比如网络超时策略、并发数上限等。这套思路不仅是针对 chunked_uploader任何 Flutter 三方库做鸿蒙适配都可以照这个框架来。2. 鸿蒙适配前置准备环境、依赖与权限盘点2.1 搭建 Flutter 鸿蒙开发环境这次适配的环境搭建比预想中繁琐因为不能直接用官方 Flutter SDK需要切换到支持 OpenHarmony 的版本。我用的组合是OpenHarmony SDK Flutter SDK for OpenHarmony从官方代码仓拉取对应分支 DevEco Studio。需要注意版本匹配问题Flutter SDK 的分支和 OpenHarmony SDK 的 API 版本要对应上否则编译阶段就会报一堆奇奇怪怪的错误。这里我踩过一个坑直接flutter doctor检查环境时会提示当前配置的 Flutter SDK 不被完整支持。这个提示不代表不能编译只是说官方 Flutter 工具链还不认识这个定制版 SDK。处理办法是忽略这个警告只要鸿蒙的设备连接识别正常、构建命令能跑通就行。初次接触的同事容易在这里卡住以为是环境有问题实际上不影响开发。环境搭建完成后建议先跑一个空项目确认链路通了再动手改业务代码。我在这一步浪费了半天就是因为跳过验证直接改业务最后分不清报错是环境问题还是代码问题。2.2 梳理依赖树哪些插件需要鸿蒙实现使用 chunked_uploader 的项目通常不会只依赖它一个库比如获取文件路径可能要 path_provider选择文件可能要 file_selector权限申请可能要 permission_handler。这些库能否在鸿蒙上跑取决于它们是否有 OpenHarmony 平台的实现。我在适配前先做了一轮依赖清单梳理把涉及原生能力的依赖全部列出来逐个查它们在 OpenHarmony 社区里的适配状态。结果比较现实path_provider 有社区维护的鸿蒙实现但版本要手动指定permission_handler 在鸿蒙上不可用权限申请得改为调用系统 API 或自己封装file_selector 也没有现成的鸿蒙实现文件选择得靠 Intent 调起系统文件管理器再用返回的 URI 解析路径。这一步是整个适配计划的基础。没有提前做依赖盘点很容易在编码中途才发现某个关键能力无法使用导致方案返工。我的建议是使用插件之前先看它是否用到了 Federated Plugin 结构——如果插件有多个平台的实现目录鸿蒙适配的难度会低很多因为只需要在插件目录下新增一个 OpenHarmony 平台实现即可。2.3 网络与存储权限的鸿蒙式声明鸿蒙的权限模型和 Android 类似但声明方式不同。网络权限需要在模块的 module.json5 中显式声明否则请求会直接失败。文件读取权限则要看具体场景如果只读取应用沙箱内的文件不需要申请存储权限但如果要获取用户相册里的文件就需要申请媒体库相关权限。我这里拿到的需求是从应用内沙箱上传文件所以只需要声明 INTERNET 权限。但在调试阶段为了从相册选文件测试我额外申请了媒体读取权限。权限声明的写法如下{ module: { name: entry, requestPermissions: [ { name: ohos.permission.INTERNET }, { name: ohos.permission.READ_MEDIA, reason: 上传用户选择的视频文件, usedScene: { abilities: [EntryAbility], when: inuse } } ] } }注意鸿蒙 5.0 之后的权限弹窗策略和 Android 类似运行时会弹出授权请求所以除了配置声明还要在代码里处理授权状态的判断。如果只配置不申请接口调用时依然会被拒绝。3. 适配改造实战分片、续传与平台通道3.1 分片大小与并发数的选型分片参数是整个上传模块最重要的调优项。分片太大单次失败重传的成本高分片太小请求数量暴增HTTP 连接和 TLS 握手的开销会拖慢整体速度。并发数也要权衡太保守速度上不去太激进容易触发服务端限流和内存问题。我在这次适配中给出了一组经验参数分片大小 1MB 到 4MB默认 2MB并发数 2 到 4默认 3。这个组合在移动网络和 WiFi 下都有不错的表现。以 500MB 文件为例2MB 分片意味着 256 个分片3 路并发大约是 256 除以 3 轮请求单轮请求耗时按 200ms 算整体耗时在 20 秒左右属于可接受范围。参数不是写死就完事的我在代码里把它做成了可配置项用户根据网络状态可以切换档位class UploadConfig { final int chunkSize; final int concurrentCount; final Duration connectTimeout; final Duration readTimeout; const UploadConfig({ this.chunkSize 2 * 1024 * 1024, this.concurrentCount 3, this.connectTimeout Duration(seconds: 5), this.readTimeout Duration(seconds: 10), }); }这里有一个工程上的细节并发数不等于无脑开线程Flutter 侧是单 isolate 事件循环真正的并发是靠 HTTP 连接池和异步 IO 撑起来的。chunked_uploader 的并发实现也是基于 Future 并发而不是真的多线程。所以并发数开得再大底层还是同一个事件循环在调度反而是开的请求越多每个请求等待响应的时间越长整体吞吐量不一定线性增长。3.2 断点续传的元数据持久化设计断点续传的核心是把“哪些分片已经上传成功”这个状态可靠地保存下来。chunked_uploader 内部有自己的状态管理能力但跨进程、跨启动的持久化需要自己处理。我的方案是维护一个本地元数据文件记录上传任务的唯一标识、文件路径、分片大小、总片数、已完成分片列表、最后更新时间。存储格式用的是 JSON简单直接出了问题还能手动改{ taskId: 8f7a2b5d-3c1e-4f6a-9b7d-1a2b3c4d5e6f, filePath: /data/storage/el2/base/files/large_video.mp4, fileSize: 524288000, chunkSize: 2097152, totalChunks: 250, completedChunks: [0, 1, 2, 4, 5, 8] }恢复流程是启动上传任务时先检查是否存在对应的元数据文件如果存在把已完成分片从任务列表里剔除只上传剩余部分。这是一个简单的比对逻辑在分片数量几千片时性能也没问题因为只涉及整数集合的操作。真正需要注意的是元数据的写入时机。我会在每片上传成功后追加写入一次但避免频繁全量重写文件。实现上是在内存里维护一个集合然后定时落盘或者用本地数据库。实测下来分片数量在几百到一千的场景落盘频率控制在每 5 秒一次或者每次完成 10 片写一次性能完全能接受。如果文件特别大导致分片上万个可以考虑用更轻量的位图格式替代 JSON。3.3 鸿蒙原生侧实现上传通道跑通 dart:io 的分片逻辑后我遇到一个绕不开的问题在部分鸿蒙版本上Dart 层 HttpClient 发送大体积 PUT 请求时表现不稳定偶尔会出现连接被重置的情况。为了保险起见我针对上传动作做了一个平台通道让鸿蒙原生侧的网络框架来发送分片内容。Flutter 侧的调用封装import package:flutter/services.dart; class HarmonyUploadChannel { static const MethodChannel _channel MethodChannel(com.example.uploader/harmony); static FutureMapdynamic, dynamic uploadChunk({ required String url, required String filePath, required int offset, required int length, }) async { final result await _channel.invokeMethod(uploadChunk, { url: url, filePath: filePath, offset: offset, length: length, }); return Mapdynamic, dynamic.from(result); } }鸿蒙原生侧的 ArkTS 实现核心是用 ohos.net.http 发送 PUT 请求用 ohos.file.fs 定位读取文件片段import http from ohos.net.http; import fs from ohos.file.fs; async function uploadChunk(url: string, filePath: string, offset: number, length: number) { const file fs.openSync(filePath, fs.OpenMode.READ_ONLY); const buffer new ArrayBuffer(length); const readOptions: fs.ReadOptions { offset: offset, length: length }; fs.readSync(file.fd, buffer, readOptions); const httpRequest http.createHttp(); const response await httpRequest.request(url, { method: http.RequestMethod.PUT, header: { Content-Type: application/octet-stream, Content-Range: bytes ${offset}-${offset length - 1}/* }, extraData: buffer }); fs.closeSync(file); return { statusCode: response.responseCode, body: response.result }; }这段代码只是示意不同版本的 OpenHarmony SDK 对 fs 模块的 API 命名略有差异实际开发时请以官方文档为准。走原生通道后上传稳定性确实有明显改善但多了一次平台调用的开销。如果你的场景里 Dart 层 HttpClient 已经足够稳定也可以不引入原生通道减少一层依赖总是更优的选择。3.4 进度回调与错误处理分片上传的进度计算和普通上传不太一样。普通上传只需要知道“已经发送了多少字节”分片上传则需要同时考虑已完成分片的总大小和正在传输中的分片大小。我的进度公式是overallProgress (completedBytes inFlightBytes) / totalFileSize * 100inFlightBytes 的统计比较容易遗漏但如果省略它进度条会在每一片完成时跳变而不是平滑推进。用户体验上一个 2MB 分片完成前进度会卡住几秒钟体感很差。做了即时计算后进度会更接近匀速递增。错误处理这块我采用分片级别重试和任务级别暂停两套机制。分片请求失败时先判断错误类型超时和网络抖动类的错误走指数退避重试最多 3 次如果连续失败达到上限整个任务进入暂停状态等待用户手动恢复。任务暂停时保存元数据并取消未完成的请求恢复时重新读取元数据并继续。有个细节值得提醒Progress 回调如果来自原生平台通道默认是在平台线程返回不能直接操作 UI。我收到回调后会显式切回主 isolate 再刷新状态避免出现 “setState called after dispose” 之类的异常。4. 实测数据与调优记录4.1 实测场景与基线数据适配完成后我在几类场景下做了完整的回归测试WiFi 环境、4G 网络、弱网模拟、以及 App 被强杀后的恢复场景。测试文件选了一个 600MB 的视频分片大小 2MB并发 3。基线数据如下场景总耗时是否续传备注正常 WiFi38.6s否峰值内存 82MB正常 4G62.4s否网络波动较多弱网 30% 丢包超过 180s是自动触发 2 次重试后完成中途杀进程后恢复剩余分片耗时是耗时与剩余片数成正比整体来看分片方案在弱网场景下的价值体现得最明显。没有断点续传能力时一次丢包可能让用户白白等待几分钟甚至更久而分片续传把损失控制在单片重传的范围内。4.2 内存、超时与弱网表现内存峰值是一个必须关注的点。默认并发数乘以分片大小就能估算出基础内存占用比如并发 3、分片 2MB光分片缓冲区就需要 6MB再加上 HTTP 分块传输编码、TLS 缓冲、以及 JSON 解析临时对象实际内存要再乘 2 到 3 倍。我在鸿蒙真机上观测到的峰值在 80MB 左右如果确认分片大小太大比如超过 4MB可能会接近 150MB在低端设备上有 OOM 风险。超时参数的设置也需要注意。移动网络下 10MB 文件的传输链路可能并不长但很多时候服务端逻辑处理慢读取超时设置太短会误杀正常请求。我的经验是连接超时 5 秒、读取超时 30 秒起步根据服务端实际响应时间再收紧。如果服务端有负载均衡或网关层还要考虑网关的超时时间不能小于客户端否则会出现客户端等不起、服务端还没处理完的尴尬局面。弱网环境下并发数要动态调低这也是容易踩坑的地方。我用一个简单的策略如果连续检测到 3 次分片上传超时就把并发数从 3 降到 1等整体进度稳定后再逐步恢复。这个策略可以在上传通道内部用状态机实现不需要额外引入流量监控库。4.3 调优清单做完整个适配和调优后我整理了一份可复用的清单这里直接列出来供参考调优项推荐值调优依据分片大小1MB - 4MB过小增加请求开销过大增加重传成本并发数2 - 4弱网降到 1并发越高单请求延迟越明显连接超时5 秒明显短于读取超时快速失败读取超时30 秒预留服务端处理时间重试次数3 次指数退避超过 3 次基本是网络不可用状态落盘频率每 10 片或 5 秒平衡 IO 开销与恢复粒度进度回调频率每片完成时过高导致 UI 频繁刷新闪烁另外网络请求的缓冲区尽量复用。鸿蒙原生通道实现中每次上传分片都会创建新的 ArrayBuffer分片数量多时 GC 压力很大。我把缓冲区抽成了一个可复用的对象池实测 GC 次数下降了约 40%这个优化在分片数量大的场景下尤为重要。5. 真实踩坑记录与常见问题速查5.1 高频错误码排查速查表适配过程中我记录下了一批高频错误整理成速查表希望能帮后面的人少走弯路错误现象可能原因解决方式MissingPluginException插件没有鸿蒙平台实现检查插件是否有 OpenHarmony 实现目录没有则需要自建通道文件路径找不到鸿蒙沙箱路径规则不同不要硬编码路径通过 path_provider 鸿蒙版获取真实路径SocketException: Connection reset证书校验失败或网关断连检查证书链必要时更新系统 CA 配置请求耗时长但一直不返回读取超时设置过短增大读取超时或降低并发数权限弹窗不出现权限声明位置错误确认 module.json5 的 requestPermissions 配置恢复续传后重复上传某片元数据写入时机太晚确保每片成功后再落盘不要批量落盘其中 MissingPluginException 是跨平台 Flutter 开发最常遇到的问题。解决思路是优先查插件在 pub.dev 上是否有鸿蒙支持的 fork如果找不到就要自己写一个平台通道版本封装不要试图改插件源码强行兼容。5.2 证书校验与明文流量问题鸿蒙网络框架默认启用证书校验自签名证书或私有 CA 签发的证书会直接请求失败。这和 Android 的 SSL 行为类似但配置路径不同。调试阶段如果遇到证书问题不要直接在代码里绕过校验这会在生产环境埋下安全隐患。我当时的处理办法是正式环境使用合规证书测试环境单独准备一个 dev 模式仅在 debug 包中允许弱化证书策略。切到生产包时将 dev 模式代码直接排除在发布编译之外。另外如果服务端接口是 http 明文鸿蒙默认行为也是拒绝的需要按照鸿蒙官方文档配置网络安全策略允许特定域名的明文流量。上线前记得移除这些配置否则会有安全审计风险。5.3 分片读取与写入的暗坑分片上传最隐蔽的坑往往不在网络而在文件读写。我在鸿蒙上遇到过一个奇怪的问题同一个文件用 Dart 的 RandomAccessFile 读取时某些分片的数据会出现截断但用鸿蒙原生 fs 读取却完全正常。排查下来发现是 Dart 兼容层在高版本鸿蒙上对文件偏移量的处理存在边界问题读取到文件尾部时返回的字节数不稳定。解决方法是放弃用 Dart 层读文件统一走原生通道读分片数据。顺带提醒如果你也在做类似适配务必检查文件读取的边界情况文件总大小能否被分片大小整除、最后一片长度小于分片大小、以及偏移量加长度是否越界。这些都是最容易触雷的点。还有个不起眼但很影响体验的问题上传过程中用户切换网络比如从 WiFi 切到移动网络通常 IP 会变化部分服务端会因此断开长连接。wait我的处理方式是监听网络变化事件发现网络变更后立即把当前并发数降为 0等待新网络稳定后再恢复上传而不是任由连接池里的旧连接反复超时。5.4 平台通道调用频率的性能边界走到原生通道上传分片后我一开始天真地把所有分片的读取都放在 Dart 层循环里然后逐片调用原生通道。后果是性能非常差。原生通道每次调用都有消息序列化和进程间通信的开销分片越小、调用越频繁开销占比越高。实测 2MB 分片、256 片每片调用耗时约 8ms累计下来多出了 2 秒左右的纯通道开销。优化办法是批量处理原生通道一次接收多个分片的读取请求返回一个字节数组列表或者当文件较大时直接让原生侧读完整片。合理分组后把调用次数从几百次降到几十次性能提升非常明显。这也是一般 plugin 设计建议不要为小操作频繁开通道优先用 batch API。这次适配做下来我最大的体会是跨平台移植工作里最耗时、最让人头疼的不是业务逻辑重写而是“平台能力差异清单”的维护。Dart 层的分片调度、断点续传状态机只花了约三分之一的时间剩下三分之二全在路径处理、权限配置、网络行为差异、文件系统边界条件这些零零碎碎的地方。如果你接下来也要做类似的鸿蒙适配我建议第一周别急着写代码先把项目里所有涉及原生能力的点列出来逐一对齐 OpenHarmony 的 API 能力。这个清单做得越细后面的开发越顺畅。另一个建议是尽早把弱网模拟环境搭起来分片上传的逻辑在正常网络下很难暴露出问题真正考验它的是网络抖动和进程被杀后的恢复场景。
返回列表