RequestAgent后台下载封面

之前做了个应用,要下载一些离线资源包。文件不大的时候,直接用 http 请求下,切个后台也没事。后来换成 500MB 的资源包,问题就全出来了——用户切到后台,下载直接停了;锁屏之后再回来,连接断了;网络抖一下,整个下载任务就挂了,得从头再来。

普通的 http 请求跑前台没问题,真要跑大文件后台下载,根本扛不住。后来换了 RequestAgent,把下载任务交给系统管理,才算是把这件事真正做稳了。这篇就讲讲从"能下载"到"后台也能稳定下载"中间踩的那些坑。

一、普通下载跑得好好的,为什么还要 RequestAgent

最开始我就是用 http.createHttp() 直接下的。代码很简单,发个 GET 请求,把返回的数据写到文件里。小文件测着没问题,10MB 的包几秒钟就下完了。

后来换成 500MB 的资源包,问题马上就来了:

用户切到后台,应用一进后台,系统就把网络连接掐了。等用户再切回来,连接早就断了,文件下到一半就停了。

锁屏更严重。手机一锁屏,CPU 都休眠了,下载线程直接被挂起。等用户解锁回来,TCP 连接早就超时了,根本不知道下到哪了。

还有更烦的:用户不小心杀了应用,下载直接停了。再打开应用,只能重新下。500MB 的文件,白下了一半。

这些问题靠自己写重试逻辑根本解决不了——应用都被系统杀了,你拿什么重试?这种事只能交给系统来管。RequestAgent 就是系统层面的下载任务管理,应用退到后台、甚至被杀了,下载任务系统还帮你跑着。

二、先把一个后台下载任务真正跑起来

RequestAgent 的思路和普通 http 请求不一样。你不是直接发请求,而是先创建一个下载任务,把 URL、保存路径、参数都告诉系统。系统帮你管整个下载过程,你只需要监听进度和状态。

这里有个关键认知:下载任务是系统级的,不是应用级的。应用退了、杀了,任务还在。系统会在后台继续下,下完了再通知应用。

我们封装的下载管理器放在 entry/src/main/ets/download/DownloadManager.ets,页面只负责调用,不要把所有任务逻辑都塞进 UI。

import { request } from '@kit.BasicServicesKit';

export class DownloadManager {
  private static agent: request.RequestAgent | null = null;

  // 初始化下载代理
  static async initAgent(): Promise<void> {
    if (!this.agent) {
      this.agent = await request.createRequestAgent(
        'download_agent',
        {}
      );
    }
  }

  // 创建下载任务
  static async createDownloadTask(
    url: string,
    savePath: string,
    taskId: string
  ): Promise<number> {
    
    // 先查一下这个任务是不是已经存在
    const existing = await this.getTaskById(taskId);
    if (existing) {
      console.info('Task already exists, id: ' + existing.taskId);
      return existing.taskId;
    }

    const config: request.DownloadConfig = {
      url: url,
      filePath: savePath,
      title: '资源下载',
      description: taskId,
      network: request.NetworkType.NETWORK_WIFI | request.NetworkType.NETWORK_MOBILE,
      overwrite: false,  // 不覆盖已有文件
      retry: {
        retryCount: 3,  // 失败自动重试3次
        retryDelay: 2000
      }
    };

    const task = await this.agent!.createDownload(config);
    console.info('Download task created: ' + task.taskId);
    
    // 保存任务 ID,方便后面查询和恢复
    await this.saveTaskMapping(taskId, task.taskId);
    
    return task.taskId;
  }

  // 查询任务状态
  static async getTaskById(
    localTaskId: string
  ): Promise<request.DownloadTask | null> {
    
    const systemTaskId = await this.getSystemTaskId(localTaskId);
    if (!systemTaskId) return null;

    const task = await this.agent!.getTask(systemTaskId);
    return task || null;
  }

  // 任务状态查询
  static async queryTaskStatus(
    localTaskId: string
  ): Promise<TaskStatus> {
    
    const task = await this.getTaskById(localTaskId);
    if (!task) {
      return { state: 'unknown', progress: 0 };
    }

    const progress = task.progress.processed / task.progress.totalSize * 100;
    
    return {
      state: task.state,
      progress: Math.round(progress),
      totalSize: task.progress.totalSize,
      processed: task.progress.processed
    };
  }
}

