HarmonyOS鸿蒙Next中flutter下载比较大的文件进入后台怎么保证下载成功后才退出长时任务?

HarmonyOS鸿蒙Next中flutter下载比较大的文件进入后台怎么保证下载成功后才退出长时任务? flutter里有个下载模块,鸿蒙端配置后台长时任务最多只能保活十分钟,但是文件可能没下载完,怎么设置长时任务能支撑到下载完成?

6 回复

解决方案

鸿蒙系统对后台长时任务有严格的生命周期管理。单纯依靠申请长时任务无法保证下载任务无限期运行。系统会在10分钟无进度更新时自动取消长时任务。要实现长时间下载,必须结合以下两种方案:


方案一:使用系统托管下载(推荐)

通过@ohos.request模块将下载任务托管给系统,即使应用进程被挂起,系统服务仍会继续执行下载

  1. 配置长时任务类型与权限module.json5中声明数据传输类型的长时任务和所需权限:

    {
      "module": {
        "abilities": [
          {
            "name": "EntryAbility",
            "backgroundModes": ["dataTransfer"] // 声明数据传输类型
          }
        ],
        "requestPermissions": [
          {
            "name": "ohos.permission.KEEP_BACKGROUND_RUNNING" // 长时任务权限
          },
          {
            "name": "ohos.permission.INTERNET" // 网络权限
          }
        ]
      }
    }
    
  2. **使用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({ /*...*/ });
      });
    });
    
  3. 处理任务恢复 应用再次启动时,可通过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();

关键注意事项

  1. 进度更新是关键:系统会监测任务进度,若10分钟内无更新,将自动取消长时任务。必须通过progress事件或主动调用updateContinuousTaskNotification保持活跃。
  2. 断点续传必需:确保服务器支持HTTP Range请求,否则任务中断后无法恢复。
  3. 模式选择:务必使用request.agent.Mode.BACKGROUND(后台任务),而非FOREGROUND(前台任务)。
  4. 限制:此方案适用于大文件下载,但若用户手动杀死应用或系统资源极度紧张,任务仍可能被终止。

更多关于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。

以下载网络视频到应用本地为例,具体步骤如下:

  1. 添加后台运行权限ohos.permission.KEEP_BACKGROUND_RUNNING和网络权限ohos.permission.INTERNET。
"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET",
  },
  {
    "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
  },
],
  1. 声明数据传输后台模式类型。在module.json5文件中为需要使用长时任务的UIAbility声明相应的长时任务类型,配置文件中填写长时任务类型的配置项。示例如下:
"module": {
  "abilities": [
  {
    "backgroundModes": [
    // 配置长时任务类型为数据传输
    "dataTransfer"
    ]
  }
  ],
  // ...
}
  1. 导入长时任务相关模块。
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';
  1. 在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}`);
  }
}
  1. 在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}`);
  }
}
  1. 更新下载进度,代码示例如下:
// 应用更新进度
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_fetchworkmanager插件实现后台任务管理。利用鸿蒙的长时任务机制,在onTaskRemovedonDetached回调中注册长时任务,并通过requestSuspendDelay申请延迟挂起。下载完成后调用cancelSuspendDelay结束任务,确保下载完整性。注意使用diohttp插件时配置DownloadOptions支持断点续传。

在HarmonyOS Next中,可以通过以下方式优化长时任务管理以确保文件下载完成:

  1. 使用ServiceAbility延长后台执行时间
    通过配置ServiceAbility并申请长时任务(如dataTransfer),可突破默认10分钟限制。需在config.json中声明后台服务权限:

    "abilities": [
      {
        "name": ".ServiceAbility",
        "srcEntrance": "./ets/ServiceAbility/ServiceAbility.ts",
        "backgroundModes": ["dataTransfer"]
      }
    ]
    
  2. 分块下载与断点续传
    将大文件分割为多个小块下载,每次下载完成后更新进度并持久化存储(如使用Preferences)。若任务中断,重启后可基于存储的进度恢复下载。

  3. 任务状态监听与重启机制
    通过@ohos.backgroundTaskManager监听任务生命周期,在任务即将终止时保存状态,并尝试重新申请长时任务或触发通知让用户手动恢复。

  4. 电源管理优化
    调用power.request保持CPU唤醒,避免系统休眠中断下载,完成后及时释放资源。

示例代码片段(ServiceAbility中):

import backgroundTaskManager from '@ohos.backgroundTaskManager';

// 申请长时任务
let delaySuspendInfo = { reason: 'downloadFile', requestTime: 600000 }; // 申请10分钟
backgroundTaskManager.requestSuspendDelay(delaySuspendInfo).then(() => {
  // 执行下载逻辑
});

结合Flutter端,可通过Channel调用HarmonyOS原生能力,确保下载进程在后台持续运行至完成。

回到顶部