第30篇|图片文件落盘:沙箱路径、Uri 与后续读取

第30篇|图片文件落盘:沙箱路径、Uri 与后续读取

第 30 篇关注照片落盘。相机项目最容易把"拍到照片"和"能长期使用照片"混在一起,实际上它们是两件事:拍到的是内存里的 JPEG 数据,长期使用需要把数据写入应用沙箱,并让相册、地图、分享、云同步都能用同一种路径规则读取。双镜记忆相机把拍摄文件统一放在 filesDir/dual_captures,再由记录服务转换为可展示的 file:// Uri。

本文是 21 天「智能相机开发实战」训练营中的一篇实操记录。所有代码片段都来自当前项目,配图围绕运行页面和源码关键路径展开,读完以后可以直接回到工程里按函数名定位。

本篇目标

  • 理解应用沙箱路径和相册展示 Uri 的区别。
  • 读懂目录创建、文件路径拼接、byteBuffer 写入三个动作。
  • 知道为什么记录模型里同时保留 path 和 uri。
  • 把文件保存失败和相册入库失败拆成两个可排查问题。

代码位置

  • entry/src/main/ets/pages/Index.ets
  • entry/src/main/ets/services/GalleryRecordService.ets

一、效果不是"有图",而是重启后还有图

相册页能看到缩略图,只能说明当前记录可以被页面读取。真正合格的落盘链路,还要保证 App 重启后记录仍然存在,图片路径仍能被转换成可展示 Uri,后续导出、分享或云同步也能继续引用同一份文件。项目把图片文件放在固定目录里,不把临时路径塞进 UI 层。

图1 图片落盘链路:沙箱目录、文件写入、file Uri 和相册缩略图

二、路径生成:所有拍摄文件先进入 dual_captures

路径生成由 ensureCaptureDirectorybuildCaptureFilePath 负责。前者拿到 UIAbilityContext.filesDir 并确保 dual_captures 目录存在,后者根据拍摄角色和时间戳生成文件名。文件名里带 role,可以区分后摄、前摄和合成图;时间戳则和 captureId 对齐,方便从日志追踪一次拍摄。

图2 ensureCaptureDirectory 与 buildCaptureFilePath 定义本地保存位置

ts 复制代码
  private ensureCaptureDirectory(): string {
    const hostContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
    const captureDir = `${hostContext.filesDir}/dual_captures`;
    this.ensureDirectoryExists(captureDir);
    return captureDir;
  }

  private ensureDirectoryExists(targetPath: string): void {
    if (this.pathExists(targetPath)) {
      return;
    }
    try {
      fs.mkdirSync(targetPath);
    } catch (error) {
      if (!this.pathExists(targetPath)) {
        console.error(`Failed to create capture directory: ${JSON.stringify(error)}`);
      }
    }
  }

  private pathExists(targetPath: string): boolean {
    try {
      return fs.accessSync(targetPath);
    } catch (error) {
      return false;
    }
  }

  private unlinkLocalFileQuietly(targetPath: string): void {
    if (targetPath.trim().length === 0 || !this.pathExists(targetPath)) {
      return;
    }
    try {
      fs.unlinkSync(targetPath);
    } catch (error) {
      console.error(`Failed to delete local file: ${JSON.stringify(error)}`);
    }
  }

  private buildCaptureFilePath(role: 'back' | 'front', timestamp: string): string {
    return `${this.ensureCaptureDirectory()}/${timestamp}_${role}.jpg`;
  }

  private buildCompositeCaptureFilePath(timestamp: string): string {
    return `${this.ensureCaptureDirectory()}/${timestamp}_dual.jpg`;
  }

这一层不要放到组件模板里临时拼字符串。路径规则一旦集中,后面无论是单拍、双拍还是顺序双拍,都只需要告诉它角色和时间戳。

三、文件写入:把 JPEG byteBuffer 变成可持久保存的文件

writeCaptureFile 使用 fs.openSync 创建或覆盖目标文件,再把 JPEG 的 byteBuffer 写进去。这里的返回值是布尔值,调用方可以在失败时中断入库。它没有吞掉错误,而是记录日志并返回 false,这样 UI 层可以继续走统一失败处理。

图3 writeCaptureFile 将 JPEG byteBuffer 写入目标文件

