HarmonyOS鸿蒙Next中下载大文件导致卡死闪退问题

HarmonyOS鸿蒙Next中下载大文件导致卡死闪退问题

import {
    DownloadFile,
    DownloadFileFail,
    DownloadFileOptions,
    DownloadFileSuccess,
    DownloadFileComplete,
    DownloadFileProgress
} from '../interface.uts'

export {
    DownloadFile,
    DownloadFileFail,
    DownloadFileOptions,
    DownloadFileSuccess,
    DownloadFileComplete
}

import fs from '@ohos.file.fs';
import http from '@ohos.net.http';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

export function hmDownloadFile(url : string, filePath : string, options : DownloadFileOptions, isGetData : Boolean = false, isOverwrite : Boolean = false) {
    function success() : void {
        let result : DownloadFileSuccess = {
            errMsg: "ok",
            fileData: fileData,
            filePath: DOWNLOAD_TO_PATH
        };
        const completeResult : DownloadFileComplete = {
            errMsg: "ok"
        }
        options?.success?.(result);
        options?.complete?.(completeResult);
    }
    function error(err : BusinessError<void>) : void {
        console.error('$$`' + `failed. code is ${err.code}, message is ${err.message}`);
        let result : DownloadFileFail = {
            errMsg: err.message ?? ""
        };
        const completeResult : DownloadFileComplete = {
            errMsg: err.message ?? ""
        }
        options?.fail?.(result);
        options?.complete?.(completeResult);
    }
    let fileData = '';
    let contentLength = 0;
    let cuContentLength = 0;
    let arrayBuffer = new ArrayBuffer(0);
    let context = UTSHarmony.getUIAbilityContext() as common.UIAbilityContext;
    let DOWNLOAD_TO_PATH = `${context.filesDir}/save_path/${filePath}`;
    console.info('$$`' + `即将下载:${DOWNLOAD_TO_PATH}`)
    // 检查目标路径是否存在
    if (fs.accessSync(DOWNLOAD_TO_PATH)) {
        if (isOverwrite) {
            fs.rmdirSync(DOWNLOAD_TO_PATH.replace(DOWNLOAD_TO_PATH.split("/").pop(), ''));
        }
        else {
            console.info('$$`' + `已存在 ${DOWNLOAD_TO_PATH},直接返回路径`)
            if (isGetData) fileData = fs.readTextSync(DOWNLOAD_TO_PATH); // 读取文本    
            success()
            return
        }
    }
    let httpRequest = http.createHttp();
    try {
        let requestOptions : http.HttpRequestOptions = {
            method: http.RequestMethod.GET, // 可选,默认为http.RequestMethod.GET。
            expectDataType: http.HttpDataType.ARRAY_BUFFER, // 可选,指定返回数据的类型。
            // 当使用POST请求时此字段用于传递请求体内容,具体格式与服务端协商确定。
            // extraData: 'data to send',
            // usingCache: true, // 可选,默认为true。
            // priority: 1, // 可选,默认为1。
            // 开发者根据自身业务需要添加header字段。
            // header: new Header('application/json'),
            readTimeout: 600000, // 可选,默认为60000ms。
            connectTimeout: 600000, // 可选,默认为60000ms。
            // usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定。
            // usingProxy: false, //可选,默认不使用网络代理,自API 10开始支持该属性。
        };
        httpRequest.on('headersReceive', (header : Object) => {
            let _header = JSON.stringify(header)
            let _contentLength : number = Number(_header.split(`"content-length":"`)[1].split(`","`)[0]);
            if (_contentLength) {
                contentLength = _contentLength;
                console.log('$$`' + '数据总大小:', contentLength + ' bytes');
            }
        });
        httpRequest.on("dataReceive", (data : ArrayBuffer) => {
            cuContentLength += data.byteLength;
            arrayBuffer = mergeArrayBuffers([arrayBuffer, data]);
            // console.info('$$`' + "dataReceive length: " + JSON.stringify(data.byteLength));
            const progress : DownloadFileProgress = {
                progress: ((cuContentLength / contentLength) * 100).toFixed(0)
            }
            options?.progress?.(progress);
        });
        httpRequest.on("dataEnd", () => {
            console.info('$$`' + "Receive dataEnd !");
            fs.mkdirSync(DOWNLOAD_TO_PATH.replace(DOWNLOAD_TO_PATH.split("/").pop(), ''), true);
            let file = fs.openSync(DOWNLOAD_TO_PATH, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE)
            fs.writeSync(file.fd, arrayBuffer);
            if (isGetData) fileData = fs.readTextSync(DOWNLOAD_TO_PATH); // 读取文本    
            fs.closeSync(file.fd);
            success()
            httpRequest.destroy();
            console.log('$$`' + '文件已保存至:' + DOWNLOAD_TO_PATH);
        });
        httpRequest.requestInStream(url, requestOptions, (err : BusinessError<void>, data : number) => {
            if (!err) {
                console.info('$$`' + "requestInStream OK! ResponseCode is " + JSON.stringify(data));
            } else {
                error(err);
            }
        })
    }
    catch (err) {
        error(err);
    }
}

