HarmonyOS技术精讲-Network Kit(网络服务)| 02 文件传输大师:上传下载与断点续传

📅 2026/7/8 12:00:37
HarmonyOS技术精讲-Network Kit(网络服务)| 02 文件传输大师:上传下载与断点续传
HarmonyOS技术精讲-Network Kit网络服务| 02 文件传输大师上传下载与断点续传开篇上传下载不只是调一下APIHarmonyOS开发里上传下载算是最基础的需求了。但很多人第一次接触ohos.net.netmanager时会发现一个现象官方示例能跑起来真机上一测试要么下载到一半进度不动了要么上传大文件时直接崩溃。这个问题在HarmonyOS NEXT开发中比较常见。Network Kit提供了UploadTask和DownloadTask两个核心类但它们的真正使用边界和生命周期管理官方文档描述得比较简略。这篇文章会从两个实际场景出发——头像上传multipart表单和视频文件下载带断点续传把完整的实现逻辑拆开讲清楚。这个能力解决了什么问题Network Kit的文件传输能力本质上是对底层网络IO的封装。它把HTTP请求、进度监听、任务暂停/恢复等操作抽象成了统一的API开发者不需要自己拼装request body也不需要手动管理socket连接。适用场景常规文件上传图片、音频、视频需要进度回调的下载任务需要断点续传的场景不适用场景实时音视频流传输需要用WebSocket超高频的小文件批量上传建议用并发池和直接用http.request相比方案大文件支持进度回调断点续传内存管理http.request容易OOM需要自己实现不支持全量加载Network Kit流式写入原生支持有API支持按块处理对于文件传输类需求Network Kit是更稳定的选择。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机、平板核心实现上传头像multipart表单第一步权限配置在module.json5中声明网络权限{module:{requestPermissions:[{name:ohos.permission.INTERNET}]}}第二步构建上传任务// UploadManager.etsimport{fileIo}fromkit.CoreFileKit;import{netmanager}fromkit.NetworkKit;exportclassUploadManager{/** * 上传头像文件使用multipart/form-data格式 * param fileUri 文件在沙箱中的URI * param progressCallback 进度回调参数为已上传字节数 */staticasyncuploadAvatar(fileUri:string,progressCallback:(sent:number)void):Promisenetmanager.UploadTask{// 1. 构造上传数据constfilefileIo.openSync(fileUri,fileIo.OpenMode.READ_ONLY);constfileSizefileIo.statSync(fileUri).size;// 2. 配置上传任务constuploadConfig:netmanager.UploadConfig{url:https://your-server.com/api/avatar/upload,header:{Content-Type:multipart/form-data},method:POST,files:[{filename:avatar.jpg,name:file,// 表单字段名uri:fileUri,type:image/jpeg}],data:[{name:user_id,value:12345}]};// 3. 创建上传任务constuploadTaskawaitnetmanager.createUploadTask(uploadConfig);// 4. 注册进度回调uploadTask.on(progress,(uploadedSize:number,totalSize:number){// 注意totalSize可能为0需要从文件实际大小获取if(totalSize0){constpercentMath.round((uploadedSize/totalSize)*100);// eg: 日志输出 上传进度: 45%}progressCallback(uploadedSize);});// 5. 开始上传uploadTask.start();returnuploadTask;}}注意files数组中的name字段对应服务端表单的keytotalSize在部分场景可能返回0建议用fileSize做兜底创建任务后需要主动调用start()核心实现视频下载与断点续传断点续传的核心逻辑下载前检查本地是否已有部分文件如果有告诉服务端从哪个字节开始传然后追加写入。// DownloadManager.etsimport{fileIo}fromkit.CoreFileKit;import{netmanager}fromkit.NetworkKit;exportclassDownloadManager{// 保存任务状态privatestatictaskMapnewMapstring,{task:netmanager.DownloadTask;filePath:string;downloadedBytes:number;}();/** * 下载视频支持断点续传 * param url 下载URL * param savePath 保存路径含文件名 * param progressCallback 进度回调 */staticasyncdownloadVideo(url:string,savePath:string,progressCallback:(progress:number)void):Promisevoid{// 1. 检查本地是否存在部分下载文件letdownloadedBytes0;letfileExistsfalse;try{conststatfileIo.statSync(savePath);if(stat.size0){downloadedBytesstat.size;fileExiststrue;}}catch{// 文件不存在从头开始下载}// 2. 创建下载配置constdownloadConfig:netmanager.DownloadConfig{url:url,filePath:savePath,header:{// 关键断点续传需要带上Range头Range:bytes${downloadedBytes}-},enableMetered:true,// 允许在移动网络下下载enableRoaming:true// 允许漫游时下载};// 3. 创建下载任务constdownloadTaskawaitnetmanager.createDownloadTask(downloadConfig);// 4. 保存任务引用this.taskMap.set(url,{task:downloadTask,filePath:savePath,downloadedBytes:downloadedBytes});// 5. 注册进度回调downloadTask.on(progress,(receivedSize:number,totalSize:number){// receivedSize是本次下载的增量不是总大小lettotalDownloadeddownloadedBytesreceivedSize;if(totalSize0){constpercentMath.round((totalDownloaded/totalSize)*100);progressCallback(percent);}});// 6. 注册完成回调downloadTask.on(complete,(){console.log(下载完成);this.taskMap.delete(url);});// 7. 注册失败回调downloadTask.on(fail,(err:Error){// 网络中断等情况// 下次启动时通过检查本地文件大小自动续传});// 8. 开始下载downloadTask.start();}/** * 暂停下载 */staticpauseDownload(url:string):void{constentrythis.taskMap.get(url);if(entry){entry.task.pause();}}/** * 恢复下载 */staticresumeDownload(url:string):void{constentrythis.taskMap.get(url);if(entry){entry.task.resume();}}}为什么这样设计用taskMap保存活跃任务方便管理暂停和恢复Range头是实现断点续传的关键服务端需支持HTTP 206 Partial ContentenableMetered和enableRoaming是HarmonyOS特有的配置项不设置可能在某些网络下被拦截常见问题问题1进度回调不触发现象注册了progress事件但上传/下载过程中回调不触发。原因progress回调在子线程执行如果回调中执行了UI操作如修改State变量ArkUI的线程安全机制会阻止更新。解决方案使用emitBySystem方式或者通过Watch监听状态变化。推荐做法// 使用类变量存储进度值classDownloadModel{progress:number0;}StatedownloadModel:DownloadModelnewDownloadModel();// 在回调中downloadTask.on(progress,(receivedSize:number,totalSize:number){AppStorage.setnumber(downloadProgress,percent);});然后在页面中使用StorageLink或LocalStorageLink监听。问题2断点续传不生效总是从头下载现象设置了Range头但响应依然是200而非206文件从0开始下载。原因官方文档没有明确说明但实际测试中header中的Range头如果直接设置在某些情况下会被底层忽略。正确做法是不显式设置Range而是通过filePath参数结合文件是否存在来判断。解决方案先创建空文件再通过fileIo写入时追加。或者确认服务端支持206// 更可靠的方案下载前先尝试HEAD请求获取服务端支持constresponseawaithttp.httpRequest(url,{method:HEAD});constacceptRangesresponse.header[Accept-Ranges];if(acceptRanges!bytes){// 服务端不支持断点续传重新下载}问题3下载大文件时应用被系统杀死现象下载1GB以上文件时应用退到后台一段时间后被系统回收。原因HarmonyOS为了节省资源会杀后台应用。下载任务需要注册为长时任务。解决方案使用ohos.backgroundTaskManager申请长时任务许可import{backgroundTaskManager}fromkit.BackgroundTasksKit;// 在下载开始前申请constrequestIdbackgroundTaskManager.requestBackgroundTaskRunning(backgroundTaskManager.BackgroundTaskType.DOWNLOAD,{begin:0,interval:180000// 每3分钟延长一次});最佳实践文件路径优先使用沙箱路径不要使用/data/storage/el2/base这样的绝对路径用getContext().cacheDir或getContext().filesDir。不同应用间路径不可通用。下载任务完成后立即销毁监听频繁注册progress事件但不移除会在页面销毁后继续触发回调导致undefined错误。用完记得调用downloadTask.off(progress)。批量上传时控制并发数不要让多个上传任务同时发起建议使用队列或信号量控制并发为3-5个。超过这个数量ArkUI的渲染线程可能出现卡顿。入口文件示例// pages/TransferPage.etsEntryComponentstruct TransferPage{StateuploadProgress:number0StatedownloadProgress:number0build(){Column(){// 上传头像按钮Button(选择头像上传).onClick(async(){// 这里需要配合文件选择器获取fileUriconstfileUriawaitthis.getAvatarUri();awaitUploadManager.uploadAvatar(fileUri,(sent){this.uploadProgresssent;});})// 下载视频按钮Button(下载视频).onClick(async(){constsavePathgetContext().cacheDir/videos/sample.mp4;awaitDownloadManager.downloadVideo(https://example.com/sample.mp4,savePath,(progress){this.downloadProgressprogress;});})}.padding(20)}// 获取头像文件URI的占位方法实际需要调用文件选择器APIasyncgetAvatarUri():Promisestring{// 略return;}}FAQQ为什么真机可以正常下载模拟器上却提示网络错误A模拟器可能无法正确处理部分网络包的校验。真机环境更接近生产建议以真机测试结果为准。Q上传文件时totalSize总是返回0怎么回事A这可能是底层实现的一个限制。建议在创建UploadConfig时从文件系统读取实际大小作为参考值。Q页面销毁后下载任务还在跑如何清理A在页面的aboutToDisappear生命周期中调用downloadTask.off()和downloadTask.off(complete)移除所有回调。