38 图片压缩与本地存储:文件管理与缓存策略
前言
图:38 图片压缩与本地存储:文件管理与缓存策略 运行效果截图(HarmonyOS NEXT)
拍照或从相册选取图片后,原始图片通常体积较大(单张可达 10MB+),如果不经过压缩和处理就直接存储,不仅浪费存储空间,还可能影响 App 的读写性能。
本文解析鸿蒙应用中的图片压缩策略、文件存储管理和缓存清理机制。
鸿蒙官方·文件管理文档:developer.huawei.com
项目源码仓库:harmony-app GitHub
图:图片压缩与本地存储全流程——ImagePacker 编码 → fileIo 写入 → 缓存管理
一、图片压缩策略
1.1 使用 ImagePacker 压缩
import{image}from'@kit.ImageKit'import{fileIoasfs}from'@kit.CoreFileKit'asyncfunctioncompressImage(inputPath:string,outputPath:string):Promise<void>{// 1. 加载原图constsource=image.createImageSource(inputPath)constpixelMap=awaitsource.createPixelMap({desiredPixelFormat:image.ImagePixelFormat.RGBA_8888,desiredSize:{width:1080,height:1440}// 限制最大分辨率})// 2. 压缩编码constpacker=image.createImagePacker()constpackedImage=awaitpacker.packing(pixelMap,{format:'image/jpeg',quality:80// 压缩质量 (0-100)})// 3. 写入文件constfile=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 项目中的文件存储
// 拍照后临时保存到缓存目录constcacheDir=`/data/storage/el2/base/cache/`constfileName=`capture_${Date.now()}.jpg`constfilePath=`${cacheDir}${fileName}`// 保存照片// 在 DAO 中只存储路径awaitHandwritingDao.create({image_path:filePath,// 图片路径// ...})2.3 缓存 vs 持久存储
| 对比 | 缓存 (cache) | 持久 (files) |
|---|---|---|
| 写入本项目的文件 | 拍照原始文件 | 数据库备份 |
| 清理条件 | App 设置清缓存、系统空间不足 | 手动删除 |
| 路径 | .../cache/ | .../files/ |
| 本项目存储内容 | 临时拍摄照片 | RDB 数据库文件 |
三、文件生命周期管理
3.1 文件创建
// CapturePage — 拍照时创建constfilePath=`/data/storage/el2/base/cache/capture_${Date.now()}.jpg`// 保存到 DB 供其他页面使用AppStorage.setOrCreate<string>('capture_image_path',filePath)3.2 文件使用(跨页面)
// AnalyzingPage — 读取路径constimagePath=AppStorage.get<string>('capture_image_path')??''// 写入 DBawaitHandwritingDao.create({image_path:imagePath,// ...})3.3 文件清理策略
// 清理旧缓存文件asyncfunctioncleanOldCache(maxAgeDays:number=7):Promise<void>{constcacheDir=`/data/storage/el2/base/cache/`constnow=Date.now()constmaxAge=maxAgeDays*86400000try{// 列出缓存目录文件constfiles=fs.listFileSync(cacheDir)for(constfileNameoffiles){if(fileName.startsWith('capture_')){constfilePath=`${cacheDir}${fileName}`conststat=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'asyncfunctioncheckStorage():Promise<{free:number,total:number}>{conststat=statfs.getFreeBytes('/data/storage/el2/base/')consttotal=statfs.getTotalBytes('/data/storage/el2/base/')return{free:stat/(1024*1024),// 转换为 MBtotal:total/(1024*1024)}}// 使用示例constspace=awaitcheckStorage()if(space.free<100){// 剩余空间不足 100MB,提示用户清理}六、非相机图片的存储
6.1 Demo 种子数据
DemoSeeder 生成的演示数据中,image_path为空字符串,因为不是真实拍摄的:
awaitHandwritingDao.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()}时间戳方案 |
总结
本文解析了鸿蒙应用中图片压缩与本地存储的完整方案:
- 图片压缩:
ImagePacker.packing()+quality=80+desiredSize=1080p - 存储位置:
cache/目录存储临时拍摄照片,DB存储路径引用 - 文件清理:定期清理 7 天前的旧缓存文件
- 路径管理:不同来源的图片路径统一通过 AppStorage 和 DB 传递
- 权限策略:cache 目录不需要额外权限
下一篇文章将进入NFC 碰一碰绑定模块。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
参考资源:
- fileIo 文件管理 API
- ImagePacker 图片编码
- @kit.CoreFileKit 模块
- @kit.ImageKit 模块
- 鸿蒙应用目录结构
- statfs 磁盘空间查询
- HarmonyOS 开发文档
七、文件存储最佳实践
7.1 目录结构规划
鸿蒙应用的文件存储遵循沙箱隔离原则,常用目录说明:
| 目录 | 路径 | 用途 | 特点 |
|---|---|---|---|
| cacheDir | /data/storage/el2/base/cache | 图片缓存 | 可被系统清理 |
| filesDir | /data/storage/el2/base/files | 持久化文件 | 不会被自动清理 |
| tempDir | /data/storage/el2/base/temp | 临时文件 | 应用退出后清理 |
constcontext=getContext(this)// 持久化存储(不被系统清理)constpersistentPath=`${context.filesDir}/images/`// 缓存存储(可被系统清理)constcachePath=`${context.cacheDir}/thumbnails/`7.2 文件命名策略
// ✅ 好的命名:包含时间戳 + 类型 + 后缀constfileName=`handwriting_${userId}_${Date.now()}.jpg`// ❌ 避免的命名:容易冲突constbadName=`photo.jpg`// 多次拍照会覆盖constbadName2=`image_${Math.random()}.jpg`// 随机数可能重复7.3 缓存清理策略
// 定期清理超过 7 天的缓存图片asyncfunctioncleanOldCache(context:common.UIAbilityContext):Promise<void>{constcacheDir=context.cacheDirconstfiles=fs.listFileSync(cacheDir)constnow=Date.now()constsevenDaysMs=7*24*60*60*1000for(constfileoffiles){constfilePath=`${cacheDir}/${file}`conststat=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 磁盘空间检查
在写入文件前,建议先检查磁盘可用空间:
importstatfsfrom'@ohos.file.statvfs'asyncfunctioncheckDiskSpace(path:string,requiredBytes:number):Promise<boolean>{try{conststat=awaitstatfs.statvfs(path)constfreeBytes=stat.bfree*stat.bsizeif(freeBytes<requiredBytes){hilog.warn(0x0000,'Storage',`磁盘空间不足: 可用${freeBytes}bytes,需要${requiredBytes}bytes`)returnfalse}returntrue}catch(err){hilog.error(0x0000,'Storage','检查磁盘空间失败: %s',JSON.stringify(err))returntrue// 检查失败时允许继续}}// 使用示例consthasSpace=awaitcheckDiskSpace(context.cacheDir,5*1024*1024)// 需要 5MBif(!hasSpace){this.showToast('存储空间不足,请清理缓存后重试')return}如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!