export function hmDeleteFile(fileName : string) {
    let context = UTSHarmony.getUIAbilityContext() as common.UIAbilityContext;
    let filePath = `${context.filesDir}/save_path/${fileName}`;
    try {
        fs.unlinkSync(filePath);
        console.info('$$`' + `delete File ${filePath} OK!`);
        return true;
    }
    catch (err) {
        console.info('$$`' + `delete File ${filePath} fail! error:${err}`);
        return false;
    }
}

function mergeArrayBuffers(buffers : ArrayBuffer[]) : ArrayBuffer {
    // 计算总长度
    let totalLength = buffers.reduce((acc, buf) => acc + buf.byteLength, 0);
    // 创建目标ArrayBuffer和视图
    let mergedArray = new Uint8Array(totalLength);
    let offset = 0;
    // 合并每个片段
    buffers.forEach(buf => {
        let uint8View = new Uint8Array(buf);
        mergedArray.set(uint8View, offset);
        offset += uint8View.length;
    });
    return mergedArray.buffer;
}

以上是源码 uniapp开发的 uts插件 鸿蒙系统下载几十兆的音频时会卡死

更多关于HarmonyOS鸿蒙Next中下载大文件导致卡死闪退问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html

7 回复

开发者您好,在此方法mergeArrayBuffers中,您每次接收数据的时,都会创建当前文件下载整体长度的Unit8Array,这样会导致内存数据过大,从而出现卡死现象。

let mergedArray = new Uint8Array(totalLength)

您可以在每次接收数据时,只创建当前接收数据大小的Uint8Array,然后合并到整体下载数据中。

您可以参考此demo:HarmonyOS_Samples/RcpFileTransfer: 本示例基于Remote Communication Kit远场通信服务,使用post、fetch、downloadToFile等方法实现相册的文件上传下载、文件分片下载、断点续传、后台文件上传下载功能。为开发者提供基于RCP上传下载各种场景的开发指导。

或者参考此demo: http:本示例通过@ohos.net.http等接口,实现了根据URL地址和相关配置项发起http请求的功能。 - AtomGit | GitCode

如果还是不能解决您的问题,麻烦您提供下完整能复现问题的最小demo吧(可以压缩整体项目为.zip 上传一下,注意:不要涉及您的隐私信息)。

更多关于HarmonyOS鸿蒙Next中下载大文件导致卡死闪退问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


将几十兆的文件数据全部缓存到arrayBuffer中,鸿蒙应用内存限制较严格,大文件会直接撑爆内存导致闪退,可以改用边下载边写文件(流式写入),还有大文件下载建议增加断点续传逻辑(可基于Range请求头实现),避免网络波动后重新下载

直接处理大文件,可能会出现内存溢出。可以采用分片后,循环追加写入数据到文件。

下面代码,设置每次最多写入2M的数据。

// 分块追加写入大文件,支持 isFirst 控制覆盖/追加
public async appendFileStream(filePath: string, data: Uint8Array, isFirst: boolean = false): Promise<void> {
  try {
    const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
    await this.ensureDirectoryExists(dirPath);
    const openMode = isFirst
      ? fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.TRUNC
      : fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.APPEND;
    const file = await fileIo.open(filePath, openMode);
    const CHUNK_SIZE = 2 * 1024 * 1024;
    let offset = 0;
    while (offset < data.length) {
      const chunk = data.slice(offset, offset + CHUNK_SIZE);
      await fileIo.write(file.fd, chunk.buffer, { offset: 0, length: chunk.length });
      offset += CHUNK_SIZE;
    }
    await fileIo.close(file.fd);
  } catch (e) {
  }
}

为何会出现卡死问题

