文档管理中心
|

HarmonyOS 6

文件下载到沙箱后为什么大小与源文件不一致

已解决
在应用中从云端下载文件到沙箱目录,文件下载成功但打开文件时发现文件损坏。我对比源文件和沙箱文件大小,发现沙箱文件体积明显偏小或偏大,下载过程也没有报错,这种一般是什么情况导致的呢。
点赞
收藏
2
回复
16
分享
举报
浏览113 发布于7天前北京
全部评论
最多点赞
最新发布
最早发布
|
采纳答复

很大原因是request.downloadFile 下载的是未解码的压缩数据

当服务器对响应体启用了 gzip/deflate 压缩传输时,request.downloadFile 方法保存到沙箱的是 未经解码的原始压缩数据,而非解压后的真实文件内容。

判断方法:用文件查看器或代码读取沙箱文件前几个字节,如果以 1F 8B 开头,说明是 gzip 压缩数据。

解决方案:使用http.createHttp().request(url: string, callback: AsyncCallback)方法请求图片数据,然后把接收的数据写入沙箱.

官方文档:使用request.downloadFile下载图片到沙箱,图片无法打开-基础服务(Basics Service)-基础功能-系统开发 - 华为HarmonyOS开发者

cke_7031.png

15楼回复于4天前 来自北京

文件下载成功但写入沙箱后损坏、体积异常,通常是写入环节的数据处理不当导致的,而非下载本身的问题。下载过程不报错是因为网络请求和响应流读取都正常完成了,但在将接收到的数据写入本地文件时出现了偏差。

原因一:未循环读取写入(最常见,导致体积偏小)

下载大文件时通常分块接收数据,每次收到一个 ArrayBuffer。如果只写入了一次就认为完成了,或者 buffer 被写成了一个固定值而没有循环读取,就只有开头部分内容被存入沙箱,文件体积明显偏小1。

原因二:文件已存在且未清空(导致体积偏大)

如果目标文件已存在,打开时未使用 TRUNC 模式(截断为零),新数据会从文件头部开始覆盖写入,但如果新数据比旧数据短,文件尾部会残留旧数据,导致体积偏大且内容混乱。若使用了 APPEND 模式,则新数据会追加在旧数据之后,同样导致体积异常。

原因三:写入后未关闭文件(导致体积偏小或内容缺失)

文件写入后数据可能还在系统缓存中,如果没有调用 closeSync 或 fsyncSync 将缓存刷入磁盘,文件实际写入的数据量可能小于预期。

原因四:二进制数据被当作字符串处理(导致体积偏大或内容错误)

将二进制文件数据通过字符串编码方式(如 Base64 或 UTF-8)处理后再写入,编码转换会改变数据体积和内容,导致文件损坏。

正确的写入方式

以下示例展示了通过 HTTP 请求下载文件并正确写入沙箱目录的完整流程,使用分块接收、循环写入的方式确保数据完整性:

import { http } from '@kit.NetworkKit';
import { fileIo } from '@kit.CoreFileKit';
import { buffer } from '@kit.ArkTS';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct DownloadFilePage {
  @State downloadProgress: number = 0;
  @State isDownloading: boolean = false;

  async downloadFile(context: common.UIAbilityContext): Promise<void> {
    const url: string = 'https://example.com/test.pdf';
    const filePath: string = `${context.filesDir}/test.pdf`;

    // 创建 HTTP 请求
    const httpRequest: http.HttpRequest = http.createHttp();

    try {
      // 发起请求,启用流式接收
      const promise: http.HttpResponse = await httpRequest.request(url, {
        method: http.RequestMethod.GET,
        expectDataType: http.HttpDataType.ARRAY_BUFFER,
        connectTimeout: 60000,
        readTimeout: 60000,
      });

      // 获取响应数据
      const responseData: ArrayBuffer = promise.result as ArrayBuffer;

      // 打开文件并写入,使用 CREATE | WRITE_ONLY | TRUNC 确保文件清空后写入
      const file: fileIo.File = fileIo.openSync(
        filePath,
        fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.TRUNC
      );

      // 将完整数据写入文件
      fileIo.writeSync(file.fd, responseData);

      // 关闭文件,确保数据刷入磁盘
      fileIo.closeSync(file);

      console.info('文件下载并写入成功');
    } catch (error) {
      console.error(`下载失败: ${(error as BusinessError).message}`);
    } finally {
      httpRequest.destroy();
    }
  }

  build() {
    Column() {
      Button('下载文件')
        .onClick(() => {
          this.downloadFile(this.getUIContext().getHostContext() as common.UIAbilityContext);
        })
    }
  }
}

