OpenHarmony 本地文件 FS 文件系统操作封装(API Version23 + 适配版)
摘要
fs 文件系统模块用于应用沙盒目录下文件 / 文件夹的创建、读写、删除、遍历,常用于保存相机拍摄照片、缓存文本、本地日志、离线文档等场景。API Version23 重构文件句柄生命周期、同步 / 异步 IO、权限隔离、大文件分片读写逻辑,修复低版本文件句柄泄漏、大文件读取卡顿、文件夹遍历报错、文件删除残留、沙盒路径访问失效等兼容缺陷。旧项目升级 API23 后常出现:多次读写文件占用无法删除、读取超大文本页面卡死、创建文件夹失败、退出应用文件句柄未释放内存上涨等问题。本文基于 DevEco Studio 适配 OpenHarmony API23 及以上版本,封装通用文件工具类,实现文件读写、文件夹创建、文件删除、遍历目录、图片缓存存储五大核心能力,搭配日志保存、相机图片缓存两大实战页面提供完整可运行代码,输出文件句柄关闭、沙盒路径使用、大文件分片规范,汇总版本升级兼容故障解决方案,为鸿蒙本地文件管理开发提供标准化实操模板。
关键词
OpenHarmony;ArkUI;API Version23;fs;文件系统;沙盒目录;文件读写;目录遍历
一、引言
1.1 文件系统开发背景
应用所有私有文件只能存储在系统分配的独立沙盒目录,无法随意访问设备公共存储。相机拍摄图片、本地日志、离线缓存、自定义文档都依赖 @ohos.file.fs 完成操作。文件操作分为同步、异步两套 API,同步 IO 简单但主线程大量读写会阻塞 UI,异步 IO 适合大文件、批量操作。 OpenHarmony API Version23 针对 fs 底层 IO 引擎完成全面升级,核心变更点:
- 强制文件 fd 句柄使用后必须 closeSync 释放,长期不关闭会锁定文件;
- 统一沙盒路径上下文获取规则,废弃硬编码固定路径写法;
- 优化大文件 Buffer 分片读取,避免一次性加载全部内容占用内存;
- 区分文件 / 文件夹操作接口,新增 existsSync 统一判断资源是否存在;
- 修复多线程并发读写同一文件内容错乱问题,增加文件读写互斥逻辑;
- 限制跨沙盒、系统目录访问,仅允许操作自身 context 沙盒路径。
大量 API9~11 旧项目升级后,文件删不掉、重复写入内容错乱、读取大文件页面卡死,根源是未及时关闭文件 fd、主线程大量同步 IO,因此掌握 API23 标准 fs 封装规范是本地缓存、文件存储类功能必备技能。
1.2 开发环境与测试场景
开发工具:DevEco Studio 5.0 及以上 适配系统:OpenHarmony API Version23、HarmonyOS NEXT 导入模块:@ohos.file.fs、@ohos.app.ability.common 无需额外动态权限:沙盒目录应用自带读写权限 测试场景:写入本地日志文本、相机图片保存、创建缓存文件夹、遍历图片目录、删除过期缓存文件
二、API23+ fs 文件系统核心 API 与版本变更说明
2.1 常用沙盒基础路径(通过上下文获取)
ets
const ctx = getContext(this) ctx.filesDir // 持久化文件目录(常用,图片、文档) ctx.cacheDir // 缓存目录,系统可自动清理 ctx.tempDir // 临时目录,应用重启清空2.2 核心同步基础 API(轻量小文件使用)
- fs.existsSync (path):判断文件 / 文件夹是否存在
- fs.mkdirSync (path):创建文件夹
- fs.openSync (path, mode):打开文件获取 fd 句柄
- fs.writeSync (fd, buffer / 字符串):写入内容
- fs.readSync (fd, buffer):读取内容
- fs.closeSync (fd):释放文件句柄(必执行)
- fs.unlinkSync (path):删除文件
- fs.rmdirSync (path):删除空文件夹
- fs.readdirSync (path):遍历目录下所有文件名称
2.3 文件打开模式常量
ets
fs.OpenMode.CREATE // 文件不存在则创建 fs.OpenMode.WRITE_ONLY // 只写 fs.OpenMode.READ_ONLY // 只读 fs.OpenMode.READ_WRITE // 读写 fs.OpenMode.APPEND // 追加写入,不覆盖原有内容2.4 API23 废弃与强制约束
- 废弃不关闭 fd 的文件读写逻辑,长期运行会出现文件占用报错;
- 禁止硬编码绝对路径,全部通过 UIAbility 上下文获取沙盒根目录;
- 主线程禁止同步一次性读取 1MB 以上大文件,必须分片异步读取;
- rmdirSync 仅能删除空文件夹,删除带文件目录需先遍历删除内部文件;
- 不支持直接访问外部存储、相册原始路径,媒体文件统一用 picker uri。
三、API23 标准基础示例代码
3.1 写入文本文件(追加日志)
ets
import fs from '@ohos.file.fs' import common from '@ohos.app.ability.common' function writeLog(ctx: common.UIAbilityContext, text: string) { const logPath = ctx.filesDir + "/log.txt" const fd = fs.openSync(logPath, fs.OpenMode.CREATE | fs.OpenMode.APPEND | fs.OpenMode.READ_WRITE) const content = text + "\n" fs.writeSync(fd, content) fs.closeSync(fd) }3.2 读取全部文本文件
ets
function readTextFile(ctx: common.UIAbilityContext, filePath: string): string { if (!fs.existsSync(filePath)) return "" const fd = fs.openSync(filePath, fs.OpenMode.READ_ONLY) const stat = fs.statSync(filePath) const buf = new ArrayBuffer(stat.size) fs.readSync(fd, buf) fs.closeSync(fd) return String.fromCharCode(...new Uint8Array(buf)) }3.3 遍历文件夹所有文件名称
ets
function listDir(ctx: common.UIAbilityContext, dirPath: string): string[] { if (!fs.existsSync(dirPath)) return [] return fs.readdirSync(dirPath) }四、两大业务完整实战案例(全兼容 API23)
4.1 实战一:全局文件工具类封装(utils/file_util.ets)
ets
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 } setContext(context: common.UIAbilityContext) { this.ctx = context } // 获取files持久化根目录 getFilesRoot(): string { return this.ctx?.filesDir ?? "" } // 获取cache缓存目录 getCacheRoot(): string { return this.ctx?.cacheDir ?? "" } // 判断文件/文件夹是否存在 exists(path: string): boolean { return fs.existsSync(path) } // 创建文件夹 mkdir(dirPath: string): boolean { try { if (!this.exists(dirPath)) { fs.mkdirSync(dirPath) } return true } catch (err) { promptAction.showToast({ message: "文件夹创建失败" }) return false } } // 写入文本,覆盖原有内容 writeText(path: string, content: string): boolean { try { const fd = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY) fs.writeSync(fd, content) fs.closeSync(fd) return true } catch (err) { promptAction.showToast({ message: "写入文件失败" }) return false } } // 文本追加写入(日志专用) appendText(path: string, content: string): boolean { try { const fd = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.APPEND | fs.OpenMode.READ_WRITE) fs.writeSync(fd, content + "\n") fs.closeSync(fd) return true } catch (err) { return false } } // 读取完整文本 readText(path: string): string { if (!this.exists(path)) return "" try { const fd = fs.openSync(path, fs.OpenMode.READ_ONLY) const stat = fs.statSync(path) const buf = new ArrayBuffer(stat.size) fs.readSync(fd, buf) fs.closeSync(fd) return String.fromCharCode(...new Uint8Array(buf)) } catch (err) { return "" } } // 删除单个文件 deleteFile(path: string): boolean { if (!this.exists(path)) return true try { fs.unlinkSync(path) return true } catch (err) { promptAction.showToast({ message: "文件删除失败" }) return false } } // 遍历目录,返回所有文件名数组 listDirectory(dirPath: string): string[] { if (!this.exists(dirPath)) return [] try { return fs.readdirSync(dirPath) } catch (err) { return [] } } // 清空文件夹内所有文件(保留文件夹) clearDir(dirPath: string) { const files = this.listDirectory(dirPath) for (let name of files) { const fullPath = dirPath + "/" + name this.deleteFile(fullPath) } } } export default FileUtil.getInstance()4.2 实战二:本地日志记录页面(文本读写追加)
ets
import FileUtil from '../utils/file_util' @Entry @Component struct FileLogPage { @State logContent: string = "" logFilePath: string = "" aboutToAppear() { FileUtil.setContext(getContext(this)) this.logFilePath = FileUtil.getFilesRoot() + "/app_log.txt" // 页面加载读取已有日志 this.logContent = FileUtil.readText(this.logFilePath) } // 新增一条日志 addLog() { const time = new Date().toLocaleString() const text = `【${time}】用户操作页面` FileUtil.appendText(this.logFilePath, text) // 刷新显示 this.logContent = FileUtil.readText(this.logFilePath) } // 清空全部日志 clearLog() { FileUtil.writeText(this.logFilePath, "") this.logContent = "" } build() { Column({ space: 16 }) { Text("本地日志文件读写演示") .fontSize(22) .fontWeight(FontWeight.Bold) TextArea({ text: this.logContent, placeholder: "暂无日志记录" }) .width("95%") .layoutWeight(1) .backgroundColor("#fff") .fontSize(14) Row({ space: 20 }) { Button("添加日志记录").width(160).height(44).onClick(() => this.addLog()) Button("清空日志").width(160).height(44).backgroundColor("#f56c6c").onClick(() => this.clearLog()) } .width("100%") .justifyContent(FlexAlign.Center) .margin({ bottom: 20 }) } .width("100%") .height("100%") .padding(12) .backgroundColor("#f5f5f5") } }4.3 实战三:相机图片缓存目录遍历页面
ets
import FileUtil from '../utils/file_util' @Entry @Component struct FileImageListPage { @State imgNameList: string[] = [] imageDir: string = "" aboutToAppear() { FileUtil.setContext(getContext(this)) // 创建图片缓存文件夹 this.imageDir = FileUtil.getFilesRoot() + "/camera_img" FileUtil.mkdir(this.imageDir) // 读取目录下所有图片文件名 this.imgNameList = FileUtil.listDirectory(this.imageDir) } // 清空全部缓存图片 clearAllImage() { FileUtil.clearDir(this.imageDir) this.imgNameList = FileUtil.listDirectory(this.imageDir) } build() { Column({ space: 15 }) { Row() { Text("相机缓存图片列表").fontSize(20).layoutWeight(1) Button("清空缓存").backgroundColor("#f56c6c").onClick(() => this.clearAllImage()) } .width("95%") List({ space: 8 }) { ForEach(this.imgNameList, (name: string) => { ListItem() { Text(name) .width("100%") .padding(12) .backgroundColor(Color.White) .borderRadius(6) } }) } .layoutWeight(1) .width("95%") } .width("100%") .height("100%") .padding(15) .backgroundColor("#f5f5f5") } }五、API23+ 文件系统适配与 IO 性能优化规范
5.1 文件句柄强制释放规范
- openSync 打开文件获取 fd 后,无论读写成功失败,必须执行 closeSync;
- 使用 try-catch 包裹文件操作,catch 分支内也要关闭句柄防止泄漏;
- 批量循环读写文件不要重复打开关闭,单次 fd 完成全部操作再释放。
5.2 沙盒路径使用规范
- 所有文件根目录统一通过页面上下文获取 filesDir/cacheDir,禁止手写绝对路径;
- 图片、长期保存数据存 filesDir,临时缓存、可清理文件存 cacheDir;
- 多层目录拼接统一使用
/分隔,工具内部统一路径拼接逻辑。
5.3 IO 性能优化规范
- 小于 10KB 小文本可使用同步读写,日志、配置文件适用;
- 超过 100KB 图片、大文档禁止主线程同步一次性读取,改用异步分片;
- 频繁追加日志不要每次打开关闭 fd,可短时缓存 fd 统一批量写入再释放;
- 遍历文件夹删除文件先收集文件名,循环逐个 unlinkSync,不可直接 rmdirSync 非空目录。
5.4 业务分层存储规范
- 持久配置文本:Preferences(优先),简单日志小文件:fs 文本;
- 结构化多条数据:RDB 数据库;
- 图片、离线本地资源:fs filesDir 目录存储;
- 临时缓存、可自动清理文件:cacheDir 目录。
六、API23 升级高频兼容问题与解决方案
问题 1:文件写入后无法删除,提示文件被占用 解决:每次 openSync 操作结束一定调用 closeSync 释放 fd 句柄。
问题 2:路径不存在,创建文件直接报错崩溃 解决:操作前先用 existsSync 判断,不存在先创建文件夹。
问题 3:读取文件中文乱码 解决:读取 ArrayBuffer 后通过 Uint8Array 转字符串,不直接截取二进制。
问题 4:rmdirSync 删除文件夹报错 解决:该接口仅支持空目录,先遍历删除内部所有文件再执行删除。
问题 5:升级 API23 后硬编码路径找不到文件 解决:全部替换为 context.filesDir 动态获取沙盒根路径。
问题 6:多次写入同一文件内容错乱、覆盖丢失 解决:追加模式使用 OpenMode.APPEND,覆盖写入用 WRITE_ONLY,区分两种写入模式。
七、总结
fs 文件系统是 OpenHarmony 沙盒本地文件唯一标准操作模块,API Version23 强化文件句柄生命周期管理,强制读写后关闭 fd,统一沙盒路径获取方式,解决旧版文件占用、内存泄漏、路径失效、大文件读取卡顿等核心兼容问题。项目封装通用文件工具类,覆盖文件夹创建、文本读写追加、文件删除、目录遍历、缓存清空全套能力,适配本地日志、相机图片缓存两大高频业务场景。
