OpenHarmony 通用文件工具 FileUtil(沙盒文件读写、目录操作、缓存清理 API23)
摘要
项目中日志、临时图片、缓存、本地配置文件都需要文件 IO 操作,原生@ohos.fileio、@ohos.file.fs接口零散,缺少统一封装:目录创建、文本读写、二进制复制、文件删除、遍历文件夹、缓存大小计算、清空缓存等通用逻辑重复编写。封装全局 FileUtil 统一管理沙盒私有目录操作,包含文本读写、文件复制删除、文件夹遍历、缓存统计清理、临时文件管理能力,配合前文图片工具、网络上传工具使用。API23 重构文件句柄生命周期、沙盒路径权限、异步文件锁机制,解决文件句柄泄漏、并发读写错乱、缓存无法彻底删除、外部文件访问权限不足等问题。
关键词
OpenHarmony;ArkTS;API23;fileio;fs;文件读写;沙盒目录;缓存清理;文件工具
一、引言
1.1 文件开发痛点
- 每次读写文件都要手动判断目录是否存在,不存在需递归创建文件夹;
- 文件打开后忘记 close 句柄,长期运行导致文件句柄泄漏,无法读写新文件;
- 文本读写、二进制复制、删除文件逻辑分散,大量重复模板代码;
- 缓存文件夹无法一键清理,缺少缓存占用大小计算功能;
- 页面销毁临时文件残留占用存储空间;
- 并发读写同一文件出现内容错乱、文件损坏。
1.2 应用沙盒内置目录说明(无需动态权限)
表格
| 目录 | 用途 |
|---|---|
| context.filesDir | 持久化私有文件,重启不删除 |
| context.cacheDir | 缓存目录,系统可自动清理,适合临时图片 / 日志 |
| context.tempDir | 临时目录,APP 后台休眠自动清空 |
API23 文件模块核心升级:
- 所有文件操作强制异步优先,同步 IO 禁止在主线程使用;
- 自动文件锁,同一文件并发读写串行执行,防止内容损坏;
- 新增批量删除目录递归接口,简化缓存清理;
- 统一沙盒路径访问校验,杜绝越权访问外部文件;
- 文件句柄自动回收机制,异常场景自动关闭 fd。
二、完整文件工具封装 utils/file_util.ets
ets
import fileio from '@ohos.fileio'
import fs from '@ohos.file.fs'
import common from '@ohos.app.ability.common'
import promptAction from '@ohos.promptAction'
class FileUtil {
private static instance: FileUtil
private ctx: common.UIAbilityContext | null = null
static getInstance(): FileUtil {
if (!FileUtil.instance) {
FileUtil.instance = new FileUtil()
}
return FileUtil.instance
}
// 注入页面/Ability上下文,获取沙盒根目录
setContext(context: common.UIAbilityContext) {
this.ctx = context
}
// 获取持久化文件根目录 filesDir
getFilesRoot(): string {
if (!this.ctx) return ""
return this.ctx.filesDir
}
// 获取缓存根目录 cacheDir
getCacheRoot(): string {
if (!this.ctx) return ""
return this.ctx.cacheDir
}
// 获取临时目录 tempDir
getTempRoot(): string {
if (!this.ctx) return ""
return this.ctx.tempDir
}
// 递归创建文件夹,不存在则创建
async mkdir(dirPath: string): Promise<boolean> {
try {
const exists = await fs.access(dirPath)
if (!exists) {
await fs.mkdir(dirPath, true) // true代表递归创建多级目录
}
return true
} catch (err) {
console.error("mkdir failed", dirPath, err)
return false
}
}
// 写入文本文件(覆盖原有内容)
async writeText(filePath: string, content: string): Promise<boolean> {
try {
// 先确保父目录存在
const parentDir = filePath.substring(0, filePath.lastIndexOf("/"))
await this.mkdir(parentDir)
const fd = await fileio.open(filePath, fileio.OpenMode.CREATE | fileio.OpenMode.WRITE_ONLY)
await fileio.write(fd, content)
await fileio.close(fd)
return true
} catch (err) {
promptAction.showToast({ message: "写入文件失败" })
console.error("writeText error", err)
return false
}
}
// 读取文本文件
async readText(filePath: string): Promise<string> {
try {
const exists = await fs.access(filePath)
if (!exists) return ""
const fd = await fileio.open(filePath, fileio.OpenMode.READ_ONLY)
const stat = await fileio.fstat(fd)
const buf = new ArrayBuffer(stat.size)
await fileio.read(fd, buf)
await fileio.close(fd)
return String.fromCharCode(...new Uint8Array(buf))
} catch (err) {
console.error("readText error", err)
return ""
}
}
// 删除单个文件
async deleteFile(filePath: string): Promise<boolean> {
try {
const exists = await fs.access(filePath)
if (!exists) return true
await fs.unlink(filePath)
return true
} catch (err) {
console.error("deleteFile error", err)
return false
}
}
// 递归删除整个文件夹(含内部所有文件子目录)
async deleteDir(dirPath: string): Promise<boolean> {
try {
const exists = await fs.access(dirPath)
if (!exists) return true
await fs.rmdir(dirPath, { recursive: true })
return true
} catch (err) {
console.error("deleteDir error", err)
return false
}
}
// 计算文件夹总大小(单位:字节)
async getDirSize(dirPath: string): Promise<number> {
let totalSize = 0
try {
const exists = await fs.access(dirPath)
if (!exists) return 0
const dir = await fs.opendir(dirPath)
let entry: fs.Dirent | null
while ((entry = await dir.read()) !== null) {
const fullPath = dirPath + "/" + entry.name
if (entry.isFile()) {
const stat = await fs.stat(fullPath)
totalSize += stat.size
} else if (entry.isDirectory()) {
// 递归累加子文件夹大小
totalSize += await this.getDirSize(fullPath)
}
}
await dir.close()
return totalSize
} catch (err) {
console.error("getDirSize error", err)
return 0
}
}
// 字节自动转换为 KB / MB 展示文本
formatSize(byteNum: number): string {
if (byteNum < 1024) return byteNum + " B"
if (byteNum < 1024 * 1024) return (byteNum / 1024).toFixed(2) + " KB"
return (byteNum / (1024 * 1024)).toFixed(2) + " MB"
}
// 清空缓存目录 cacheDir
async clearCache(): Promise<boolean> {
const cachePath = this.getCacheRoot()
if (!cachePath) return false
return await this.deleteDir(cachePath)
}
// 复制文件
async copyFile(srcPath: string, destPath: string): Promise<boolean> {
try {
const parentDir = destPath.substring(0, destPath.lastIndexOf("/"))
await this.mkdir(parentDir)
await fs.copyFile(srcPath, destPath)
return true
} catch (err) {
console.error("copyFile error", err)
return false
}
}
}
export default FileUtil.getInstance()
三、页面实战调用示例
3.1 日志保存页面(文本读写)
ets
import FileUtil from '../utils/file_util'
@Entry
@Component
struct LogPage {
@State logContent: string = ""
logPath: string = ""
aboutToAppear() {
FileUtil.setContext(getContext(this))
const root = FileUtil.getFilesRoot()
this.logPath = root + "/app_log.txt"
this.loadLog()
}
// 读取本地日志
async loadLog() {
this.logContent = await FileUtil.readText(this.logPath)
}
// 追加日志内容
async writeLog(msg: string) {
const oldText = await FileUtil.readText(this.logPath)
const newText = oldText + "\n" + new Date().toLocaleString() + ":" + msg
await FileUtil.writeText(this.logPath, newText)
await this.loadLog()
}
build() {
Column({ space: 15 }) {
Text("本地日志文件")
.fontSize(22)
TextInput({ text: "", placeholder: "输入日志内容" })
.width("90%")
.onChange(async (val) => await this.writeLog(val))
Scroll() {
Text(this.logContent)
.width("100%")
.padding(10)
.backgroundColor(Color.White)
}
.layoutWeight(1)
.width("90%")
}
.width("100%")
.height("100%")
.padding(12)
}
}
3.2 设置页:缓存大小查看与一键清理
ets
import FileUtil from '../utils/file_util'
import DialogUtil from '../utils/dialog_util'
@Entry
@Component
struct CacheSettingPage {
@State cacheSizeText: string = "计算中..."
async aboutToAppear() {
FileUtil.setContext(getContext(this))
await this.calcCacheSize()
}
// 计算缓存占用
async calcCacheSize() {
const cachePath = FileUtil.getCacheRoot()
const byte = await FileUtil.getDirSize(cachePath)
this.cacheSizeText = FileUtil.formatSize(byte)
}
// 清空缓存
async clearAllCache() {
DialogUtil.confirm({
title: "清理缓存",
content: "确定要清除全部缓存图片、临时文件吗?"
}, async () => {
await FileUtil.clearCache()
await this.calcCacheSize()
DialogUtil.alert({ content: "缓存清理完成" })
})
}
build() {
Column({ space: 30 }) {
Text("缓存管理").fontSize(24).margin({ top: 60 })
Row() {
Text("当前缓存占用:").fontSize(18)
Text(this.cacheSizeText).fontSize(18).fontColor("#007DFF")
}
Button("一键清理缓存")
.width(300)
.height(48)
.backgroundColor("#f56c6c")
.onClick(() => this.clearAllCache())
}
.width("100%")
.height("100%")
.justifyContent(Center)
}
}
3.3 图片工具配套:临时图片存放至缓存目录
ets
// 拼接缓存目录下图片完整路径
function getTempImagePath(fileName: string): string {
const cacheRoot = FileUtil.getCacheRoot()
return cacheRoot + "/temp_img/" + fileName + ".jpg"
}
// 页面销毁清空临时图片文件夹
async function clearTempImg() {
const tempDir = FileUtil.getCacheRoot() + "/temp_img"
await FileUtil.deleteDir(tempDir)
}
四、API23 文件开发编码规范
4.1 上下文与目录规范
- 页面
aboutToAppear调用FileUtil.setContext(getContext(this))初始化沙盒路径; - 持久化数据存
filesDir,临时图片、网络缓存存cacheDir,短时临时文件用tempDir; - 禁止硬编码绝对路径,统一通过工具方法获取根目录拼接子路径。
4.2 文件句柄安全规范
- 所有 open 打开的 fd 必须配套 close,工具内部封装自动关闭,业务层无需手动处理;
- 大文件读写分块 Buffer,避免一次性加载全部内容占用内存;
- 并发读写同一文件依靠 API23 内置文件锁串行执行,不额外加锁。
4.3 缓存清理规范
- APP 设置页提供一键清空 cacheDir 功能;
- 页面退出可删除自身业务临时文件夹,减少存储空间占用;
- 重要业务数据禁止存入 cacheDir,系统会自动回收删除。
4.4 性能规范
- 批量删除目录使用
rmdir(recursive:true)递归删除,比循环删文件更快; - 计算文件夹大小仅在设置页面加载时执行,不频繁轮询;
- 主线程全部使用 async/await 异步文件接口,禁用同步阻塞 IO。
五、高频报错问题解决方案
问题 1:文件写入提示目录不存在 解决:工具内 writeText 自动递归创建父目录,确认已注入上下文获取正确沙盒路径。
问题 2:多次操作文件后无法读写,提示句柄耗尽 解决:工具所有读写逻辑内部自动 close fd,避免遗漏关闭句柄。
问题 3:缓存清理后文件夹仍残留文件 解决:使用 deleteDir 递归删除整个缓存目录,而非遍历单文件删除。
问题 4:读取文件返回空字符串 解决:检查文件路径是否正确,文件是否存在,权限仅允许访问应用自身沙盒。
问题 5:升级 API23 旧版同步 fileio 代码卡顿、编译警告 解决:全部替换为 async/await 异步文件 API,主线程移除同步读写逻辑。
六、总结
FileUtil 统一封装鸿蒙沙盒全套文件操作,覆盖目录创建、文本读写、文件复制删除、缓存大小统计、一键清理缓存核心能力,区分持久化、缓存、临时三大沙盒目录,完美配合 ImagePickerUtil 图片压缩缓存、HttpUtil 网络临时下载文件、本地日志存储等业务模块。 API23 完善文件异步锁、句柄自动回收、递归目录操作能力,解决文件泄漏、并发损坏、缓存清理不干净等问题,是所有存在本地文件存储需求项目必备底层通用工具,可无缝接入前文整套分层架构工程。
更多推荐

所有评论(0)