// 任务状态
interface TaskStatus {
  state: request.DownloadState;
  progress: number;
  totalSize?: number;
  processed?: number;
}

这段代码的核心:createDownloadTask 先查有没有已存在的任务,避免重复创建。overwrite 设成 false,文件已经存在就不重复下了。retry 配置让系统在失败的时候自动重试,不用自己写重试逻辑。

真正运行的时候要注意:systemTaskId 和我们业务里的 taskId 是两回事,要存一个映射表。不然应用重启之后,系统任务 ID 就找不到了。

下载任务状态流转图

三、暂停不难,难的是回来以后还能不能接着下

暂停和恢复接口本身很简单,task.pause()、task.resume() 两行代码的事。但真做的时候,问题不在接口,在状态同步。

用户点了暂停,我们 UI 上马上显示"已暂停"。但系统那边可能还在往缓冲里写数据,过了几百毫秒才真正停。这时候用户马上点恢复,两个操作撞在一起,状态就乱了。

还有更麻烦的:应用杀了之后再打开,怎么知道之前那个任务是暂停了还是在下?

这个别急着重新创建任务,先看看之前的任务还在不在。RequestAgent 的任务是持久化的,应用重启之后,之前创建的任务还能查到。

// 在 DownloadManager 里补充:恢复任务
static async resumeTask(
  localTaskId: string
): Promise<void> {
  
  const task = await this.getTaskById(localTaskId);
  if (!task) {
    console.error('Task not found: ' + localTaskId);
    return;
  }

  // 先查当前状态
  const status = await task.getTaskInfo();
  
  switch (status.state) {
    case request.DownloadState.DOWNLOAD_STATE_PAUSED:
      // 暂停状态,直接恢复
      await task.resume();
      console.info('Task resumed');
      break;
      
    case request.DownloadState.DOWNLOAD_STATE_RUNNING:
      // 已经在跑了,不用恢复
      console.info('Task already running');
      break;
      
    case request.DownloadState.DOWNLOAD_STATE_COMPLETED:
      // 已经下完了
      console.info('Task already completed');
      break;
      
    case request.DownloadState.DOWNLOAD_STATE_FAILED:
      // 失败了,重新开始
      await task.start();
      console.info('Task restarted after failure');
      break;
      
    default:
      console.warn('Unknown state: ' + status.state);
  }
}

这里的关键:恢复任务之前先查状态。已经在跑的不要重复 resume,失败了的要重新 start,下完了的就不用管了。别上来就调 resume,状态不对会报错。

断点续传原理示意图

四、断点续传到底是怎么回事

很多人以为断点续传要自己写 Range 头,自己记录下到哪个字节。其实 RequestAgent 已经帮你做了。

只要你创建任务的时候用的是同一个保存路径,任务暂停了、断了、应用杀了,下次恢复的时候,系统会自动从上次停的地方继续下,不用从头来。

这里有个坑:文件路径要固定。你这次下载存到 /cache/a.zip,下次恢复如果路径变了,系统就找不到之前下了一半的文件,只能重新下。所以保存路径要用业务 ID 固定下来,不要每次都生成新文件名。

还有一个:任务完成之后,那个临时文件要处理一下。RequestAgent 下完之后,文件就在你指定的路径了,但有些时候是临时格式,要自己 rename 成正式文件名,或者移到正式目录。

五、任务完成以后,别忘了解绑和清理

下载完了就完事了?没那么简单。

任务下完了,你要监听完成事件,拿到文件之后做后续处理——解压、校验 MD5、更新数据库状态。这些都做完了,记得把任务从 RequestAgent 里删掉,别留着占资源。

失败的任务也要清理。重试了三次还是失败,这个任务基本就是废了。要么提示用户重新下,要么删掉任务记录。别让一堆失败任务堆在系统里。

还有个容易忽略的:应用卸载的时候,系统下载任务会不会跟着删?这个不用你管,系统会处理。但如果你换了设备、清了数据,之前的任务 ID 就没用了,要重新创建。

说白了,后台下载这件事,代码本身不复杂,麻烦的是任务状态。什么时候该暂停、什么时候该恢复、什么时候该重新创建、什么时候该清理,每一步都想清楚了,下载功能才真正稳。

下一篇讲更麻烦的事——弱网和网络切换。正常 Wi-Fi 下一切正常,一切到移动网络或者信号差的地方,各种问题就全冒出来了。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