HarmonyOS 后台下载任务实战:从单文件传输到生产级并发下载管理器
文章目录

每日一句正能量
“朝着自己的方向走,哪怕慢一点,也比在别人的眼光里兜兜转转更有意义。”
别人的眼光是个迷宫,你永远不知道出口在哪;而自己的方向哪怕只是一条小路,每一步都是在向前。速度不重要,方向才重要。
摘要
在移动应用开发中,文件下载是最常见也最考验工程能力的功能之一。本文系统讲解 HarmonyOS 提供的后台下载能力,从
request.agent核心原理出发,逐步构建支持断点续传、多任务并发、优先级调度和异常恢复的完整下载管理器,帮助开发者打造稳定可靠的后台文件传输系统。
一、引言:后台下载的工程挑战
文件下载是移动应用的基础能力,但"接口能跑,体验不稳"是开发者最常遇到的困境。真实项目中,用户下载离线地图包、同步云端相册、更新应用资源包时,网络可能随时断开,应用可能切到后台被系统回收,用户可能手动暂停或取消,服务端也可能返回分片过期。
如果只写一个 download(url) 然后等待回调,失败后很难恢复,也说不清楚当前文件到底传到了哪里。后台下载任务的工程挑战可以归纳为四个核心问题:
- 生命周期管理:应用退后台后,下载任务能否继续执行?进程被回收后如何恢复?
- 断点续传:网络中断后,能否从断点位置继续下载,而不是全量重传?
- 多任务调度:多个文件同时下载时,如何控制并发数、管理优先级、防止网络和内存被打爆?
- 异常恢复:如何区分网络失败、业务失败和用户取消,并采取不同的恢复策略?
本文将围绕 HarmonyOS 的 @ohos.request 模块,从底层原理到上层架构,给出完整的工程解决方案。
二、能力选型:前台任务与后台任务的本质区别
HarmonyOS 的上传下载体系明确区分了前台任务和后台任务两种模式,理解它们的差异是做出正确技术决策的第一步。

