HarmonyOS 鸿蒙Next中如何实现不阻塞主线程的下载操作

HarmonyOS 鸿蒙Next中如何实现不阻塞主线程的下载操作 使用内部三方库提供的下载能力,在使用for循环同步执行下载时可以正常将文件下载到指定的目录文件下

但在下载任务量大的情况下会阻塞页面操作

使用taskpool发现根本不执行下载任务

使用worker也无法下载来(怀疑外部传入的文件路径在线程内找不到)

请问还有其他不阻塞主线程的方案吗?

12 回复

【解决方案】
开发者你好,可以在request.downloadFile接口设置downloadTask.on回调,以获取下载任务的"complete"、"fail"等状态,从而保障下载的正常执行。参考如下示例代码:

import { BusinessError, request } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import { taskpool } from '@kit.ArkTS';

@Concurrent
function requestDownloadFile(context: common.UIAbilityContext, name: string) {
  try {

    const filePath = context.tempDir + "/" + name
    // filePath为下载的地址,默认为调用方(即传入的context)对应的缓存路径。默认文件名从url的最后一个"/"后截取。
    // 如需要下载到用户目录,可从沙箱复制进去
    request.downloadFile(context, {
      url: "https://xxxxx.png",
      filePath: filePath,
      background: true
    }).then((data: request.DownloadTask) => {
      let downloadTask: request.DownloadTask = data;
      let completeCallback = () => {
        console.log("下载成功!!")
      };
      // 订阅完成事件
      downloadTask.on('complete', completeCallback);
      let failCallback = (err: number) => {
        console.error(`Failed to download the task. Code: ${err}`);
      };
      // 订阅失败事件
      downloadTask.on('fail', failCallback);

    }).catch((err: BusinessError) => {
      console.error(`Failed to request the download. Code: ${err.code}, message: ${err.message}`);
    })
  } catch (err) {
    console.error(`Failed to request the download. err: ${JSON.stringify(err)}`);
  }
}

@Entry
@Component
struct Page_250725105655087 {
  build() {
    RelativeContainer() {
      Button("下载")
        .id('Page_250725105655087HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          const context = getContext(this) as common.UIAbilityContext;

          let nameArray: string[] =
            ["1.png", "2.png", "3.png", "4.png", "5.png", "6.png", "7.png", "8.png", "9.png", "10.png", "11.png",
              "12.png", "13.png", "14.png", "15.png", "16.png", "17.png"]

          nameArray.forEach((name) => {
            let mTask = new taskpool.LongTask(requestDownloadFile, context, name)
            taskpool.execute(mTask)
          })
        })
    }
    .height('100%')
    .width('100%')
  }
}

更多关于HarmonyOS 鸿蒙Next中如何实现不阻塞主线程的下载操作的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


使用 TaskPool配合request.downloadFile。

taskPool适合IO密集型。如果资源有限,大于API 18 还可以用AsyncRunner可以控制并发数量。

request.downloadFile鸿蒙原生支持后台、并发、进度、断点等。

import { common } from '@kit.AbilityKit';
import { request } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
import { taskpool } from '@kit.ArkTS';

// 必须用 @Concurrent ,才能被 TaskPool 调度
@Concurrent
async function downloadSingleFile(
  context: common.UIAbilityContext,
  url: string,
  savePath: string
): Promise<string> {
  return new Promise(async (resolve, reject) => {
    try {
      // 确保目录存在
      const dir = savePath.substring(0, savePath.lastIndexOf('/'));
      await fileIo.mkdir(dir, true);

      // 下载配置
      const config: request.DownloadConfig = {
        url: url,
        filePath: savePath,
        enableMetered: true,
        enableRoaming: true
      };

      // 发起下载
      const task = await request.downloadFile(context, config);

      // 监听完成/失败
      task.on('complete', () => {
        task.off('complete');
        task.off('fail');
        resolve(savePath);
      });

      task.on('fail', (err) => {
        task.off('complete');
        task.off('fail');
        reject(new Error(`Download fail: ${err}`));
      });

    } catch (e) {
      reject(e);
    }
  });
}

@Entry
@Component
struct Index {
  @State progressText: string = '准备开始';
  context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;

  // 多个文件 URL
  private urls: string[] = [
    'https://xx.com/file1.xx',
    'https://xx.com/file2.xx',
    'https://xx.com/file3.xx'
  ];

