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

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

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

开篇:上传下载不只是调一下API

HarmonyOS开发里,上传下载算是最基础的需求了。但很多人第一次接触@ohos.net.netmanager时,会发现一个现象:官方示例能跑起来,真机上一测试,要么下载到一半进度不动了,要么上传大文件时直接崩溃。

这个问题在HarmonyOS NEXT开发中比较常见。Network Kit提供了UploadTaskDownloadTask两个核心类,但它们的真正使用边界和生命周期管理,官方文档描述得比较简略。这篇文章会从两个实际场景出发——头像上传(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}from'@kit.CoreFileKit';import{netmanager}from'@kit.NetworkKit';exportclassUploadManager{/** * 上传头像文件,使用multipart/form-data格式 * @param fileUri 文件在沙箱中的URI * @param progressCallback 进度回调,参数为已上传字节数 */staticasyncuploadAvatar(fileUri:string,progressCallback:(sent:number)=>void):Promise<netmanager.UploadTask>{// 1. 构造上传数据constfile=fileIo.openSync(fileUri,fileIo.OpenMode.READ_ONLY);constfileSize=fileIo.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. 创建上传任务constuploadTask=awaitnetmanager.createUploadTask(uploadConfig);// 4. 注册进度回调uploadTask.on('progress',(uploadedSize:number,totalSize:number)=>{// 注意:totalSize可能为0,需要从文件实际大小获取if(totalSize>0){constpercent=Math.round((uploadedSize/totalSize)*100);// eg: 日志输出 "上传进度: 45%"}progressCallback(uploadedSize);});// 5. 开始上传uploadTask.start();returnuploadTask;}}

注意:

  • files数组中的name字段对应服务端表单的key
  • totalSize在部分场景可能返回0,建议用fileSize做兜底
  • 创建任务后需要主动调用start()

核心实现:视频下载与断点续传

断点续传的核心逻辑:下载前检查本地是否已有部分文件,如果有,告诉服务端从哪个字节开始传,然后追加写入。

// DownloadManager.etsimport{fileIo}from'@kit.CoreFileKit';import{netmanager}from'@kit.NetworkKit';exportclassDownloadManager{// 保存任务状态privatestatictaskMap=newMap<string,{task:netmanager.DownloadTask;filePath:string;downloadedBytes:number;}>();/** * 下载视频,支持断点续传 * @param url 下载URL * @param savePath 保存路径(含文件名) * @param progressCallback 进度回调 */staticasyncdownloadVideo(url:string,savePath:string,progressCallback:(progress:number)=>void):Promise<void>{// 1. 检查本地是否存在部分下载文件letdownloadedBytes=0;letfileExists=false;try{conststat=fileIo.statSync(savePath);if(stat.size>0){downloadedBytes=stat.size;fileExists=true;}}catch{// 文件不存在,从头开始下载}// 2. 创建下载配置constdownloadConfig:netmanager.DownloadConfig={url:url,filePath:savePath,header:{// 关键:断点续传需要带上Range头'Range':`bytes=${downloadedBytes}-`},enableMetered:true,// 允许在移动网络下下载enableRoaming:true// 允许漫游时下载};// 3. 创建下载任务constdownloadTask=awaitnetmanager.createDownloadTask(downloadConfig);// 4. 保存任务引用this.taskMap.set(url,{task:downloadTask,filePath:savePath,downloadedBytes:downloadedBytes});// 5. 注册进度回调downloadTask.on('progress',(receivedSize:number,totalSize:number)=>{// receivedSize是本次下载的增量,不是总大小lettotalDownloaded=downloadedBytes+receivedSize;if(totalSize>0){constpercent=Math.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{constentry=this.taskMap.get(url);if(entry){entry.task.pause();}}/** * 恢复下载 */staticresumeDownload(url:string):void{constentry=this.taskMap.get(url);if(entry){entry.task.resume();}}}

为什么这样设计:

  • taskMap保存活跃任务,方便管理暂停和恢复
  • Range头是实现断点续传的关键,服务端需支持HTTP 206 Partial Content
  • enableMeteredenableRoaming是HarmonyOS特有的配置项,不设置可能在某些网络下被拦截

常见问题

问题1:进度回调不触发

现象:注册了progress事件,但上传/下载过程中回调不触发。

原因:progress回调在子线程执行,如果回调中执行了UI操作(如修改@State变量),ArkUI的线程安全机制会阻止更新。

解决方案:使用emitBySystem方式或者通过@Watch监听状态变化。推荐做法:

// 使用类变量存储进度值classDownloadModel{progress:number=0;}@StatedownloadModel:DownloadModel=newDownloadModel();// 在回调中:downloadTask.on('progress',(receivedSize:number,totalSize:number)=>{AppStorage.set<number>('downloadProgress',percent);});

然后在页面中使用@StorageLink@LocalStorageLink监听。

问题2:断点续传不生效,总是从头下载

现象:设置了Range头,但响应依然是200而非206,文件从0开始下载。

原因:官方文档没有明确说明,但实际测试中,header中的Range头如果直接设置,在某些情况下会被底层忽略。正确做法是:不显式设置Range,而是通过filePath参数结合文件是否存在来判断。

解决方案:先创建空文件,再通过fileIo写入时追加。或者确认服务端支持206:

// 更可靠的方案:下载前先尝试HEAD请求获取服务端支持constresponse=awaithttp.httpRequest(url,{method:'HEAD'});constacceptRanges=response.header['Accept-Ranges'];if(acceptRanges!=='bytes'){// 服务端不支持断点续传,重新下载}

问题3:下载大文件时应用被系统杀死

现象:下载1GB以上文件时,应用退到后台一段时间后被系统回收。

原因:HarmonyOS为了节省资源,会杀后台应用。下载任务需要注册为长时任务。

解决方案:使用@ohos.backgroundTaskManager申请长时任务许可:

import{backgroundTaskManager}from'@kit.BackgroundTasksKit';// 在下载开始前申请constrequestId=backgroundTaskManager.requestBackgroundTaskRunning(backgroundTaskManager.BackgroundTaskType.DOWNLOAD,{begin:0,interval:180000// 每3分钟延长一次});

最佳实践

  1. 文件路径优先使用沙箱路径:不要使用/data/storage/el2/base这样的绝对路径,用getContext().cacheDirgetContext().filesDir。不同应用间路径不可通用。

  2. 下载任务完成后立即销毁监听:频繁注册progress事件但不移除,会在页面销毁后继续触发回调,导致undefined错误。用完记得调用downloadTask.off('progress')

  3. 批量上传时控制并发数:不要让多个上传任务同时发起,建议使用队列或信号量控制并发为3-5个。超过这个数量,ArkUI的渲染线程可能出现卡顿。

入口文件示例

// pages/TransferPage.ets@Entry@Componentstruct TransferPage{@StateuploadProgress:number=0@StatedownloadProgress:number=0build(){Column(){// 上传头像按钮Button('选择头像上传').onClick(async()=>{// 这里需要配合文件选择器获取fileUriconstfileUri=awaitthis.getAvatarUri();awaitUploadManager.uploadAvatar(fileUri,(sent)=>{this.uploadProgress=sent;});})// 下载视频按钮Button('下载视频').onClick(async()=>{constsavePath=getContext().cacheDir+'/videos/sample.mp4';awaitDownloadManager.downloadVideo('https://example.com/sample.mp4',savePath,(progress)=>{this.downloadProgress=progress;});})}.padding(20)}// 获取头像文件URI的占位方法,实际需要调用文件选择器APIasyncgetAvatarUri():Promise<string>{// 略return'';}}

FAQ

Q:为什么真机可以正常下载,模拟器上却提示网络错误?
A:模拟器可能无法正确处理部分网络包的校验。真机环境更接近生产,建议以真机测试结果为准。

Q:上传文件时,totalSize总是返回0怎么回事?
A:这可能是底层实现的一个限制。建议在创建UploadConfig时,从文件系统读取实际大小作为参考值。

Q:页面销毁后,下载任务还在跑,如何清理?
A:在页面的aboutToDisappear生命周期中,调用downloadTask.off()downloadTask.off('complete')移除所有回调。