38 图片压缩与本地存储:文件管理与缓存策略

前言

在这里插入图片描述

图:38 图片压缩与本地存储:文件管理与缓存策略 运行效果截图(HarmonyOS NEXT)

拍照或从相册选取图片后,原始图片通常体积较大(单张可达 10MB+),如果不经过压缩和处理就直接存储,不仅浪费存储空间,还可能影响 App 的读写性能。

本文解析鸿蒙应用中的图片压缩策略、文件存储管理和缓存清理机制。

鸿蒙官方·文件管理文档:developer.huawei.com
项目源码仓库:harmony-app GitHub

图片压缩与存储流程

图:图片压缩与本地存储全流程——ImagePacker 编码 → fileIo 写入 → 缓存管理

PixelMap

ImagePacker
createImagePacker

packing 编码
format+quality

ArrayBuffer
二进制数据

fs.openSync
创建文件

fs.writeSync
写入数据

fs.closeSync
关闭文件

本地文件路径
传入 AnalyzingPage

一、图片压缩策略

1.1 使用 ImagePacker 压缩

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

async function compressImage(inputPath: string, outputPath: string): Promise<void> {
  // 1. 加载原图
  const source = image.createImageSource(inputPath)
  const pixelMap = await source.createPixelMap({
    desiredPixelFormat: image.ImagePixelFormat.RGBA_8888,
    desiredSize: { width: 1080, height: 1440 }  // 限制最大分辨率
  })

  // 2. 压缩编码
  const packer = image.createImagePacker()
  const packedImage = await packer.packing(pixelMap, {
    format: 'image/jpeg',
    quality: 80                                      // 压缩质量 (0-100)
  })

  // 3. 写入文件
  const file = fs.openSync(outputPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
  file.writeSync(packedImage.data.buffer)
  fs.closeSync(file)

  // 4. 释放资源
  source.release()
  packer.release()
}

1.2 压缩参数对比

quality 值 图片质量 文件大小(以 1080p 为例) 适用场景
90-100 极高 2-5MB 保存到相册
80-85 500KB-1.5MB AI 分析输入
60-75 200-600KB 缩略图预览
30-50 50-200KB 网络传输

1.3 压缩效果估算

// 原始照片(后置摄像头)≈ 12MP → 约 5-15MB
// 压缩到 1080p + quality=80 → 约 500KB-1.5MB
// 压缩率:约 90%

二、文件存储位置

2.1 应用目录结构

鸿蒙应用的文件存储有以下几个主要目录:

目录路径 用途 特点 存储策略
.../cache/ 缓存目录 系统可随时清理 ✅ 临时照片
.../files/ 文件目录 持久存储 ❌ 暂不使用
.../database/ 数据库目录 自动管理 RDB 存储位置

2.2 项目中的文件存储

// 拍照后临时保存到缓存目录
const cacheDir = `/data/storage/el2/base/cache/`
const fileName = `capture_${Date.now()}.jpg`
const filePath = `${cacheDir}${fileName}`

// 保存照片
// 在 DAO 中只存储路径
await HandwritingDao.create({
  image_path: filePath,   // 图片路径
  // ...
})

2.3 缓存 vs 持久存储

对比 缓存 (cache) 持久 (files)
写入本项目的文件 拍照原始文件 数据库备份
清理条件 App 设置清缓存、系统空间不足 手动删除
路径 .../cache/ .../files/
本项目存储内容 临时拍摄照片 RDB 数据库文件

三、文件生命周期管理

3.1 文件创建

// CapturePage — 拍照时创建
const filePath = `/data/storage/el2/base/cache/capture_${Date.now()}.jpg`

// 保存到 DB 供其他页面使用
AppStorage.setOrCreate<string>('capture_image_path', filePath)

3.2 文件使用(跨页面)

// AnalyzingPage — 读取路径
const imagePath = AppStorage.get<string>('capture_image_path') ?? ''

// 写入 DB
await HandwritingDao.create({
  image_path: imagePath,
  // ...
})

3.3 文件清理策略

// 清理旧缓存文件
async function cleanOldCache(maxAgeDays: number = 7): Promise<void> {
  const cacheDir = `/data/storage/el2/base/cache/`
  const now = Date.now()
  const maxAge = maxAgeDays * 86400000

  try {
    // 列出缓存目录文件
    const files = fs.listFileSync(cacheDir)
    for (const fileName of files) {
      if (fileName.startsWith('capture_')) {
        const filePath = `${cacheDir}${fileName}`
        const stat = fs.statSync(filePath)
        if (now - stat.mtime > maxAge) {
          fs.unlinkSync(filePath)    // 删除旧文件
          hilog.info(0x0000, 'FileManager', '清理旧缓存: %s', filePath)
        }
      }
    }
  } catch (e) {
    hilog.error(0x0000, 'FileManager', '清理缓存失败: %s', JSON.stringify(e))
  }
}

四、文件操作 API

4.1 项目中使用的方法

方法 用途 本项目使用
fs.openSync(path, mode) 打开/创建文件 ✅ 拍照保存
fs.closeSync(file) 关闭文件 ✅ 用完关闭
file.writeSync(buffer) 写入二进制数据 ✅ 图片数据
fs.listFileSync(dir) 列出目录文件 可用于清理
fs.unlinkSync(path) 删除文件 可用于清理
fs.statSync(path) 获取文件状态 可用于检查

4.2 文件打开模式

模式 说明
fs.OpenMode.READ_ONLY 只读
fs.OpenMode.WRITE_ONLY 只写(会清空已有内容)
fs.OpenMode.READ_WRITE 读写
fs.OpenMode.CREATE 不存在时创建
fs.OpenMode.APPEND 追加写入

本项目使用: fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE

五、存储空间管理

5.1 存储空间估算

数据类型 单条数据大小 估算总量(1000 条)
压缩后照片 ~800KB ~800MB
RDB 数据库 ~5KB ~5MB
App 代码 - ~20MB

5.2 空间检查

import { statfs } from '@kit.CoreFileKit'

async function checkStorage(): Promise<{ free: number, total: number }> {
  const stat = statfs.getFreeBytes('/data/storage/el2/base/')
  const total = statfs.getTotalBytes('/data/storage/el2/base/')
  return {
    free: stat / (1024 * 1024),   // 转换为 MB
    total: total / (1024 * 1024)
  }
}

// 使用示例
const space = await checkStorage()
if (space.free < 100) {
  // 剩余空间不足 100MB,提示用户清理
}

六、非相机图片的存储

6.1 Demo 种子数据

DemoSeeder 生成的演示数据中,image_path 为空字符串,因为不是真实拍摄的:

await HandwritingDao.create({
  archive_id: archiveId,
  source: 'demo',
  image_path: '',       // 演示数据无图片
  ocr_text: p.text,
  feature_json: JSON.stringify(p.scores),
  created_at: timestamp
})

6.2 图片缺失的处理

在 UI 层展示时,需要处理图片路径为空的情况:

if (imagePath && imagePath !== '') {
  Image(imagePath)
    .width(120).height(120)
    .objectFit(ImageFit.Contain)
} else {
  // 无图片的占位符
  Text('📝')
    .fontSize(40)
}

七、权限声明

module.json5 中声明文件读写权限(虽然 cache 目录不需要额外权限):

{
  "requestPermissions": [
    // cache 目录读写不需要额外权限
    // 但访问 files 目录需要:
    // {
    //   "name": "ohos.permission.WRITE_MEDIA",
    //   ...
    // }
  ]
}

提示:应用私有的 cache/files/ 目录不需要额外权限即可读写。只有访问系统相册或其他应用的共享文件时才需要媒体权限。

八、文件管理最佳实践

8.1 检查清单

检查项 项目中的做法
是否使用 cache 而非 files ✅ 临时照片存 cache
文件是否有关联记录在 DB ✅ HandwritingDao 存 image_path
是否有文件清理策略 ✅ 可定期清理 7 天前的缓存
是否在组件销毁时释放文件句柄 ✅ fs.closeSync 立即关闭
文件名是否唯一 ${Date.now()} 时间戳方案

总结

本文解析了鸿蒙应用中图片压缩与本地存储的完整方案:

  1. 图片压缩ImagePacker.packing() + quality=80 + desiredSize=1080p
  2. 存储位置cache/ 目录存储临时拍摄照片,DB 存储路径引用
  3. 文件清理:定期清理 7 天前的旧缓存文件
  4. 路径管理:不同来源的图片路径统一通过 AppStorage 和 DB 传递
  5. 权限策略:cache 目录不需要额外权限

下一篇文章将进入 NFC 碰一碰绑定 模块。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


参考资源:


七、文件存储最佳实践

7.1 目录结构规划

鸿蒙应用的文件存储遵循沙箱隔离原则,常用目录说明:

目录 路径 用途 特点
cacheDir /data/storage/el2/base/cache 图片缓存 可被系统清理
filesDir /data/storage/el2/base/files 持久化文件 不会被自动清理
tempDir /data/storage/el2/base/temp 临时文件 应用退出后清理
const context = getContext(this)
// 持久化存储(不被系统清理)
const persistentPath = `${context.filesDir}/images/`
// 缓存存储(可被系统清理)
const cachePath = `${context.cacheDir}/thumbnails/`

7.2 文件命名策略

// ✅ 好的命名:包含时间戳 + 类型 + 后缀
const fileName = `handwriting_${userId}_${Date.now()}.jpg`

// ❌ 避免的命名:容易冲突
const badName = `photo.jpg`  // 多次拍照会覆盖
const badName2 = `image_${Math.random()}.jpg`  // 随机数可能重复

7.3 缓存清理策略

// 定期清理超过 7 天的缓存图片
async function cleanOldCache(context: common.UIAbilityContext): Promise<void> {
  const cacheDir = context.cacheDir
  const files = fs.listFileSync(cacheDir)
  const now = Date.now()
  const sevenDaysMs = 7 * 24 * 60 * 60 * 1000

  for (const file of files) {
    const filePath = `${cacheDir}/${file}`
    const stat = fs.statSync(filePath)
    if (now - stat.mtime * 1000 > sevenDaysMs) {
      fs.unlinkSync(filePath)
      hilog.info(0x0000, 'Cache', `已清理过期缓存: ${file}`)
    }
  }
}

7.4 压缩质量参考指南

quality 值 文件大小(相对原始) 视觉质量 推荐场景
100 50-80% 无损失 原图存档
85 30-50% 几乎无差异 ✅ 本项目选用
70 20-35% 轻微压缩 网络传输
50 10-20% 明显压缩 缩略图
30 5-10% 明显失真 极小文件

7.5 磁盘空间检查

在写入文件前,建议先检查磁盘可用空间:

import statfs from '@ohos.file.statvfs'

async function checkDiskSpace(path: string, requiredBytes: number): Promise<boolean> {
  try {
    const stat = await statfs.statvfs(path)
    const freeBytes = stat.bfree * stat.bsize
    if (freeBytes < requiredBytes) {
      hilog.warn(0x0000, 'Storage', `磁盘空间不足: 可用 ${freeBytes} bytes,需要 ${requiredBytes} bytes`)
      return false
    }
    return true
  } catch (err) {
    hilog.error(0x0000, 'Storage', '检查磁盘空间失败: %s', JSON.stringify(err))
    return true  // 检查失败时允许继续
  }
}

// 使用示例
const hasSpace = await checkDiskSpace(context.cacheDir, 5 * 1024 * 1024)  // 需要 5MB
if (!hasSpace) {
  this.showToast('存储空间不足,请清理缓存后重试')
  return
}

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