在HarmonyOS Next中下载大文件导致卡死闪退,通常与内存管理或线程阻塞有关。可检查是否在UI线程执行了耗时下载操作,应使用异步任务或后台服务处理。同时确保应用有足够的内存分配,避免一次性加载过大文件到内存。建议分块下载并优化缓存策略。

根据你提供的代码,下载大文件导致卡死闪退的核心问题在于内存管理。代码在dataReceive事件中,持续将接收到的ArrayBuffer片段合并到一个不断增长的arrayBuffer变量中。对于几十兆的音频文件,这会导致一个巨大的ArrayBuffer被完整地保存在内存中,直至下载完成才写入文件。这会迅速耗尽应用内存,引发卡顿甚至应用闪退。

根本原因与解决方案:

  1. 流式写入,避免内存累积 当前的mergeArrayBuffers操作是问题的根源。正确的做法是使用fs模块的流式写入能力,将每次接收到的数据块(ArrayBuffer)直接写入文件,而不是累积在内存中。

  2. 修改dataReceivedataEnd事件处理逻辑

    • dataReceive中,将接收到的dataArrayBuffer)直接通过fs.writeSync追加写入已打开的文件句柄。
    • 移除mergeArrayBuffers函数及其相关调用。
    • 在开始请求前就创建并打开目标文件。
    • dataEnd中,主要进行关闭文件句柄等清理工作。

代码修改示例:

// ... 前面的代码保持不变 ...

let httpRequest = http.createHttp();
let fileFd: number = -1; // 用于保存文件句柄

try {
    // 1. 在开始下载前,创建目录并打开文件准备写入
    fs.mkdirSync(DOWNLOAD_TO_PATH.replace(DOWNLOAD_TO_PATH.split("/").pop(), ''), true);
    fileFd = fs.openSync(DOWNLOAD_TO_PATH, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);

    let requestOptions: http.HttpRequestOptions = {
        // ... 你的配置保持不变 ...
    };

    httpRequest.on('headersReceive', (header: Object) => {
        // ... 你的进度计算逻辑保持不变 ...
    });

    httpRequest.on("dataReceive", (data: ArrayBuffer) => {
        cuContentLength += data.byteLength;
        
        // 2. 关键修改:直接将数据块写入文件,而不是合并到内存
        if (fileFd !== -1) {
            fs.writeSync(fileFd, data);
        }
        
        // 进度回调
        const progress: DownloadFileProgress = {
            progress: ((cuContentLength / contentLength) * 100).toFixed(0)
        }
        options?.progress?.(progress);
    });

    httpRequest.on("dataEnd", () => {
        console.info('$$`' + "Receive dataEnd !");
        
        // 3. 确保文件句柄关闭
        if (fileFd !== -1) {
            fs.closeSync(fileFd);
            fileFd = -1;
        }
        
        if (isGetData) {
            // 注意:如果是大文件,readTextSync可能仍会占用大量内存。请根据文件实际类型谨慎使用。
            fileData = fs.readTextSync(DOWNLOAD_TO_PATH);
        }
        success();
        httpRequest.destroy();
        console.log('$$`' + '文件已保存至:' + DOWNLOAD_TO_PATH);
    });

    httpRequest.requestInStream(url, requestOptions, (err: BusinessError<void>, data: number) => {
        if (!err) {
            console.info('$$`' + "requestInStream OK! ResponseCode is " + JSON.stringify(data));
        } else {
            // 4. 发生错误时,也应确保关闭已打开的文件句柄
            if (fileFd !== -1) {
                fs.closeSync(fileFd);
                fileFd = -1;
            }
            error(err);
        }
    });
} catch (err) {
    // 5. 异常处理中也需关闭文件句柄
    if (fileFd !== -1) {
        fs.closeSync(fileFd);
        fileFd = -1;
    }
    error(err);
}

// 可以删除 mergeArrayBuffers 函数

关键改进点:

  • 内存优化:将数据流直接写入文件系统,内存中仅保留当前接收到的数据块,内存占用恒定且很低。
  • 健壮性:确保在任何路径(成功、失败、异常)下都正确关闭文件句柄,避免资源泄漏。
  • isGetData警告:如果isGetDatatrue,对于大文件,fs.readTextSync仍会将整个文件内容读入内存的fileData字符串中。如果文件确实很大(如几十兆文本),这本身也可能导致内存压力。请评估此操作的必要性。对于音频等二进制文件,通常不需要读取为文本。

按照以上方式修改代码,应该能彻底解决下载大文件时的卡死和闪退问题。

回到顶部