HarmonyOS技术精讲-Network Kit(网络服务)| 02 文件传输大师:上传下载与断点续传
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.ets
import { fileIo } from '@kit.CoreFileKit';
import { netmanager } from '@kit.NetworkKit';
export class UploadManager {
/**
* 上传头像文件,使用multipart/form-data格式
* @param fileUri 文件在沙箱中的URI
* @param progressCallback 进度回调,参数为已上传字节数
*/
static async uploadAvatar(
fileUri: string,
progressCallback: (sent: number) => void
): Promise<netmanager.UploadTask> {
// 1. 构造上传数据
const file = fileIo.openSync(fileUri, fileIo.OpenMode.READ_ONLY);
const fileSize = fileIo.statSync(fileUri).size;
// 2. 配置上传任务
const uploadConfig: 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. 创建上传任务
const uploadTask = await netmanager.createUploadTask(uploadConfig);
// 4. 注册进度回调
uploadTask.on('progress', (uploadedSize: number, totalSize: number) => {
// 注意:totalSize可能为0,需要从文件实际大小获取
if (totalSize > 0) {
const percent = Math.round((uploadedSize / totalSize) * 100);
// eg: 日志输出 "上传进度: 45%"
}
progressCallback(uploadedSize);
});
// 5. 开始上传
uploadTask.start();
return uploadTask;
}
}
注意:
files数组中的name字段对应服务端表单的keytotalSize在部分场景可能返回0,建议用fileSize做兜底- 创建任务后需要主动调用
start()
核心实现:视频下载与断点续传
断点续传的核心逻辑:下载前检查本地是否已有部分文件,如果有,告诉服务端从哪个字节开始传,然后追加写入。
// DownloadManager.ets
import { fileIo } from '@kit.CoreFileKit';
import { netmanager } from '@kit.NetworkKit';
export class DownloadManager {
// 保存任务状态
private static taskMap = new Map<string, {
task: netmanager.DownloadTask;
filePath: string;
downloadedBytes: number;
}>();
/**
* 下载视频,支持断点续传
* @param url 下载URL
* @param savePath 保存路径(含文件名)
* @param progressCallback 进度回调
*/
static async downloadVideo(
url: string,
savePath: string,
progressCallback: (progress: number) => void
): Promise<void> {
// 1. 检查本地是否存在部分下载文件
let downloadedBytes = 0;
let fileExists = false;
try {
const stat = fileIo.statSync(savePath);
if (stat.size > 0) {
downloadedBytes = stat.size;
fileExists = true;
}
} catch {
// 文件不存在,从头开始下载
}
// 2. 创建下载配置
const downloadConfig: netmanager.DownloadConfig = {
url: url,
filePath: savePath,
header: {
// 关键:断点续传需要带上Range头
'Range': `bytes=${downloadedBytes}-`
},
enableMetered: true, // 允许在移动网络下下载
enableRoaming: true // 允许漫游时下载
};
// 3. 创建下载任务
const downloadTask = await netmanager.createDownloadTask(downloadConfig);
// 4. 保存任务引用
this.taskMap.set(url, {
task: downloadTask,
filePath: savePath,
downloadedBytes: downloadedBytes
});
// 5. 注册进度回调
downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
// receivedSize是本次下载的增量,不是总大小
let totalDownloaded = downloadedBytes + receivedSize;
if (totalSize > 0) {
const percent = 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();
}
/**
* 暂停下载
*/
static pauseDownload(url: string): void {
const entry = this.taskMap.get(url);
if (entry) {
entry.task.pause();
}
}
/**
* 恢复下载
*/
static resumeDownload(url: string): void {
const entry = 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监听状态变化。推荐做法:
// 使用类变量存储进度值
class DownloadModel {
progress: number = 0;
}
@State downloadModel: DownloadModel = new DownloadModel();
// 在回调中:
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请求获取服务端支持
const response = await http.httpRequest(url, {
method: 'HEAD'
});
const acceptRanges = response.header['Accept-Ranges'];
if (acceptRanges !== 'bytes') {
// 服务端不支持断点续传,重新下载
}
问题3:下载大文件时应用被系统杀死
现象: 下载1GB以上文件时,应用退到后台一段时间后被系统回收。
原因: HarmonyOS为了节省资源,会杀后台应用。下载任务需要注册为长时任务。
解决方案: 使用@ohos.backgroundTaskManager申请长时任务许可:
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
// 在下载开始前申请
const requestId = 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
@Component
struct TransferPage {
@State uploadProgress: number = 0
@State downloadProgress: number = 0
build() {
Column() {
// 上传头像按钮
Button('选择头像上传')
.onClick(async () => {
// 这里需要配合文件选择器获取fileUri
const fileUri = await this.getAvatarUri();
await UploadManager.uploadAvatar(fileUri, (sent) => {
this.uploadProgress = sent;
});
})
// 下载视频按钮
Button('下载视频')
.onClick(async () => {
const savePath = getContext().cacheDir + '/videos/sample.mp4';
await DownloadManager.downloadVideo(
'https://example.com/sample.mp4',
savePath,
(progress) => {
this.downloadProgress = progress;
}
);
})
}
.padding(20)
}
// 获取头像文件URI的占位方法,实际需要调用文件选择器API
async getAvatarUri(): Promise<string> {
// 略
return '';
}
}
FAQ
Q:为什么真机可以正常下载,模拟器上却提示网络错误?
A:模拟器可能无法正确处理部分网络包的校验。真机环境更接近生产,建议以真机测试结果为准。
Q:上传文件时,totalSize总是返回0怎么回事?
A:这可能是底层实现的一个限制。建议在创建UploadConfig时,从文件系统读取实际大小作为参考值。
Q:页面销毁后,下载任务还在跑,如何清理?
A:在页面的aboutToDisappear生命周期中,调用downloadTask.off()和downloadTask.off('complete')移除所有回调。
更多推荐

所有评论(0)