如果使用 request.downloadFile(系统下载服务),它内部会自动处理文件写入,只需在 DownloadConfig中指定 filePath 即可,无需手动写入:

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

async function downloadWithSystemAPI(context: common.UIAbilityContext): Promise<void> {
  const filePath: string = `${context.filesDir}/test.pdf`;

  try {
    const downloadTask: request.DownloadTask = await request.downloadFile(context, {
      url: 'https://example.com/test.pdf',
      filePath: filePath,
    });

    // 监听下载进度
    downloadTask.on('progressReceive', (receivedSize: number, totalSize: number) => {
      console.info(`下载进度: ${receivedSize}/${totalSize}`);
    });

    // 监听下载完成
    downloadTask.on('complete', () => {
      console.info('下载完成,文件已保存至: ' + filePath);
    });
  } catch (error) {
    console.error(`下载失败: ${(error as BusinessError).message}`);
  }
}
展开全部
1楼回复于7天前 来自北京
如果使用request.downloadFile,可能会碰到这个现象。楼主可以贴出代码看看。
后来换成request.agent,就正常了。可以参考 这里,快速运行下载看效果。
cke_2881.gif
用到实际项目时可以参考《文件上传下载优化》
如果是网页中的下载,可以参考《ArkWeb打开网页,网页中下载内容保存到手机存储》
14楼回复于6天前 来自北京

你好,这类问题通常要从 响应类型、写入方式、完成时机 排查:

  1. 如果用 @ohos.net.http 下载,文件必须按二进制接收,使用 HttpDataType.ARRAY_BUFFER,不要先转成字符串再写入,否则可能导致文件损坏   参考官方 HTTP 数据请求文档

  2. 如果复用同一个沙箱文件路径,WRITE_ONLY 只会覆盖,不会清理旧文件尾部。源文件变小时,本地文件反而可能更大,应使用 WRITE_ONLY | CREATE | TRUNC,或下载任务配置 overwrite: true   参考官方 FAQ

  3. 使用 request.downloadFile 或云存储下载时,要等 complete/completed 回调后再打开文件,并通过 fs.stat().size 核对实际字节数;同时检查 HTTP 状态码、分片 offset/length 和最终保存路径   参考官方应用文件上传下载示例文件下载管理案例

一般判断:文件偏小,优先查二进制被当文本、下载未完成或分片写入不完整;文件偏大,优先查未使用 TRUNC 或重复写入

6楼回复于7天前 来自湖南
您好,这个问题通常是流式写入没闭合或编码转换导致的。建议检查http.request的onDataReceived回调,确保用file.write追加写入二进制数据,并且最后调用了file.close()。另外,确认没对文件内容做多余的字符串转换,直接存ArrayBuffer最稳妥。
4楼回复于7天前 来自山东

文件下载到沙箱后大小不一致,可能是3个原因所致:一是传输模式错误(最常见):使用了文本(ASCII)模式而非二进制模式下载。系统会对换行符(如LFCRLF)进行转换,直接改变文件字节数,导致图片、压缩包等二进制文件损坏。二是写入逻辑缺陷:代码中使用NSMutableData拼接数据,可能因内存或数据截断问题导致写入不全。更建议用NSFileHandle将每次接收到的data直接追加到文件末尾。三是断点续传异常:如果使用Range头实现断点续传,可能因请求的偏移量错误或恢复逻辑有误,导致部分数据块丢失。

16楼回复于3天前 来自北京
请求成功只代表传输结束,不能证明落盘内容正确。先同时记录 HTTP 状态码、Content-Length/Content-Range、累计写入字节数和最终文件大小。偏小常见于分块数据没完整循环写入;偏大常见于旧文件未 TRUNC 或断点续传重复追加;还有一种是鉴权失败返回了 JSON/HTML,却被当成目标文件保存。建议先写 .part 文件,校验长度和 SHA-256 后再原子重命名。 
13楼回复于7天前 来自四川

六大根因深度剖析

