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
开发者您好,在此方法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被完整地保存在内存中,直至下载完成才写入文件。这会迅速耗尽应用内存,引发卡顿甚至应用闪退。
根本原因与解决方案:
-
流式写入,避免内存累积 当前的
mergeArrayBuffers操作是问题的根源。正确的做法是使用fs模块的流式写入能力,将每次接收到的数据块(ArrayBuffer)直接写入文件,而不是累积在内存中。 -
修改
dataReceive和dataEnd事件处理逻辑- 在
dataReceive中,将接收到的data(ArrayBuffer)直接通过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警告:如果isGetData为true,对于大文件,fs.readTextSync仍会将整个文件内容读入内存的fileData字符串中。如果文件确实很大(如几十兆文本),这本身也可能导致内存压力。请评估此操作的必要性。对于音频等二进制文件,通常不需要读取为文本。
按照以上方式修改代码,应该能彻底解决下载大文件时的卡死和闪退问题。


