HarmonyOS鸿蒙Next中flutter下载比较大的文件进入后台怎么保证下载成功后才退出长时任务?
HarmonyOS鸿蒙Next中flutter下载比较大的文件进入后台怎么保证下载成功后才退出长时任务? flutter里有个下载模块,鸿蒙端配置后台长时任务最多只能保活十分钟,但是文件可能没下载完,怎么设置长时任务能支撑到下载完成?
解决方案
鸿蒙系统对后台长时任务有严格的生命周期管理。单纯依靠申请长时任务无法保证下载任务无限期运行。系统会在10分钟无进度更新时自动取消长时任务。要实现长时间下载,必须结合以下两种方案:
方案一:使用系统托管下载(推荐)
通过@ohos.request
模块将下载任务托管给系统,即使应用进程被挂起,系统服务仍会继续执行下载。
-
配置长时任务类型与权限 在
module.json5
中声明数据传输类型的长时任务和所需权限:{ "module": { "abilities": [ { "name": "EntryAbility", "backgroundModes": ["dataTransfer"] // 声明数据传输类型 } ], "requestPermissions": [ { "name": "ohos.permission.KEEP_BACKGROUND_RUNNING" // 长时任务权限 }, { "name": "ohos.permission.INTERNET" // 网络权限 } ] } }
-
**使用
request.agent
创建系统托管的后台下载任务,**创建后台任务(mode: request.agent.Mode.BACKGROUND
)并设置断点续传:let config: request.agent.Config = { action: request.agent.Action.DOWNLOAD, url: ' https://xxxx/xxxx.txt ', saveas: filesDir + '/xxxx.txt', mode: request.agent.Mode.BACKGROUND, // 关键:后台任务模式 network: request.agent.Network.ANY, overwrite: true, gauge: true // 启用进度跟踪 }; request.agent.create(context, config).then((task: request.agent.Task) => { task.start((err: BusinessError) => { if (err) { console.error(`启动失败: ${err.message}`); return; } // 设置速度限制(可选) task.setMaxSpeed(1024 * 1024).catch((err) => {}); }); // 监听进度并定期更新(防止10分钟超时) task.on('progress', (progress) => { console.log(`进度: ${progress.processed}`); // 关键:通过更新进度阻止系统回收任务 backgroundTaskManager.updateContinuousTaskNotification({ /*...*/ }); }); });
-
处理任务恢复 应用再次启动时,可通过
request.agent.query()
查询未完成的任务并重新绑定监听器。
方案二:结合能效资源申请(增强保活)
在方案一基础上,申请CPU资源防止进程挂起:
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
// 申请CPU资源
let request: backgroundTaskManager.EfficiencyResourcesRequest = {
resourceTypes: backgroundTaskManager.ResourceType.CPU,
isApply: true,
reason: "文件下载中",
isPersist: true // 持续持有
};
backgroundTaskManager.applyEfficiencyResources(request);
// 下载完成后释放资源
backgroundTaskManager.resetAllEfficiencyResources();
关键注意事项
- 进度更新是关键:系统会监测任务进度,若10分钟内无更新,将自动取消长时任务。必须通过
progress
事件或主动调用updateContinuousTaskNotification
保持活跃。 - 断点续传必需:确保服务器支持HTTP Range请求,否则任务中断后无法恢复。
- 模式选择:务必使用
request.agent.Mode.BACKGROUND
(后台任务),而非FOREGROUND
(前台任务)。 - 限制:此方案适用于大文件下载,但若用户手动杀死应用或系统资源极度紧张,任务仍可能被终止。
更多关于HarmonyOS鸿蒙Next中flutter下载比较大的文件进入后台怎么保证下载成功后才退出长时任务?的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
配置长时任务权限与类型
在module.json5中声明长时任务类型及权限(以数据传输场景为例):
{
"module": {
"abilities": [
{
"backgroundModes": [ "dataTransfer" ] // 配置长时任务类型
}
],
"requestPermissions": [
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING" // 申请长时任务权限
}
]
}
}
鸿蒙的DATA_TRANSFER类型长时任务会挂起应用进程,必须使用系统上传下载代理接口托管任务,而非自行实现下载逻辑:
import download from '@kit.RequestKit';
// 使用系统下载接口托管任务
const task: download.DownloadTask = download.request({
url: 'https://example.com/file.zip',
filePath: '本地存储路径'
});
// 监听下载进度
task.on('progress', (received: number, total: number) => {
console.log(`下载进度: ${(received / total * 100).toFixed(2)}%`);
});
在下载开始前启动长时任务,完成后停止:
import backgroundTaskManager from '@kit.BackgroundTasksKit';
// 开启长时任务
backgroundTaskManager.startBackgroundRunning(context, {
backgroundMode: backgroundTaskManager.BackgroundMode.DATA_TRANSFER
}).then(() => {
console.log('长时任务启动成功');
});
// 下载完成后关闭长时任务
task.on('complete', () => {
backgroundTaskManager.stopBackgroundRunning(context).then(() => {
console.log('长时任务已停止');
});
});
- 在数据传输时,应用需要更新进度。如果进度长时间(超过10分钟)不更新,数据传输的长时任务会被取消。更新进度实现可参考startBackgroundRunning()中的示例。
- 进度更新的频率要控制在每秒十次,否则会报错1600009。
以下载网络视频到应用本地为例,具体步骤如下:
- 添加后台运行权限ohos.permission.KEEP_BACKGROUND_RUNNING和网络权限ohos.permission.INTERNET。
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
},
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
},
],
- 声明数据传输后台模式类型。在module.json5文件中为需要使用长时任务的UIAbility声明相应的长时任务类型,配置文件中填写长时任务类型的配置项。示例如下:
"module": {
"abilities": [
{
"backgroundModes": [
// 配置长时任务类型为数据传输
"dataTransfer"
]
}
],
// ...
}
- 导入长时任务相关模块。
import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { WantAgent, wantAgent } from '@kit.AbilityKit';
import { MessageEvents, worker } from '@kit.ArkTS';
- 在UI页面申请数据传输长时任务,并且通知worker线程下载视频。代码示例如下:
// 模拟一定数量的下载链接
Button('开始下载')
.type(ButtonType.Capsule)
.margin({ top: 10 })
.backgroundColor('#0D9FFB')
.width(300)
.height(40)
.onClick(() => {
this.filePath = this.context.filesDir;
let fileUrlArrTemp: Array<string> = new Array(this.fileCount);
let fileNameArrTemp: Array<string> = new Array(this.fileCount);
for (let index = 0; index < this.fileCount; index++) {
this.fileNameArr[index] = `trailer${index}.mp4`;
fileUrlArrTemp[index] = '替换为自有网络地址/trailer.mp4';
fileNameArrTemp[index] = `trailer${index + 1}.mp4`;
}
workerInstance.postMessage({
code: 200,
context: this.context,
fileUrlArr: fileUrlArrTemp,
filePath: this.filePath,
fileNameArr: fileNameArrTemp
});
this.startContinuousTask();
})
// 申请长时任务
startContinuousTask() {
let wantAgentInfo: wantAgent.WantAgentInfo = {
// 点击通知后,将要执行的动作列表
// 添加需要被拉起应用的bundleName和abilityName
wants: [
{
bundleName: "com.example.downloadfiledemo",
abilityName: "MainAbility"
}
],
// 指定点击通知栏消息后的动作是拉起ability
actionType: wantAgent.OperationType.START_ABILITY,
// 使用者自定义的一个私有值
requestCode: 0,
// 点击通知后,动作执行属性
actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG],
};
try {
// 通过wantAgent模块下getWantAgent方法获取WantAgent对象
wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
try {
let list: Array<string> = ["dataTransfer"];
backgroundTaskManager.startBackgroundRunning(this.context, list, wantAgentObj)
.then((res: backgroundTaskManager.ContinuousTaskNotification) => {
console.info("Operation startBackgroundRunning succeeded");
// 保存通知id,用于更新进度条
this.notificationId = res.notificationId;
})
.catch((error: BusinessError) => {
console.error(`Failed to Operation startBackgroundRunning. code is ${error.code} message is ${error.message}`);
});
} catch (error) {
console.error(`Failed to Operation startBackgroundRunning. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
}
});
} catch (error) {
console.error(`Failed to Operation getWantAgent. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
}
}
- 在worker中使用request.downloadFile接口下载视频,代码示例如下:
function downLoadFile(context: common.UIAbilityContext, fileUrl: string, filePath: string, fileName: string) {
console.info("download fileUrl:" + fileUrl + " fileName:" + fileName + " filePath:" + filePath);
try {
// 需要手动将url替换为真实服务器的HTTP协议地址
isDownLoading = true
request.downloadFile(context, {
url: fileUrl,
filePath: context.filesDir + `/${fileName}`
}, (err: BusinessError, data: request.DownloadTask) => {
if (err) {
console.error(`Failed to request the download. Code: ${err.code}, message: ${err.message}`);
return;
}
let downloadTask: request.DownloadTask = data;
downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
console.info("download receivedSize:" + receivedSize + " totalSize:" + totalSize);
let percent = Math.ceil((receivedSize / totalSize) * 100) // 计算进度值
workerPort.postMessage({
code: 100,
percent: percent, // 当前正在下载的文件进度
currentName: fileName, // 当前正在下载的文件名
completeCount: completeCount // 下载完成的个数
})
});
downloadTask.on('complete', () => {
console.info('download complete');
completeCount++;
isDownLoading = false
})
});
} catch (err) {
console.error(`Failed to request the download. Code: ${err.code}, message: ${err.message}`);
}
}
- 更新下载进度,代码示例如下:
// 应用更新进度
updateProcess(process: Number, fileName: string, completeCount: number) {
// 应用定义下载类通知模版
let downLoadTemplate: notificationManager.NotificationTemplate = {
name: 'downloadTemplate', // 当前只支持downloadTemplate,保持不变
data: {
title: `已下载${completeCount}个/共${this.fileNameArr.length}个`, // 必填
fileName: `正在下载:${fileName}`, // 必填
progressValue: process, // 应用更新进度值,自定义
}
};
let request: notificationManager.NotificationRequest = {
content: {
// 系统实况类型,保持不变
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW,
systemLiveView: {
typeCode: 8, // 上传下载类型需要填写 8,当前仅支持此类型。保持不变
title: `保存视频到相册`, // 应用自定义
text: `正在下载:${fileName}`, // 应用自定义
}
},
id: this.notificationId, // 必须是申请长时任务返回的id,否则应用更新通知失败
notificationSlotType: notificationManager.SlotType.LIVE_VIEW, // 实况窗类型,保持不变
template: downLoadTemplate // 应用需要设置的模版名称
};
try {
notificationManager.publish(request).then(() => {
console.info("publish success, id= " + this.id);
}).catch((err: BusinessError) => {
console.error(`publish fail. code is ${(err as BusinessError).code} message is ${(err as BusinessError).message}`);
});
} catch (err) {
console.error(`publish fail. code is ${(err as BusinessError).code} message is ${(err as BusinessError).message}`);
}
}
完整代码示例如下: Index.ets文件。
import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { WantAgent, wantAgent } from '@kit.AbilityKit';
import { MessageEvents, worker } from '@kit.ArkTS';
const workerInstance = new worker.ThreadWorker("entry/ets/workers/Worker.ets");
@Entry
@Component
struct Index {
context = this.getUIContext().getHostContext() as Context;
private notificationId: number = 0; // 保存通知id
private filePath: string = '';
private fileCount: number = 5;
@State fileNameArr: Array<string> = new Array(this.fileCount);
aboutToAppear(): void {
workerInstance.onmessage = (e: MessageEvents): void => {
if (e.data.code == 100) {
this.updateProcess(e.data.percent, e.data.currentName, e.data.completeCount)
}
if (e.data.code == 200) {
this.stopContinuousTask()
}
}
}
build() {
Column() {
Button('开始下载')
.type(ButtonType.Capsule)
.margin({ top: 10 })
.backgroundColor('#0D9FFB')
.width(300)
.height(40)
.onClick(() => {
this.filePath = this.context.filesDir;
let fileUrlArrTemp: Array<string> = new Array(this.fileCount);
let fileNameArrTemp: Array<string> = new Array(this.fileCount);
for (let index = 0; index < this.fileCount; index++) {
this.fileNameArr[index] = `trailer${index}.mp4`;
fileUrlArrTemp[index] = '替换为自有网络地址/trailer.mp4';
fileNameArrTemp[index] = `trailer${index + 1}.mp4`;
}
workerInstance.postMessage({
code: 200,
context: this.context,
fileUrlArr: fileUrlArrTemp,
filePath: this.filePath,
fileNameArr: fileNameArrTemp
});
this.startContinuousTask();
})
}
.height('100%')
.width('100%')
}
startContinuousTask() {
let wantAgentInfo: wantAgent.WantAgentInfo = {
// 点击通知后,将要执行的动作列表
// 添加需要被拉起应用的bundleName和abilityName
wants: [
{
bundleName: "com.example.downloadfiledemo",
abilityName: "MainAbility"
}
],
// 指定点击通知栏消息后的动作是拉起ability
actionType: wantAgent.OperationType.START_ABILITY,
// 使用者自定义的一个私有值
requestCode: 0,
// 点击通知后,动作执行属性
actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG],
};
try {
// 通过wantAgent模块下getWantAgent方法获取WantAgent对象
wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
try {
let list: Array<string> = ["dataTransfer"];
backgroundTaskManager.startBackgroundRunning(this.context, list, wantAgentObj)
.then((res: backgroundTaskManager.ContinuousTaskNotification) => {
console.info("Operation startBackgroundRunning succeeded");
// 保存通知id,用于更新进度条
this.notificationId = res.notificationId;
})
.catch((error: BusinessError) => {
console.error(`Failed to Operation startBackgroundRunning. code is ${error.code} message is ${error.message}`);
});
} catch (error) {
console.error(`Failed to Operation startBackgroundRunning. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
}
});
} catch (error) {
console.error(`Failed to Operation getWantAgent. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
}
}
// 停止长时任务
stopContinuousTask() {
backgroundTaskManager.stopBackgroundRunning(this.context).then(() => {
console.info(`Succeeded in operationing stopBackgroundRunning.`);
}).catch((err: BusinessError) => {
console.error(`Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
});
}
// 应用更新进度
updateProcess(process: Number, fileName: string, completeCount: number) {
// 应用定义下载类通知模版
let downLoadTemplate: notificationManager.NotificationTemplate = {
name: 'downloadTemplate', // 当前只支持downloadTemplate,保持不变
data: {
title: `已下载${completeCount}个/共${this.fileNameArr.length}个`, // 必填
fileName: `正在下载:${fileName}`, // 必填
progressValue: process, // 应用更新进度值,自定义
}
};
let request: notificationManager.NotificationRequest = {
content: {
// 系统实况类型,保持不变
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW,
systemLiveView: {
typeCode: 8, // 上传下载类型需要填写 8,当前仅支持此类型。保持不变
title: `保存视频到相册`, // 应用自定义
text: `正在下载:${fileName}`, // 应用自定义
}
},
id: this.notificationId, // 必须是申请长时任务返回的id,否则应用更新通知失败
notificationSlotType: notificationManager.SlotType.LIVE_VIEW, // 实况窗类型,保持不变
template: downLoadTemplate // 应用需要设置的模版名称
};
try {
notificationManager.publish(request).then(() => {
console.info("publish success, id= " + this.id);
}).catch((err: BusinessError) => {
console.error(`publish fail. code is ${(err as BusinessError).code} message is ${(err as BusinessError).message}`);
});
} catch (err) {
console.error(`publish fail. code is ${(err as BusinessError).code} message is ${(err as BusinessError).message}`);
}
}
}
Worker.ets文件。
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
import { BusinessError, request } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
let completeCount: number = 0
let isDownLoading: boolean = false;
let IntervalID: number = 0;
function downLoadFile(context: common.UIAbilityContext, fileUrl: string, filePath: string, fileName: string) {
console.info("download fileUrl:" + fileUrl + " fileName:" + fileName + " filePath:" + filePath);
try {
// 需要手动将url替换为真实服务器的HTTP协议地址
isDownLoading = true
request.downloadFile(context, {
url: fileUrl,
filePath: context.filesDir + `/${fileName}`
}, (err: BusinessError, data: request.DownloadTask) => {
if (err) {
console.error(`Failed to request the download. Code: ${err.code}, message: ${err.message}`);
return;
}
let downloadTask: request.DownloadTask = data;
downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
console.info("download receivedSize:" + receivedSize + " totalSize:" + totalSize);
let percent = Math.ceil((receivedSize / totalSize) * 100) // 计算进度值
workerPort.postMessage({
code: 100,
percent: percent, // 当前正在下载的文件进度
currentName: fileName, // 当前正在下载的文件名
completeCount: completeCount // 下载完成的个数
})
});
downloadTask.on('complete', () => {
console.info('download complete');
completeCount++;
isDownLoading = false
})
});
} catch (err) {
console.error(`Failed to request the download. Code: ${err.code}, message: ${err.message}`);
}
}
workerPort.onmessage = (event: MessageEvents) => {
completeCount = 0;
let context: common.UIAbilityContext = event.data.context;
let fileUrlArr: string = event.data.fileUrlArr;
let filePath: string = event.data.filePath;
let fileNameArr: Array<string> = event.data.fileNameArr;
let totalCount: number = fileNameArr.length;
console.info('------ start ------------')
// 开启定时器,定时扫描当前是否下载完成,或者是否正在下载
IntervalID = setInterval(() => {
if (completeCount < totalCount) {
if (isDownLoading === false) {
downLoadFile(context, fileUrlArr[completeCount], filePath, fileNameArr[completeCount])
}
} else {
console.info('------ end ------------')
workerPort.postMessage({ code: 200, data: 20 })
clearInterval(IntervalID)
}
}, 500)
};
workerPort.onmessageerror = (event: MessageEvents) => {
};
workerPort.onerror = (event: ErrorEvent) => {
};
在HarmonyOS Next中,使用Flutter下载大文件时,可通过background_fetch
或workmanager
插件实现后台任务管理。利用鸿蒙的长时任务机制,在onTaskRemoved
或onDetached
回调中注册长时任务,并通过requestSuspendDelay
申请延迟挂起。下载完成后调用cancelSuspendDelay
结束任务,确保下载完整性。注意使用dio
或http
插件时配置DownloadOptions
支持断点续传。
在HarmonyOS Next中,可以通过以下方式优化长时任务管理以确保文件下载完成:
-
使用ServiceAbility延长后台执行时间
通过配置ServiceAbility并申请长时任务(如dataTransfer),可突破默认10分钟限制。需在config.json
中声明后台服务权限:"abilities": [ { "name": ".ServiceAbility", "srcEntrance": "./ets/ServiceAbility/ServiceAbility.ts", "backgroundModes": ["dataTransfer"] } ]
-
分块下载与断点续传
将大文件分割为多个小块下载,每次下载完成后更新进度并持久化存储(如使用Preferences)。若任务中断,重启后可基于存储的进度恢复下载。 -
任务状态监听与重启机制
通过@ohos.backgroundTaskManager
监听任务生命周期,在任务即将终止时保存状态,并尝试重新申请长时任务或触发通知让用户手动恢复。 -
电源管理优化
调用power.request
保持CPU唤醒,避免系统休眠中断下载,完成后及时释放资源。
示例代码片段(ServiceAbility中):
import backgroundTaskManager from '@ohos.backgroundTaskManager';
// 申请长时任务
let delaySuspendInfo = { reason: 'downloadFile', requestTime: 600000 }; // 申请10分钟
backgroundTaskManager.requestSuspendDelay(delaySuspendInfo).then(() => {
// 执行下载逻辑
});
结合Flutter端,可通过Channel调用HarmonyOS原生能力,确保下载进程在后台持续运行至完成。