  // 多线程批量下载
  async downloadFiles() {
    this.progressText = '多线程下载中...';

    // 控制并发数量
    @Available({ minApiVersion: 'OpenHarmony 18' })
    let asyncRunner: taskpool.AsyncRunner = new taskpool.AsyncRunner(2);

    for (let i = 0; i < this.urls.length; i++) {
      const url = this.urls[i];
      const fileName = `file_${i}.xxx`;
      const savePath = `${this.context.filesDir}/fdownload/${fileName}`;

      // 提交到 TaskPool 并发执行
      const task: taskpool.Task = new taskpool.Task(downloadSingleFile,this.context, url, savePath);
      if (deviceInfo.sdkApiVersion >= 18) {
        asyncRunner.execute(task).catch(() => {
          // TODO: Implement error handling.
        });
      } else {
        // Fallback to an earlier version
        taskpool.execute(task).catch(() => {
          // TODO: Implement error handling.
        });
      }
    }
  }

  build() {
    Column() {
      Button('开始下载')
        .margin(20)
        .onClick(() => {
          this.downloadFiles();
        });

      Text(this.progressText)
        .fontSize(14)
        .margin(10)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

根据自己项目实际改改。优化的时候可以研究下这个多线程并发,希望帮到你。

这个场景建议把“下载”和“下载调度”拆开处理,不建议依赖 taskpool/worker 去包住一个同步下载库。

下载本身是 I/O,优先用系统 request.downloadFile 或异步下载库来发起任务;页面层只维护队列、并发数、进度和取消。批量场景里不要在 UI 线程同步 for 循环等待每个任务完成,可以做一个下载队列,把并发控制在 2~4 个,每个任务完成后再调度下一个。

另外注意两点:

  1. 目标目录需要提前创建,目标文件已存在时要按业务选择覆盖或跳过,否则很多下载能力会直接失败。
  2. taskpool/worker 和主线程内存隔离,传参走结构化克隆,不要把 UI 对象、Context、不可序列化对象或文件句柄传进去;如果一定让 Worker 做调度,只传 URL、任务 ID、字符串路径等简单数据,并确认 worker 文件已在 build-profile.json5 的 sourceOption.workers 中配置。

如果内部三方库本身是同步/native 阻塞实现,最好在库侧改成 Promise/回调异步;native 层可以用 NAPI async work。简单说:主线程负责队列和状态,下载能力本身异步化,taskpool/worker 不是解决同步下载库阻塞的银弹。

我先按 HarmonyOS 线程模型和文件下载能力查一下,重点确认为什么 taskpool/worker 里下载不生效,以及有没有更合适的异步下载接口或后台任务方案。

Searched 15 web pages

我已经看到一个比较明确的方向了:你现在的问题本质上不是“有没有线程”,而是“下载库本身是不是同步阻塞/是否依赖主线程上下文”。我再补抓两页官方文档,把 TaskPool/Worker 的适用边界和系统下载接口一起对齐。

Searched 3 web pages

有,而且大概率不需要靠 taskpool/worker 才能做到不阻塞主线程

先说结论

你现在卡住的根因通常是这三种之一:

  1. 你用的三方下载能力是同步阻塞式for 循环串行调用自然会卡 UI。
  2. 这个三方库内部可能依赖了主线程上下文 / 非线程安全模块,所以丢到 taskpool/worker 里根本跑不起来。
  3. 你在线程间传了不能序列化的对象,比如 Context、文件句柄、复杂对象,或者传入的路径/uri 在线程里不成立。

HarmonyOS 官方对 TaskPool/Worker 的说明里也提到:

  • TaskPool/Worker 线程和主线程内存隔离
  • 只能传可序列化的数据
  • 不支持 UI 模块、主线程专用模块、线程不安全模块
  • TaskPool 更适合大多数独立任务,Worker 更适合长时间占用线程或依赖线程上下文的任务
    参考官方文档:TaskPool 和 Worker 对比TaskPool 简介

最推荐方案

方案 1:直接换系统异步下载接口

如果可以替换三方下载库,优先用系统的 @ohos.request

官方 request 模块本身就提供:

  • request.downloadFile
  • DownloadTask
  • 进度、完成、失败回调
  • 以及后台传输 agent 能力
    参考:@ohos.request

这类接口本身就是异步的,在主线程发起也不会像同步 for 循环那样卡页面
真正卡 UI 的不是“下载”,而是你现在调用的是同步阻塞式下载实现

方案 2:做“异步队列 + 限流”,不要同步 for 循环

大批量下载时,不要这样做:

for (let i = 0; i < list.length; i++) {
  syncDownload(list[i])
}

要改成:

  • 任务队列
  • 限制并发数
  • 每次只跑 2 到 4 个
  • 用异步回调/Promise 更新进度

思路比“把同步库塞进线程”更重要。

一个典型结构是:

const MAX_CONCURRENT = 3;

async function runDownloadQueue(tasks: DownloadItem[]): Promise<void> {
  let index = 0;
  const workers: Promise<void>[] = [];

  async function next(): Promise<void> {
    if (index >= tasks.length) {
      return;
    }
    const current = tasks[index++];
    await downloadOne(current); // 这里必须是异步下载
    await next();
  }

  for (let i = 0; i < Math.min(MAX_CONCURRENT, tasks.length); i++) {
    workers.push(next());
  }

  await Promise.all(workers);
}

如果 downloadOne() 是异步下载,页面通常不会卡死。

方案 3:如果必须继续用三方库,就不要直接从 ArkTS 同步调

如果这个三方库只有同步接口,那最稳的方案通常不是继续硬塞 taskpool/worker,而是:

  • 在 native/库内部自己起线程池
  • 做异步回调或 Promise 封装
  • ArkTS 只负责:
    • 提交任务
    • 更新进度
    • 取消
    • 队列管理

也就是说:

把“同步下载”改成“native 异步下载”,而不是让 ArkTS 主线程去循环调同步库。

为什么你的 taskpool 可能“不执行”

这个非常像下面几类问题:

1. 并发函数不符合 @Concurrent 约束

TaskPool 的函数必须满足 @Concurrent 规则,且不能乱用闭包、类方法、线程不安全对象。
官方说明见:TaskPool 简介

2. 你在 TaskPool 里用了不能跨线程的对象

比如:

  • this
  • Context
  • 文件对象
  • UI 相关对象
  • 没有 @Sendable 的复杂实例

这些都很容易导致任务根本没正常调度。

3. 三方库内部依赖主线程模块

官方明确说了,TaskPool/Worker 线程里不能用 UI 模块、主线程专用模块、线程不安全模块。
如果三方库内部依赖这些,即使你外面包装成 taskpool,它也跑不通。

为什么你的 worker 也可能失败

你怀疑“文件路径在线程里找不到”,这个方向是对的,但更准确说是:

常见问题有两个

  1. 传过去的数据不对

    • 不要传 Context
    • 不要传文件对象
    • 只传:
      • url
      • taskId
      • fileName
      • dirUri / dirPath
      • 纯字符串配置
  2. 线程里的路径语义不对

    • 如果三方库要求的是物理路径,你传的是 uri
    • 如果它要求的是 uri,你传的是普通字符串路径
    • 或者主线程拿到的是某种临时授权路径,子线程里不能直接复用

更稳的做法

如果必须在线程里跑下载:

  • 主线程只传纯字符串参数
  • 在线程里自己重新拼目标路径
  • 不要传 Context
  • 不要传复杂对象
  • 不要传主线程获取的临时句柄

我建议你怎么选

如果你可以换实现

直接用:

  • @ohos.requestdownloadFile
  • 如果需要后台下载,再看 request.agent

这是最推荐的。
因为官方 request 模块本身就包含“上传、下载、后台传输 agent 能力”:官方文档

如果你不能换三方库

就按这个顺序处理:

  1. 先确认三方库是不是同步阻塞
  2. 确认它是否线程安全
  3. 确认它是否依赖主线程上下文
  4. 若是同步且线程安全:
    • Worker
    • 只传可序列化参数
  5. 若不是线程安全或依赖主线程:
    • 不要再硬塞 taskpool/worker
    • 改成 native 异步封装

实战建议

批量下载最稳的架构通常是:

  • ArkTS 主线程:
    • 管下载队列
    • 控并发数 2~4
    • 更新 UI 进度
  • 下载层:
    • 用系统异步下载接口,或 native 异步下载
  • 不要:
    • 在主线程 for 循环调同步下载
    • 一次性开太多 Worker
    • 在线程间传 Context/文件对象/复杂实例

一句话结论

有不阻塞主线程的方案,而且最优解通常不是继续折腾 taskpool/worker,而是改成“系统异步下载 + 队列限流”,或者把同步三方库改成 native 异步封装。

taskpool 这个方法就行啊, 你为啥说没有触发下载呢 ? 大概率代码问题吧

需要注意的是 TaskPool 需要 @Concurrent 装饰器才能执行异步操作, 你试试看 , 如果 你说的两种方式都不用的话 可以尝试使用 Promise.all 的方式进行下载 ,这个方法你可以搜一下, 很多案例 , 如有帮助给个采纳谢谢

HarmonyOS的分布式文件系统让我在多设备间传输文件变得轻松无比。

不能用三方库时,思路是不要把“同步下载库”直接包进 UI 线程循环。优先看系统下载能力或把 native 下载实现改成异步:ArkTS 层只维护队列、并发数、进度和取消。批量下载建议做任务队列,并发控制在 2 到 4 个,不要 for 循环同步等待。若必须用 Worker,只传 URL、任务 ID、字符串路径等可序列化数据,不要传 Context、文件句柄或 UI 对象;Worker 文件也要确认已在构建配置里声明。若内部 native 库本身是阻塞实现,最好在库侧改为 NAPI async work 或回调/Promise,否则换线程也容易卡住调度和资源访问。

推荐使用三方库:axios

[https://ohpm.openharmony.cn/#/cn/detail/@ohos%2Faxios](https://ohpm.openharmony.cn/#/cn/detail/@ohos%2Faxios)

下载是不会阻塞主线程的。

  • 下载文件时,如果filePath已存在该文件则下载失败,下载之前需要先删除文件
  • 不支持自动创建目录,若下载路径中的目录不存在,则下载失败
let filePath = getContext(this).cacheDir + '/blue.jpg'
// 下载。如果文件已存在,则先删除文件。
try {
  fs.accessSync(filePath);
  fs.unlinkSync(filePath);
} catch(err: Error) {}

axios({
  url: 'https://www.xxx.com/blue.jpg',
  method: 'get',
  // context: getContext(this),
  filePath: filePath ,
  onDownloadProgress: (progressEvent: AxiosProgressEvent): void => {
    console.info("progress: " + progressEvent && progressEvent.loaded && progressEvent.total ? Math.ceil(progressEvent.loaded / progressEvent.total * 100) : 0)
  }
}).then((res: AxiosResponse)=>{
  console.info("result: " + JSON.stringify(res.data));
}).catch((error: AxiosError)=>{
  console.error("error:" + JSON.stringify(error));
})

不能使用外部三方库😭

在HarmonyOS Next中,使用@ohos.taskpool任务池将下载操作提交到后台线程,主线程不会被阻塞。示例:

import taskpool from '@ohos.taskpool';
@Concurrent
async function downloadTask(url: string): Promise<void> { /* 下载逻辑 */ }
taskpool.execute(downloadTask, url).then(() => { /* 处理完成 */ });

也可使用@ohos.request模块的downloadFile方法,其基于Promise异步执行,不阻塞UI线程。

HarmonyOS Next 中推荐直接使用系统下载能力,无需依赖第三方库。系统 API @ohos.request 支持创建下载任务,任务在独立线程中异步执行,完全不阻塞主线程。

核心示例代码如下:

import request from '@ohos.request';
import { BusinessError } from '@ohos.base';

// 配置下载任务
let config: request.DownloadConfig = {
  url: 'https://example.com/file.zip',            // 下载地址
  filePath: getContext().filesDir + '/file.zip',  // 必须用应用沙箱路径
  enableMetered: true,                           // 允许移动网络下载
  enableRoaming: true                            // 允许漫游下载
};

try {
  // 创建并启动下载任务,整个过程异步
  request.downloadFile(getContext(), config)
    .then((task: request.DownloadTask) => {
      task.on('progress', (receivedSize, totalSize) => {
        console.info(`进度: ${receivedSize}/${totalSize}`);
      });
      task.on('complete', () => {
        console.info('下载完成');
      });
      task.on('fail', (errCode: number, errMsg: string) => {
        console.error(`下载失败: ${errCode} ${errMsg}`);
      });
    }).catch((err: BusinessError) => {
      console.error('创建任务失败: ' + err.message);
    });
} catch (err) {
  console.error('配置异常: ' + JSON.stringify(err));
}

为什么 TaskPool / Worker 可能失败?

  • TaskPool 引擎对部分系统能力上下文有严格限制,某些第三方库或涉及沙箱路径的操作可能不被支持。
  • Worker 自身有独立沙箱,直接传入主线程的文件路径可能因权限隔离导致无法访问。必须通过 workerPort.postMessage 传递应用沙箱路径(如 context.filesDir 拼接路径),且需要提前确认 Worker 对该路径有读写权限。

综上,直接采用系统下载 API 是最简洁、稳定的非阻塞方案。

回到顶部