根因 发生机制 典型表现
① 流未 flush/close fs.write() 后数据仍在内核缓冲区,未调用 fs.closeSync(fd) 就认为完成 沙箱文件比源文件偏小(最后几 KB 丢失)
② ArrayBuffer 偏移错误 request.downloadon('data') 回调中 slice 范围计算错误 文件偏小或内容错乱,MD5 不匹配
③ 分片合并遗漏 多线程/分片下载时最后一片未写入或合并顺序错乱 文件偏小,尾部数据缺失
④ 编码隐式转换 string 模式写入二进制文件(如图片/PDF),UTF-8 编码破坏字节 文件偏大(多字节字符膨胀)或损坏
⑤ 异步竞态覆盖 多次触发下载,后一次 write 覆盖前一次但未 truncate 文件大小随机,内容混杂
⑥ 沙箱配额静默截断 应用沙箱空间不足时,部分 API 不抛错而是截断写入 文件偏小,hilog 可能有 quota warning

⚠️ 关键认知:HarmonyOS 的 @ohos.request 下载任务和 @ohos.fs 文件系统是两套独立 API。下载成功 ≠ 落盘成功request.DownloadTask.on('complete') 只表示网络传输结束,不代表数据已安全刷入磁盘。


 标准修复方案:原子化写入 + 校验闭环

方案一:使用 request.download 的正确姿势(推荐)

import { request } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

class SafeDownloader {
  private context: Context;
  
  constructor(context: Context) {
    this.context = context;
  }

  async downloadWithVerification(
    url: string, 
    expectedSize?: number,
    expectedMd5?: string
  ): Promise<string> {
    const fileName = url.split('/').pop() ?? `download_${Date.now()}`;
    // 先写入临时文件,验证通过后再 rename(原子操作)
    const tempPath = `${this.context.cacheDir}/${fileName}.tmp`;
    const finalPath = `${this.context.filesDir}/${fileName}`;

    return new Promise((resolve, reject) => {
      const config: request.DownloadConfig = {
        url: url,
        filePath: tempPath, // 直接指定落盘路径,避免手动拼接流
        enableMetered: true,
        enableRoaming: true
      };

      let downloadTask: request.DownloadTask | undefined;

      try {
        downloadTask = request.createDownloadTask(this.context, config);
      } catch (err) {
        reject(new Error(`Create task failed: ${JSON.stringify(err)}`));
        return;
      }

      let receivedBytes = 0;

      downloadTask.on('progress', (received: number, total: number) => {
        receivedBytes = received;
        if (expectedSize && total > 0 && total !== expectedSize) {
          hilog.warn(0x0000, 'Download', 
            `Size mismatch header: expected=${expectedSize}, actual=${total}`);
        }
      });

      downloadTask.on('complete', async () => {
        try {
          // 1. 强制同步文件系统(确保内核缓冲区刷盘)
          await this.syncFile(tempPath);
          
          // 2. 校验文件大小
          const stat = await fs.stat(tempPath);
          if (expectedSize && stat.size !== expectedSize) {
            throw new Error(`Size mismatch: expected=${expectedSize}, got=${stat.size}`);
          }
          if (stat.size === 0) {
            throw new Error('Downloaded file is empty');
          }

          // 3. 校验 MD5(如有提供)
          if (expectedMd5) {
            const actualMd5 = await this.calculateMd5(tempPath);
            if (actualMd5.toLowerCase() !== expectedMd5.toLowerCase()) {
              throw new Error(`MD5 mismatch: expected=${expectedMd5}, got=${actualMd5}`);
            }
          }

          // 4. 原子性移动到目标位置(rename 在同一文件系统下是原子操作)
          await fs.rename(tempPath, finalPath);
          
          hilog.info(0x0000, 'Download', 
            `✅ Verified & saved: ${finalPath} (${stat.size} bytes)`);
          resolve(finalPath);

        } catch (verifyErr) {
          // 校验失败,删除临时文件,防止残留
          await fs.unlink(tempPath).catch(() => {});
          reject(verifyErr);
        } finally {
          downloadTask?.off('progress');
          downloadTask?.off('complete');
          downloadTask?.off('fail');
        }
      });

      downloadTask.on('fail', (err: number) => {
        fs.unlink(tempPath).catch(() => {});
        reject(new Error(`Download failed with code: ${err}`));
      });

      downloadTask.start((err) => {
        if (err) {
          reject(new Error(`Start failed: ${JSON.stringify(err)}`));
        }
      });
    });
  }

  /**
   * 强制刷盘:打开 → fsync → close
   */
  private async syncFile(path: string): Promise<void> {
    const fd = await fs.open(path, fs.OpenMode.READ_ONLY);
    try {
      await fs.fsync(fd.fd); // 关键:将内核缓冲区数据刷入物理存储
    } finally {
      await fs.close(fd);
    }
  }