| 维度 | 前台任务 | 后台任务 |
|---|---|---|
| 执行时机 | 立即执行,模态界面 | 可等待,任意界面 |
| 生命周期 | 跟随应用生命周期 | 与应用生命周期无关 |
| 数据量 | 通常较小,耗时短 | 通常较大,耗时长 |
| 优先级 | 高,倾斜带宽资源 | 较低,系统统一调度 |
| 典型场景 | 发布朋友圈、微博配图 | 缓存电影、同步数百MB数据 |
| 断点续传 | 不支持 | 支持(需服务端配合) |
核心原则:小文件、用户立即需要的结果用前台任务;大文件、允许后台慢慢完成的用后台任务。后台任务通过
request.agent创建,由系统托管,即使应用被回收,任务仍会在系统层面继续执行。
三、request.agent 后台下载核心原理
3.1 任务托管模型
request.agent 是 HarmonyOS 提供的后台传输代理能力,它将下载任务从应用进程提升到系统服务层管理。这意味着:
- 进程无关性:应用进程被回收后,系统服务继续执行下载,完成后通过回调通知应用。
- 自动恢复:网络条件不满足时任务自动暂停,网络恢复后自动继续(需要 HTTP 服务器支持断点续传)。
- 安全隔离:普通接口仅操作自己创建的任务,任务信息加密存储,防止遍历攻击和恶意静默下载。
3.2 任务状态机
后台下载任务具有完整的状态生命周期:
| 状态 | 说明 |
|---|---|
WAITING |
等待中,等待网络或系统资源 |
RUNNING |
运行中,正在传输数据 |
PAUSED |
已暂停,用户手动暂停或系统资源紧张 |
RETRYING |
重试中,网络失败后自动重试 |
COMPLETED |
已完成,下载成功 |
FAILED |
已失败,超过重试次数或业务错误 |
3.3 关键配置项
import { request } from '@kit.BasicServicesKit';
const config: request.agent.Config = {
action: request.agent.Action.DOWNLOAD, // 下载任务
url: 'https://example.com/file.zip', // 下载地址
method: 'GET', // HTTP方法
saveas: './file.zip', // 保存路径(应用沙箱内)
overwrite: true, // 是否覆盖已存在文件
mode: request.agent.Mode.BACKGROUND, // 后台模式(关键!)
gauge: true, // 启用进度通知
retry: true, // 网络恢复后自动重试
network: request.agent.Network.ANY // 允许任意网络
};
关键提示:
mode: request.agent.Mode.BACKGROUND是启用断点续传的前提。前台模式下任务跟随应用生命周期,退后台即停止;后台模式下任务由系统托管,支持暂停后继续。
四、单文件后台下载实战
4.1 基础下载实现
以下代码展示了创建后台下载任务、监听进度和状态变化的完整流程:
import { request } from '@kit.BasicServicesKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct SingleDownloadPage {
@State downloadProgress: number = 0;
@State downloadState: string = '未开始';
@State currentSize: string = '0 MB';
@State totalSize: string = '0 MB';
private downloadTask: request.agent.Task | undefined = undefined;
private readonly DOWNLOAD_URL = 'https://example.com/large-file.zip';
private readonly SAVE_PATH = './downloads/large-file.zip';
build() {
Column({ space: 20 }) {
Text('单文件后台下载演示')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Text(`状态: ${this.downloadState}`)
.fontSize(14)
.textColor(this.getStateColor())
Progress({ value: this.downloadProgress, total: 100, type: ProgressType.Ring })
.width(120)
.height(120)
Text(`${this.currentSize} / ${this.totalSize}`)
.fontSize(12)
.textColor('#666666')
Row({ space: 15 }) {
Button('开始下载')
.onClick(() => this.startDownload())
.enabled(this.downloadState !== '下载中')
Button('暂停')
.onClick(() => this.pauseDownload())
.enabled(this.downloadState === '下载中')
Button('恢复')
.onClick(() => this.resumeDownload())
.enabled(this.downloadState === '已暂停')
Button('取消')
.onClick(() => this.cancelDownload())
.enabled(this.downloadState === '下载中' || this.downloadState === '已暂停')
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
}
private getStateColor(): ResourceColor {
switch (this.downloadState) {
case '已完成': return '#43A047';
case '下载中': return '#1976D2';
case '已失败': return '#C62828';
case '已暂停': return '#F9A825';
default: return '#666666';
}
}
/**
* 创建并启动后台下载任务
*/
private async startDownload() {
if (this.downloadTask) {
console.info('[Download] 任务已存在,直接启动');
await this.downloadTask.start();
return;
}
const context = getContext(this) as common.UIAbilityContext;
const config: request.agent.Config = {
action: request.agent.Action.DOWNLOAD,
url: this.DOWNLOAD_URL,
method: 'GET',
saveas: this.SAVE_PATH,
overwrite: true,
mode: request.agent.Mode.BACKGROUND,
gauge: true,
retry: true,
network: request.agent.Network.ANY,
title: '文件下载中',
description: '正在下载 large-file.zip'
};
try {
this.downloadTask = await request.agent.create(context, config);
this.registerCallbacks();
await this.downloadTask.start();
this.downloadState = '下载中';
console.info(`[Download] 任务创建成功,tid=${this.downloadTask.tid}`);
} catch (error) {
const err = error as BusinessError;
this.downloadState = '已失败';
console.error(`[Download] 创建任务失败: ${err.code}, ${err.message}`);
}
}
/**
* 注册任务状态监听回调
*/
private registerCallbacks() {
if (!this.downloadTask) return;
// 进度更新
this.downloadTask.on('progress', (progress: request.agent.Progress) => {
const processed = progress.processed || 0;
const sizes = progress.sizes || [0];
const total = sizes[0] || 1;
this.downloadProgress = Math.round((processed / total) * 100);
this.currentSize = this.formatBytes(processed);
this.totalSize = this.formatBytes(total);
console.info(`[Download] 进度: ${this.downloadProgress}%, ${processed}/${total} bytes`);
});
// 下载完成
this.downloadTask.on('completed', (progress: request.agent.Progress) => {
this.downloadState = '已完成';
this.downloadProgress = 100;
console.info('[Download] 下载完成');
});
// 下载失败
this.downloadTask.on('failed', (progress: request.agent.Progress) => {
this.downloadState = '已失败';
console.error('[Download] 下载失败');
});
// 任务暂停
this.downloadTask.on('pause', (progress: request.agent.Progress) => {
this.downloadState = '已暂停';
console.info('[Download] 任务已暂停');
});
// 任务恢复
this.downloadTask.on('resume', (progress: request.agent.Progress) => {
this.downloadState = '下载中';
console.info('[Download] 任务已恢复');
});
// 响应头数据
this.downloadTask.on('response', (response: request.agent.HttpResponse) => {
console.info(`[Download] 响应头: ${JSON.stringify(response.headers)}`);
});
}
/**
* 暂停下载
*/
private async pauseDownload() {
if (!this.downloadTask) return;
try {
await this.downloadTask.pause();
} catch (error) {
console.error('[Download] 暂停失败:', error);
}
}
/**
* 恢复下载(断点续传)
*/
private async resumeDownload() {
if (!this.downloadTask) return;
try {
await this.downloadTask.resume();
} catch (error) {
console.error('[Download] 恢复失败:', error);
}
}
/**
* 取消下载并清理
*/
private async cancelDownload() {
if (!this.downloadTask) return;
try {
await request.agent.remove(this.downloadTask.tid);
this.downloadTask = undefined;
this.downloadState = '未开始';
this.downloadProgress = 0;
console.info('[Download] 任务已取消');
} catch (error) {
console.error('[Download] 取消失败:', error);
}
}
/**
* 格式化字节数为可读字符串
*/
private formatBytes(bytes: number): string {
if (bytes === 0) return '0 MB';
const mb = bytes / (1024 * 1024);
return mb > 1024 ? `${(mb / 1024).toFixed(2)} GB` : `${mb.toFixed(2)} MB`;
}
}
4.2 断点续传原理
后台下载的断点续传基于 HTTP 协议的 Range 请求头实现。当任务暂停或网络中断后恢复时,系统会自动在请求头中附加 Range: bytes=已下载字节- 字段,服务端返回 206 Partial Content 响应,只传输剩余部分。

断点续传的关键条件:
- 服务端支持:HTTP 服务器必须正确处理
Range请求头,返回206状态码。 - 后台模式:
mode必须设置为BACKGROUND,前台任务不支持断点续传。 - 文件一致性:服务端文件在下载期间不能发生变化,否则断点位置可能失效。
- 自动重试:
retry: true配置下,网络恢复后系统会自动尝试续传。
五、多任务并发下载管理器
单文件下载只能解决基础需求,真实项目中往往需要同时管理多个下载任务。以下是一个生产级的并发下载管理器实现,支持优先级队列、并发控制、状态追踪和异常恢复。
5.1 任务模型定义
// src/model/DownloadTypes.ets
export enum TaskStatus {
PENDING = 'pending', // 等待中
DOWNLOADING = 'downloading', // 下载中
PAUSED = 'paused', // 已暂停
COMPLETED = 'completed', // 已完成
FAILED = 'failed', // 失败
CANCELLED = 'cancelled' // 已取消
}
export interface DownloadTask {
taskId: string; // 唯一标识(不用URL,同一URL可能下载多次)
url: string; // 下载地址
fileName: string; // 文件名
filePath: string; // 保存路径
priority: number; // 优先级,数值越小优先级越高(1-10)
status: TaskStatus; // 当前状态
progress: number; // 进度 0-100
downloadedBytes: number; // 已下载字节数
totalBytes: number; // 总字节数
failedReason?: string; // 失败原因
retryCount: number; // 当前重试次数
maxRetry: number; // 最大重试次数
createdAt: number; // 创建时间戳
updatedAt: number; // 更新时间戳
}
5.2 下载管理器核心实现
// src/manager/DownloadManager.ets
import { request } from '@kit.BasicServicesKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { preferences } from '@kit.ArkData';
import { TaskStatus, DownloadTask } from '../model/DownloadTypes';
/**
* 并发下载管理器(单例模式)
* 支持:优先级队列、并发控制、断点续传、异常恢复、状态持久化
*/
export class DownloadManager {
private static instance: DownloadManager | null = null;
// 最大并发下载数
private readonly MAX_CONCURRENT = 3;
// 默认最大重试次数
private readonly DEFAULT_MAX_RETRY = 3;
// 重试退避基数(毫秒)
private readonly RETRY_BASE_DELAY = 2000;
// 当前活跃任务数
private activeCount: number = 0;
// 等待队列(按优先级排序)
private waitingQueue: DownloadTask[] = [];
// 运行中的任务映射:taskId -> request.agent.Task
private runningTasks: Map<string, request.agent.Task> = new Map();
// 所有任务的引用(用于UI绑定)
private allTasks: Map<string, DownloadTask> = new Map();
// 后台长时任务状态
private hasBackgroundTask: boolean = false;
// 持久化存储
private pref: preferences.Preferences | null = null;
// 状态变更监听器(供UI层订阅)
private listeners: Array<(tasks: DownloadTask[]) => void> = [];
public static getInstance(): DownloadManager {
if (!DownloadManager.instance) {
DownloadManager.instance = new DownloadManager();
}
return DownloadManager.instance;
}
private constructor() {
this.initPreferences();
this.restoreTasksFromStorage();
}
/**
* 初始化持久化存储
*/
private async initPreferences() {
try {
const context = getContext(this) as common.UIAbilityContext;
this.pref = await preferences.getPreferences(context, 'download_tasks');
} catch (error) {
console.error('[DownloadManager] 初始化存储失败:', error);
}
}
/**
* 添加下载任务
*/
public async addTask(url: string, fileName: string, priority: number = 5): Promise<string> {
const taskId = `dl_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
const context = getContext(this) as common.UIAbilityContext;
const task: DownloadTask = {
taskId,
url,
fileName,
filePath: `${context.filesDir}/downloads/${fileName}`,
priority,
status: TaskStatus.PENDING,
progress: 0,
downloadedBytes: 0,
totalBytes: 0,
retryCount: 0,
maxRetry: this.DEFAULT_MAX_RETRY,
createdAt: Date.now(),
updatedAt: Date.now()
};
// 检查是否已存在相同文件名的任务
const existing = Array.from(this.allTasks.values()).find(t => t.fileName === fileName && t.status !== TaskStatus.COMPLETED);
if (existing) {
console.warn(`[DownloadManager] 文件 ${fileName} 已有未完成任务,跳过创建`);
return existing.taskId;
}
this.allTasks.set(taskId, task);
this.insertIntoQueue(task);
await this.persistTask(task);
this.notifyListeners();
// 尝试执行
this.processNext();
return taskId;
}
/**
* 按优先级插入等待队列
*/
private insertIntoQueue(task: DownloadTask): void {
let inserted = false;
for (let i = 0; i < this.waitingQueue.length; i++) {
if (this.waitingQueue[i].priority > task.priority) {
this.waitingQueue.splice(i, 0, task);
inserted = true;
break;
}
}
if (!inserted) {
this.waitingQueue.push(task);
}
}
/**
* 处理下一个任务(核心调度逻辑)
*/
private async processNext(): Promise<void> {
if (this.activeCount >= this.MAX_CONCURRENT || this.waitingQueue.length === 0) {
return;
}
const nextTask = this.waitingQueue.shift();
if (!nextTask) return;
nextTask.status = TaskStatus.DOWNLOADING;
nextTask.updatedAt = Date.now();
this.activeCount++;
// 申请后台长时任务(数据传输类型)
await this.ensureBackgroundTask();
try {
const context = getContext(this) as common.UIAbilityContext;
const config: request.agent.Config = {
action: request.agent.Action.DOWNLOAD,
url: nextTask.url,
method: 'GET',
saveas: nextTask.filePath,
overwrite: true,
mode: request.agent.Mode.BACKGROUND,
gauge: true,
retry: true,
network: request.agent.Network.ANY,
title: `下载: ${nextTask.fileName}`,
description: '后台下载中...'
};
const agentTask = await request.agent.create(context, config);
this.runningTasks.set(nextTask.taskId, agentTask);
// 注册回调
agentTask.on('progress', (progress: request.agent.Progress) => {
const processed = progress.processed || 0;
const sizes = progress.sizes || [0];
const total = sizes[0] || 1;
nextTask.downloadedBytes = processed;
nextTask.totalBytes = total;
nextTask.progress = Math.round((processed / total) * 100);
nextTask.updatedAt = Date.now();
this.persistTask(nextTask);
this.notifyListeners();
});
agentTask.on('completed', async () => {
nextTask.status = TaskStatus.COMPLETED;
nextTask.progress = 100;
nextTask.updatedAt = Date.now();
await this.finishTask(nextTask);
});
agentTask.on('failed', async () => {
await this.handleTaskFailure(nextTask);
});
agentTask.on('pause', () => {
nextTask.status = TaskStatus.PAUSED;
nextTask.updatedAt = Date.now();
this.persistTask(nextTask);
this.notifyListeners();
});
await agentTask.start();
this.persistTask(nextTask);
this.notifyListeners();
} catch (error) {
nextTask.failedReason = (error as BusinessError).message;
await this.handleTaskFailure(nextTask);
}
}
/**
* 处理任务失败
*/
private async handleTaskFailure(task: DownloadTask): Promise<void> {
task.retryCount++;
if (task.retryCount <= task.maxRetry) {
// 指数退避重试
const delay = this.RETRY_BASE_DELAY * Math.pow(2, task.retryCount - 1);
console.info(`[DownloadManager] 任务 ${task.taskId} 将在 ${delay}ms 后第 ${task.retryCount} 次重试`);
task.status = TaskStatus.PENDING;
task.updatedAt = Date.now();
this.activeCount--;
this.runningTasks.delete(task.taskId);
setTimeout(() => {
this.insertIntoQueue(task);
this.processNext();
}, delay);
} else {
// 超过最大重试次数,标记为失败
task.status = TaskStatus.FAILED;
task.failedReason = task.failedReason || '超过最大重试次数';
await this.finishTask(task);
}
this.persistTask(task);
this.notifyListeners();
}
/**
* 完成任务(成功或失败)
*/
private async finishTask(task: DownloadTask): Promise<void> {
this.activeCount--;
this.runningTasks.delete(task.taskId);
task.updatedAt = Date.now();
await this.persistTask(task);
this.notifyListeners();
// 检查是否所有任务完成,释放后台长时任务
const hasRunning = Array.from(this.allTasks.values()).some(t =>
t.status === TaskStatus.DOWNLOADING || t.status === TaskStatus.PAUSED
);
if (!hasRunning && this.waitingQueue.length === 0) {
await this.releaseBackgroundTask();
}
// 继续处理队列
this.processNext();
}
/**
* 暂停指定任务
*/
public async pauseTask(taskId: string): Promise<void> {
const task = this.allTasks.get(taskId);
const agentTask = this.runningTasks.get(taskId);
if (task && agentTask && task.status === TaskStatus.DOWNLOADING) {
try {
await agentTask.pause();
// 状态在回调中更新
} catch (error) {
console.error(`[DownloadManager] 暂停任务失败: ${taskId}`, error);
}
}
}
/**
* 恢复指定任务
*/
public async resumeTask(taskId: string): Promise<void> {
const task = this.allTasks.get(taskId);
const agentTask = this.runningTasks.get(taskId);
if (task && agentTask && task.status === TaskStatus.PAUSED) {
try {
await agentTask.resume();
// 状态在回调中更新
} catch (error) {
console.error(`[DownloadManager] 恢复任务失败: ${taskId}`, error);
}
} else if (task && task.status === TaskStatus.FAILED) {
// 失败任务重新入队
task.retryCount = 0;
task.status = TaskStatus.PENDING;
task.failedReason = undefined;
this.insertIntoQueue(task);
this.processNext();
}
}
/**
* 取消指定任务
*/
public async cancelTask(taskId: string): Promise<void> {
const task = this.allTasks.get(taskId);
const agentTask = this.runningTasks.get(taskId);
if (agentTask) {
try {
await request.agent.remove(agentTask.tid);
} catch (error) {
console.error(`[DownloadManager] 移除任务失败: ${taskId}`, error);
}
this.runningTasks.delete(taskId);
this.activeCount--;
}
// 从等待队列移除
const queueIdx = this.waitingQueue.findIndex(t => t.taskId === taskId);
if (queueIdx > -1) {
this.waitingQueue.splice(queueIdx, 1);
}
if (task) {
task.status = TaskStatus.CANCELLED;
task.updatedAt = Date.now();
await this.persistTask(task);
}
this.notifyListeners();
this.processNext();
}
/**
* 申请后台长时任务
*/
private async ensureBackgroundTask(): Promise<void> {
if (this.hasBackgroundTask) return;
try {
const context = getContext(this) as common.UIAbilityContext;
await backgroundTaskManager.startBackgroundRunning(
context,
backgroundTaskManager.BackgroundMode.DATA_TRANSFER
);
this.hasBackgroundTask = true;
console.info('[DownloadManager] 后台长时任务已申请');
} catch (error) {
console.error('[DownloadManager] 申请后台长时任务失败:', error);
}
}
/**
* 释放后台长时任务
*/
private async releaseBackgroundTask(): Promise<void> {
if (!this.hasBackgroundTask) return;
try {
const context = getContext(this) as common.UIAbilityContext;
await backgroundTaskManager.stopBackgroundRunning(context);
this.hasBackgroundTask = false;
console.info('[DownloadManager] 后台长时任务已释放');
} catch (error) {
console.error('[DownloadManager] 释放后台长时任务失败:', error);
}
}
/**
* 持久化任务状态
*/
private async persistTask(task: DownloadTask): Promise<void> {
if (!this.pref) return;
try {
await this.pref.put(task.taskId, JSON.stringify(task));
await this.pref.flush();
} catch (error) {
console.error('[DownloadManager] 持久化任务失败:', error);
}
}
/**
* 从存储恢复任务
*/
private async restoreTasksFromStorage(): Promise<void> {
if (!this.pref) return;
try {
const allKeys = await this.pref.getAllKeys();
for (const key of allKeys) {
const value = await this.pref.get(key, '');
if (value) {
const task: DownloadTask = JSON.parse(value as string);
// 只恢复未完成的任务
if (task.status !== TaskStatus.COMPLETED && task.status !== TaskStatus.CANCELLED) {
task.status = TaskStatus.PENDING;
task.retryCount = 0;
this.allTasks.set(task.taskId, task);
this.insertIntoQueue(task);
}
}
}
console.info(`[DownloadManager] 从存储恢复 ${this.waitingQueue.length} 个任务`);
this.processNext();
} catch (error) {
console.error('[DownloadManager] 恢复任务失败:', error);
}
}
/**
* 获取所有任务列表(供UI绑定)
*/
public getAllTasks(): DownloadTask[] {
return Array.from(this.allTasks.values()).sort((a, b) => b.createdAt - a.createdAt);
}
/**
* 订阅状态变更
*/
public subscribe(listener: (tasks: DownloadTask[]) => void): void {
this.listeners.push(listener);
}
/**
* 取消订阅
*/
public unsubscribe(listener: (tasks: DownloadTask[]) => void): void {
const idx = this.listeners.indexOf(listener);
if (idx > -1) this.listeners.splice(idx, 1);
}
private notifyListeners(): void {
const tasks = this.getAllTasks();
this.listeners.forEach(l => l(tasks));
}
}
5.3 UI 绑定与使用示例
// src/pages/DownloadListPage.ets
import { DownloadManager } from '../manager/DownloadManager';
import { DownloadTask, TaskStatus } from '../model/DownloadTypes';
@Entry
@Component
struct DownloadListPage {
@State taskList: DownloadTask[] = [];
private manager: DownloadManager = DownloadManager.getInstance();
private listener = (tasks: DownloadTask[]) => {
this.taskList = tasks;
};
aboutToAppear() {
this.manager.subscribe(this.listener);
this.taskList = this.manager.getAllTasks();
}
aboutToDisappear() {
this.manager.unsubscribe(this.listener);
}
build() {
Column() {
Text('下载管理器')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin(20)
Row({ space: 10 }) {
Button('添加测试任务')
.onClick(() => this.addTestTasks())
Button('全部开始')
.onClick(() => this.resumeAll())
Button('全部暂停')
.onClick(() => this.pauseAll())
}
.margin(10)
List() {
ForEach(this.taskList, (task: DownloadTask) => {
ListItem() {
this.TaskItemBuilder(task)
}
}, (task: DownloadTask) => task.taskId)
}
.layoutWeight(1)
.divider({ strokeWidth: 1, color: '#EEEEEE' })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
TaskItemBuilder(task: DownloadTask) {
Column({ space: 8 }) {
Row() {
Text(task.fileName)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
Text(this.getStatusText(task.status))
.fontSize(12)
.textColor(this.getStatusColor(task.status))
}
.width('100%')
Progress({ value: task.progress, total: 100 })
.width('100%')
.height(6)
.color('#1976D2')
Row() {
Text(`${task.progress}%`)
.fontSize(11)
.textColor('#666666')
Text(`${this.formatBytes(task.downloadedBytes)} / ${this.formatBytes(task.totalBytes)}`)
.fontSize(11)
.textColor('#666666')
if (task.status === TaskStatus.DOWNLOADING) {
Button('暂停', { buttonStyle: ButtonStyleMode.TEXTUAL })
.fontSize(12)
.onClick(() => this.manager.pauseTask(task.taskId))
} else if (task.status === TaskStatus.PAUSED || task.status === TaskStatus.FAILED) {
Button('恢复', { buttonStyle: ButtonStyleMode.TEXTUAL })
.fontSize(12)
.onClick(() => this.manager.resumeTask(task.taskId))
}
Button('取消', { buttonStyle: ButtonStyleMode.TEXTUAL })
.fontSize(12)
.fontColor('#C62828')
.onClick(() => this.manager.cancelTask(task.taskId))
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
.padding(15)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ top: 8, bottom: 8, left: 12, right: 12 })
}
private addTestTasks() {
this.manager.addTask('https://example.com/file1.zip', '离线地图包.zip', 1);
this.manager.addTask('https://example.com/file2.mp4', '教学视频.mp4', 3);
this.manager.addTask('https://example.com/file3.pdf', '技术文档.pdf', 5);
}
private pauseAll() {
this.taskList
.filter(t => t.status === TaskStatus.DOWNLOADING)
.forEach(t => this.manager.pauseTask(t.taskId));
}
private resumeAll() {
this.taskList
.filter(t => t.status === TaskStatus.PAUSED || t.status === TaskStatus.FAILED)
.forEach(t => this.manager.resumeTask(t.taskId));
}
private getStatusText(status: TaskStatus): string {
const map: Record<string, string> = {
[TaskStatus.PENDING]: '等待中',
[TaskStatus.DOWNLOADING]: '下载中',
[TaskStatus.PAUSED]: '已暂停',
[TaskStatus.COMPLETED]: '已完成',
[TaskStatus.FAILED]: '失败',
[TaskStatus.CANCELLED]: '已取消'
};
return map[status] || status;
}
private getStatusColor(status: TaskStatus): ResourceColor {
switch (status) {
case TaskStatus.COMPLETED: return '#43A047';
case TaskStatus.DOWNLOADING: return '#1976D2';
case TaskStatus.FAILED: return '#C62828';
case TaskStatus.PAUSED: return '#F9A825';
case TaskStatus.PENDING: return '#9E9E9E';
default: return '#666666';
}
}
private formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
}
}
六、异常处理与状态恢复策略
6.1 区分三种失败类型
文件传输失败不能只显示"失败",三种失败的处理完全不同:
| 失败类型 | 典型例子 | 处理方式 |
|---|---|---|
| 网络失败 | 弱网、断网、超时 | 可重试,保留进度,指数退避 |
| 业务失败 | 文件不存在、权限不足、分片过期 | 停止任务,提示原因,不再自动重试 |
| 用户取消 | 手动暂停或取消 | 按用户意图保存或清理 |
6.2 检查点持久化机制
应用进程被系统回收后,内存中的任务状态全部丢失。必须通过持久化存储保存检查点:
// 关键检查点数据
interface DownloadCheckpoint {
taskId: string;
url: string;
filePath: string;
downloadedBytes: number; // 已下载字节数(断点位置)
totalBytes: number; // 文件总大小
status: TaskStatus; // 最后已知状态
etag?: string; // 服务端ETag(校验文件一致性)
lastModified?: string; // 最后修改时间
updatedAt: number; // 检查点保存时间
}
持久化触发时机:
- 每收到进度回调时(节流,如每 5% 或每 10 秒保存一次)
- 任务状态变更时(暂停、失败、完成)
- 应用收到
onWorkStop或aboutToDisappear时
恢复时机:
- 应用启动时从
Preference读取未完成任务 - 将
PENDING状态任务重新入队 - 系统会自动通过
Range请求头从断点续传
6.3 并发控制与资源保护
// 防止资源打爆的保护机制
private readonly MAX_CONCURRENT = 3; // 最大并发数
private readonly MAX_RETRY = 3; // 最大重试次数
private readonly RETRY_BASE_DELAY = 2000; // 重试退避基数
// 指数退避:第1次等2秒,第2次等4秒,第3次等8秒
const delay = RETRY_BASE_DELAY * Math.pow(2, retryCount - 1);

七、权限配置与模块声明
7.1 必要权限
在 module.json5 中声明网络权限和后台任务权限:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
]
}
}
7.2 后台模式声明
如果使用了 backgroundTaskManager.startBackgroundRunning,需要在 module.json5 中声明对应的后台模式:
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"backgroundModes": [
"dataTransfer"
]
}
]
}
}
八、测试验收清单
后台下载的稳定性需要在多种边界场景下验证:
8.1 生命周期测试
- 前台下载时切到桌面,任务是否继续执行
- 息屏待机 30 分钟,下载进度是否正常推进
- 应用被系统回收后重新启动,未完成任务是否自动恢复
- 设备重启后,检查点数据是否完整保留
- 下载过程中接听电话,任务是否被正确暂停/恢复
8.2 网络与异常测试
- 下载中切换 Wi-Fi 到移动数据,任务是否自动恢复
- 断网 1 分钟后恢复,是否从断点续传
- 服务端返回 404/403,是否正确标记为业务失败
- 服务端不支持 Range 请求,是否降级为全量下载
- 存储空间不足时,是否正确暂停并提示用户
8.3 并发与队列测试
- 同时添加 10 个任务,是否只并发执行 3 个
- 高优先级任务是否优先于低优先级任务执行
- 取消正在下载的任务,等待队列中的任务是否立即补充
- 重复添加相同文件,是否正确去重
8.4 性能指标
- 单文件下载速度是否达到网络带宽上限
- 多任务并发时,总带宽是否合理分配
- 进度回调频率是否适中(不过于频繁导致 UI 卡顿)
- 持久化操作是否使用节流策略,避免频繁写磁盘
九、常见问题与最佳实践
Q1:为什么后台任务有时不触发进度回调?
A:gauge: true 只在后台模式下生效,且回调频率由系统控制。如果进度变化很小,系统可能合并回调。建议在回调中更新 UI,但不要依赖回调的精确频率做业务逻辑。
Q2:下载完成后文件在哪里?
A:saveas 路径是相对于应用沙箱的。如果配置为 ./file.zip,实际保存路径是应用 filesDir 下的 file.zip。可以通过 context.filesDir 获取绝对路径。
Q3:如何支持服务端鉴权下载?
A:在 config 中配置 header 字段添加鉴权信息:
const config: request.agent.Config = {
// ...其他配置
header: {
'Authorization': 'Bearer your-token',
'X-Request-ID': generateRequestId()
}
};
Q4:大文件下载时内存占用过高怎么办?
A:request.agent 采用流式写入,不会将整个文件加载到内存。如果自定义实现分片下载,确保每片下载后及时写入磁盘并释放缓冲区,不要将所有分片数据同时保存在内存中。
Q5:如何清理已完成的任务记录?
A:建议定期清理持久化存储中的已完成任务,避免存储膨胀:
// 清理 7 天前已完成的任务
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
for (const [taskId, task] of this.allTasks) {
if (task.status === TaskStatus.COMPLETED && task.updatedAt < sevenDaysAgo) {
await this.pref?.delete(taskId);
this.allTasks.delete(taskId);
}
}
十、总结
HarmonyOS 的 request.agent 为后台下载提供了系统级的可靠托管能力。本文从单文件下载入手,逐步构建了一个支持并发控制、优先级队列、断点续传、异常恢复和状态持久化的生产级下载管理器。核心设计要点总结如下:
- 前台任务与后台任务严格区分:小文件用前台立即传输,大文件用后台可靠下载。
- 后台模式是断点续传的前提:
mode: BACKGROUND让任务脱离应用生命周期,由系统统一调度。 - 优先级队列 + 并发控制:防止多个大任务同时下载打爆网络和内存。
- 检查点持久化是恢复的基础:进程被回收或设备重启后,从持久化存储恢复任务状态。
- 区分网络失败、业务失败和用户取消:三种失败采取完全不同的恢复策略。
- 可观测性决定排障效率:完整的进度追踪、状态日志和错误码记录是生产环境必备。
希望本文能帮助开发者在 HarmonyOS 应用中构建稳定、高效、用户友好的后台文件传输系统。
转载自:https://blog.csdn.net/u014727709/article/details/163729821
欢迎 👍点赞✍评论⭐收藏,欢迎指正
更多推荐


所有评论(0)