HarmonyOS技术精讲-Network Kit(网络服务)| 02 文件传输大师:上传下载与断点续传
开篇:上传下载不只是调一下API
HarmonyOS开发里,上传下载算是最基础的需求了。但很多人第一次接触@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}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字段对应服务端表单的keytotalSize在部分场景可能返回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 ContentenableMetered和enableRoaming是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分钟延长一次});最佳实践
文件路径优先使用沙箱路径:不要使用
/data/storage/el2/base这样的绝对路径,用getContext().cacheDir或getContext().filesDir。不同应用间路径不可通用。下载任务完成后立即销毁监听:频繁注册
progress事件但不移除,会在页面销毁后继续触发回调,导致undefined错误。用完记得调用downloadTask.off('progress')。批量上传时控制并发数:不要让多个上传任务同时发起,建议使用队列或信号量控制并发为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')移除所有回调。