  /**
   * 计算文件 MD5
   */
  private async calculateMd5(filePath: string): Promise<string> {
    const md = cryptoFramework.createMd('MD5');
    const file = await fs.open(filePath, fs.OpenMode.READ_ONLY);
    const stat = await fs.stat(filePath);
    const buf = new ArrayBuffer(stat.size);
    await fs.read(file.fd, buf);
    await fs.close(file);
    
    const dataBlob: cryptoFramework.DataBlob = { data: new Uint8Array(buf) };
    await md.update(dataBlob);
    const digest = await md.digest();
    
    return Array.from(digest.data)
      .map(b => b.toString(16).padStart(2, '0'))
      .join('');
  }
}

方案二:手动流式下载(适用于需要自定义进度/UI 的场景)

如果你不用 request.download 而是用 http.HttpResponse 手动接收流,必须严格管理写入生命周期

import { http } from '@kit.NetworkKit';
import { fileIo as fs } from '@kit.CoreFileKit';

async function manualStreamDownload(url: string, savePath: string): Promise<void> {
  const httpRequest = http.createHttp();
  let fileFd: fs.File | null = null;
  let totalWritten = 0;

  try {
    // 1. 预先创建文件并获取 fd
    fileFd = await fs.open(savePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
    
    const response = await httpRequest.request(url, {
      method: http.RequestMethod.GET,
      expectDataType: http.HttpDataType.ARRAY_BUFFER // ⭐ 必须用 ARRAY_BUFFER,绝不能用 STRING
    });

    if (response.responseCode !== http.ResponseCode.OK) {
      throw new Error(`HTTP ${response.responseCode}`);
    }

    const arrayBuffer = response.result as ArrayBuffer;
    const uint8Array = new Uint8Array(arrayBuffer);
    
    // 2. 一次性写入(小文件)或分块写入(大文件)
    const writeLen = await fs.write(fileFd.fd, uint8Array.buffer);
    totalWritten = writeLen;
    
    if (writeLen !== uint8Array.length) {
      throw new Error(`Partial write: expected=${uint8Array.length}, written=${writeLen}`);
    }

    // 3. 【最关键】显式 fsync + close
    await fs.fsync(fileFd.fd);
    await fs.close(fileFd);
    fileFd = null; // 置空防止 finally 重复关闭
    
    // 4. 二次校验
    const stat = await fs.stat(savePath);
    if (stat.size !== totalWritten) {
      throw new Error(`Post-write size mismatch: stat=${stat.size}, written=${totalWritten}`);
    }

    hilog.info(0x0000, 'ManualDL', `✅ Success: ${stat.size} bytes`);

  } catch (err) {
    // 失败时清理不完整文件
    if (fileFd) {
      await fs.close(fileFd).catch(() => {});
    }
    await fs.unlink(savePath).catch(() => {});
    throw err;
  } finally {
    httpRequest.destroy();
  }
}

致命陷阱:如果上面代码中 expectDataType 用了默认的 STRING,二进制文件会被当作 UTF-8 文本解码再编码,导致:

  • 非法字节序列被替换为 U+FFFD()
  • 单字节 0x80-0xFF 被扩展为 2-3 字节
  • 文件体积变大且内容彻底损坏

这是"文件偏大"场景的头号元凶。


 工程化防御体系

防御层 1:写入即校验(Write-Verify Pattern)

每次下载完成后,强制执行三重校验:

interface DownloadManifest {
  url: string;
  expectedSize: number;   // 从 Content-Length 头获取
  expectedMd5?: string;   // 服务端提供
  expectedSha256?: string;
}

async function verifiedDownload(manifest: DownloadManifest): Promise<string> {
  const path = await downloader.downloadWithVerification(
    manifest.url,
    manifest.expectedSize,
    manifest.expectedMd5
  );
  
  // 额外:读取首尾字节做魔数校验(Magic Number)
  await verifyMagicNumber(path, manifest.url);
  
  return path;
}

async function verifyMagicNumber(filePath: string, url: string): Promise<void> {
  const fd = await fs.open(filePath, fs.OpenMode.READ_ONLY);
  const headerBuf = new ArrayBuffer(8);
  await fs.read(fd.fd, headerBuf, { length: 8, offset: 0 });
  await fs.close(fd);
  
  const header = new Uint8Array(headerBuf);
  const ext = url.split('.').pop()?.toLowerCase();
  
  const magicMap: Record<string, number[]> = {
    'png': [0x89, 0x50, 0x4E, 0x47],
    'jpg': [0xFF, 0xD8, 0xFF],
    'pdf': [0x25, 0x50, 0x44, 0x46], // %PDF
    'zip': [0x50, 0x4B, 0x03, 0x04],
  };
  
  if (ext && magicMap[ext]) {
    const expected = magicMap[ext];
    for (let i = 0; i < expected.length; i++) {
      if (header[i] !== expected[i]) {
        throw new Error(`Magic number mismatch for .${ext}: file may be corrupted or wrong type`);
      }
    }
  }
}

防御层 2:断点续传 + 范围请求

大文件下载必须支持 Range 请求,避免中途失败后全量重下导致的不一致窗口:

async function resumableDownload(url: string, savePath: string): Promise<void> {
  let existingSize = 0;
  
  try {
    const stat = await fs.stat(savePath);
    existingSize = stat.size;
  } catch (_) {
    // 文件不存在,从头开始
  }

  const headers: Record<string, string> = {};
  if (existingSize > 0) {
    headers['Range'] = `bytes=${existingSize}-`;
  }

  const httpRequest = http.createHttp();
  const response = await httpRequest.request(url, {
    method: http.RequestMethod.GET,
    header: headers,
    expectDataType: http.HttpDataType.ARRAY_BUFFER
  });

  const isPartial = response.responseCode === http.ResponseCode.PARTIAL_CONTENT; // 206
  const mode = isPartial 
    ? fs.OpenMode.APPEND | fs.OpenMode.WRITE_ONLY 
    : fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY | fs.OpenMode.TRUNC;

  const fd = await fs.open(savePath, mode);
  try {
    const data = new Uint8Array(response.result as ArrayBuffer);
    await fs.write(fd.fd, data.buffer);
    await fs.fsync(fd.fd);
  } finally {
    await fs.close(fd);
    httpRequest.destroy();
  }
  
  // 最终校验总大小
  const finalStat = await fs.stat(savePath);
  const contentLength = parseInt(response.header['content-length'] as string);
  const expectedTotal = isPartial ? existingSize + contentLength : contentLength;
  
  if (finalStat.size !== expectedTotal) {
    throw new Error(`Resumable download size mismatch: ${finalStat.size} vs ${expectedTotal}`);
  }
}

防御层 3:沙箱空间预检

写入前检查可用空间,避免静默截断:

import { fileIo as fs } from '@kit.CoreFileKit';

async function checkDiskSpace(requiredBytes: number, context: Context): Promise<void> {
  const statvfs = await fs.statvfs(context.filesDir);
  const availableBytes = statvfs.f_bavail * statvfs.f_bsize;
  const safetyMargin = 1.2; // 预留 20% 余量
  
  if (availableBytes < requiredBytes * safetyMargin) {
    throw new Error(
      `Insufficient disk space: need=${Math.ceil(requiredBytes * safetyMargin)}, ` +
      `available=${availableBytes}`
    );
  }
}

 诊断与验证清单

检查项 命令/代码 预期
对比源文件与沙箱文件大小 hdc shell ls -l /data/app/.../files/xxx vs 服务器 Content-Length 完全相等
对比 MD5 本地计算 vs 服务端提供值 完全一致
检查是否用了 STRING 模式 搜索代码中 expectDataType 或默认 request 调用 二进制文件必须用 ARRAY_BUFFER
检查 fs.close 是否被调用 审查所有 fs.open 配对 每个 open 必有 close,且在 finally 中
检查是否有 fsync 关键文件写入后 存在 fs.fsync(fd) 调用
模拟弱网测试 DevEco Network Throttling 设为 2G/高丢包 下载失败时不留残留文件,重试后能恢复
填充沙箱至 95% 后下载 手动写入大文件占满空间 应抛出明确的空间不足异常,而非静默截断

 快速自查决策树

文件大小不一致 + 打开损坏
    │
    ├─ 沙箱文件 < 源文件?
    │   ├─ 是 → 检查 fs.close/fsync 是否遗漏
    │   │       检查是否分片下载漏了最后一片
    │   │       检查沙箱空间是否不足
    │   └─ 否(偏大)→ 几乎必然是用了 STRING 模式写二进制
    │                   立即改为 ARRAY_BUFFER
    │
    ├─ 大小相同但内容损坏?
    │   └─ 检查编码转换 / 字节序 / 加密解密环节
    │
    ├─ 偶发性不一致?
    │   └─ 异步竞态 → 加锁或串行化下载队列
    │       弱网中断 → 实现断点续传 + 临时文件 rename 模式
    │
    └─ 稳定复现?
        └─ 打印每一步的 byte count:
            network received → buffer length → fs.write return → fs.stat size
            定位差异发生在哪一环

 核心原则总结

  1. 永远信任校验,不信任回调on('complete') 只是网络层信号,必须用 stat.size + MD5 二次确认
  2. 二进制文件禁用字符串模式:这是"文件偏大"的万恶之源
  3. 临时文件 + rename 原子提交:避免写入中途崩溃留下半残文件污染后续逻辑
  4. fsync 是数据安全落盘的最后一道闸门:尤其在掉电/强杀场景下至关重要
  5. 服务端必须提供 Content-Length + MD5/SHA256:没有校验源的下载等于裸奔

展开全部
12楼回复于7天前 来自安徽

这种情况可以优先检查下载数据 → 写入沙箱的过程,下载接口返回成功并不代表最终文件内容一定完整。

常见原因有几个:

  1. 分段下载/断点续传处理有问题

    如果使用 Range 分段下载,需要确认每段数据的 Content-Range、长度以及写入文件的 offset 是否正确。offset 算错很容易导致文件偏大、偏小或者内容覆盖。

  2. 二进制数据被当成字符串处理

    图片、PDF、压缩包等必须按二进制数据写入。中间如果经过字符串转换、UTF-8 编解码,很容易导致文件大小变化和文件损坏。

  3. 写文件没有处理完整

    重点确认下载数据长度和实际 write() 返回长度是否一致。异步写文件时也要确保所有写操作完成后再认为下载成功。

  4. 文件追加/覆盖模式错误

    重复下载时,如果使用追加模式,可能导致文件比源文件大;如果使用覆盖模式但下载过程提前结束,则可能比源文件小。

  5. 云端返回的实际内容不是源文件

    建议直接检查 HTTP 响应的 Content-LengthContent-RangeContent-Encoding。例如服务器启用了 gzip/压缩传输时,响应体长度和文件原始大小不能直接比较。

排查时建议记录这几个值:

源文件实际大小
HTTP Content-Length
实际收到的数据长度
实际写入文件长度
最终沙箱文件 stat().size

如果实际收到的数据长度就已经和源文件不一致,重点查网络请求/Range/服务端。

如果收到的数据长度正确,但沙箱文件大小不一致,重点查文件写入逻辑。

如果两边大小一致但文件仍然打不开,再检查二进制数据是否被转换、截断或拼接顺序错误。

尤其是 HarmonyOS 沙箱下载文件,建议优先确认:下载响应拿到的是 ArrayBuffer/Uint8Array 等二进制数据,并且写文件时按二进制写入,不要经过字符串转换。

展开全部
11楼回复于7天前 来自辽宁
文件下载成功但沙箱内文件损坏、大小与源文件不符,且下载过程无报错,这通常是数据在"接收-缓冲-写入"链路中被截断、填充或转码导致的。1.ArrayBuffer 大小不匹配:这是鸿蒙文件下载中最隐蔽的陷阱。很多开发者在读取网络响应或拷贝文件时,使用了固定大小的 ArrayBuffer,导致实际数据与缓冲区长度不匹配。2.流未正确关闭 / 缓冲未刷新写入文件后如果没有关闭文件描述符(fd),操作系统缓冲区的数据可能未完全落盘,导致沙箱文件大小为 0B 或小于实际大小。3.二进制数据被当作文本处理在 ArkTS/JS 层处理 HTTP 响应时,如果错误地将二进制数据转为 string 再转回 ArrayBuffer,会导致编码污染。4.服务器 Content-Encoding 导致数据被二次压缩如果服务器对文件进行了 gzip/brotli 压缩传输,而客户端没有正确解压,保存下来的就是压缩后的数据,不是原始文件。可以查看抓包或打印响应头,检查是否有 Content-Encoding: gzip以及对比沙箱文件与源文件的前几个字节:gzip 文件头通常是 1f 8b,而正常图片(如 JPEG)是 ff d86.分块下载 / Range 请求合并错如果是从 rawfile 资源目录拷贝到沙箱,使用 resourceManager.getRawFd() 获取的 fd 可能带有初始偏移量,直接 copyFile 会导致数据拷贝不全。需要查看在拷贝前确保 fd 偏移量在 0 位置,或读取完整内容后再写入
10楼编辑于7天前 来自山东
共2页
写回答
新增插入模板功能
一键使用模板,快速填写内容,轻松发帖~
知道了
  • 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
  • 如您发布的内容为转载内容,请注明内容来源。

我要提问题

了解社区公约,与您携手共创和谐专业的开发者社区。