ts 复制代码
  private writeCaptureFile(targetPath: string, buffer: ArrayBuffer): boolean {
    let file: fs.File | undefined = undefined;
    try {
      file = fs.openSync(
        targetPath,
        fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY | fs.OpenMode.TRUNC
      );
      fs.writeSync(file.fd, buffer);
      return true;
    } catch (error) {
      console.error(`Failed to write capture file: ${JSON.stringify(error)}`);
      return false;
    } finally {
      if (file) {
        try {
          fs.closeSync(file);
        } catch (error) {
          console.error(`Failed to close capture file: ${JSON.stringify(error)}`);
        }
      }
    }
  }

写入成功并不等于用户已经看到了照片,它只是完成了底层文件动作。后续还要创建 GalleryMoment,让相册页知道这张图片应该如何展示。

四、Uri 规范化:展示层读取 file://,存储层保留原始 path

记录服务里有一个小但关键的转换:toFileUri。当路径已经是 file:// 时直接返回;当路径是沙箱绝对路径时,补上 file:// 前缀;路径为空时返回空字符串。这样相册组件、详情页、导出逻辑都不需要重复判断路径格式。

图4 GalleryRecordService 将本地路径规范化为 file Uri

ts 复制代码
  private static toFileUri(path: string): string {
    if (!path || path.trim().length === 0) {
      return '';
    }
    if (path.startsWith('file://')) {
      return path;
    }
    return `file://${path}`;
  }

  private static toPhotoImageUri(path: string, storedUri: string): string {
    const normalizedUri = storedUri ? storedUri.trim() : '';
    if (normalizedUri.length > 'file://'.length) {
      return normalizedUri;
    }
    return GalleryRecordService.toFileUri(path);
  }

保留 backPath/frontPath 是为了文件操作,生成 backUri/frontUri 是为了页面展示。两者边界清楚,后续做导出或云同步时不会把 UI Uri 当成本地文件路径误用。

工程检查清单

  • 文件目录必须从 UIAbilityContext.filesDir 推导,避免写到不可控位置。
  • 路径规则集中在一个函数里,单拍、双拍和合成图复用同一套命名。
  • 写文件失败要停止入库,不能创建指向空文件的相册记录。
  • 展示 Uri 由服务层规范化,UI 不重复拼接 file://
  • 日志里要保留 role、targetPath 和 captureId,方便真机排查。

今日练习

  1. 搜索 buildCaptureFilePath 的调用点,确认单拍、双拍、合成图分别传入什么 role。
  2. 在真机拍一张照片后,通过日志确认目标路径是否进入 dual_captures
  3. 思考如果文件写入成功但记录保存失败,用户侧应该看到什么提示。

下一篇会继续沿着同一条工程链路往下拆:先看用户能看到的效果,再回到源码确认状态、文件和服务边界是否闭合。

相关推荐
枫叶丹41 小时前
【HarmonyOS 6.0】Live View Kit 实况窗开发详解:进度胶囊支持副文本功能探究
开发语言·华为·harmonyos
想你依然心痛2 小时前
HarmonyOS 6(API 23)智能体驱动的沉浸式AR城市地下管网运维中心
运维·ar·harmonyos·智能体
Goway_Hui3 小时前
【鸿蒙原生应用开发--ArkUI--014】Expense-tracker 记账应用开发教程
华为·harmonyos
不羁的木木3 小时前
《HarmonyOS技术精讲》五:实战项目 ── 智能支架助手
华为·harmonyos
枫叶丹43 小时前
【HarmonyOS 6.0】Map Kit瓦片图层深度解析:本地加载方式与瓦片数据缓存能力
开发语言·缓存·华为·harmonyos
大雷神3 小时前
第29篇|单拍按钮背后:从点击到 PhotoOutput 回调
harmonyos
不羁的木木3 小时前
《HarmonyOS底部页签-沉浸光感组件实战》模糊样式:打造毛玻璃效果
华为·harmonyos
大雷神10 小时前
第26篇|单摄预览会话:CameraInput、PreviewOutput、PhotoSession 的关系
harmonyos
不羁的木木18 小时前
Form Kit(卡片开发服务)学习笔记01-核心概念与架构设计
笔记·学习·harmonyos