HarmonyOS 5 数据持久化:沙箱文件 (FileIO)
本文介绍了HarmonyOS中的数据持久化方案之一——沙箱文件(FileIO)操作。文章首先解释了沙箱的概念,即系统为每个App分配的独立存储隔离区域,并介绍了常用的三个目录:filesDir(核心存储区)、cacheDir(缓存区)和tempDir(临时区)。 核心部分展示了如何使用FileIO API进行文件操作,强调必须遵循"打开->读写->关闭"的流程,并提
HarmonyOS 数据持久化:沙箱文件 (FileIO)
大家好,我是不想掉发的鸿蒙开发工程师 城中的雾。咱们的数据持久化系列的文章是最后一片。
- 存配置?找首选项。
- 存状态?找PersistentStorage。
- 存结构化大列表?找关系型数据库。
但有些东西,它们既不是键值对,也不是表格数据。比如:
- 用户拍的高清自拍 (几 MB)。
- 从服务器下载的 PDF 报告。
- App 运行时的 Crash 日志。
对于这些“块状”数据,数据库存起来太累(Blob 字段性能一般)。我们需要回归最原始、最通用的存储方式——文件系统 (File System),本期(系列终章),我们来探索 App的沙箱文件。
1. 核心概念:什么是“沙箱”?
在鸿蒙系统里,App 是不能随意访问手机存储的(比如不能直接去读别的 App 的文件)。系统给每个 App 分配了一个独立的隔离区域,叫沙箱 (Sandbox)。
App 在这个盒子里是“上帝”,想怎么读写都行;但出了盒子,就是“禁区”。
常用目录三剑客
要操作文件,先得知道文件存哪。通过 Context 可以获取以下常用路径:
| 属性 | 说明 | 适用场景 | 是否随云备份 |
|---|---|---|---|
| filesDir | 内部存储核心区 | 用户文档、重要数据 | 是 |
| cacheDir | 缓存区 | 网络图片缓存、临时生成的文件 | 否 (系统内存不足时可能被清理) |
| tempDir | 临时区 | 极其临时的中转文件 | 否 |
老鸟经验:
如果你的文件很重要(如用户日记),存 filesDir。
如果只是为了节省流量下载的图片,存 cacheDir。
2. 核心 API:FileIO (fs)
鸿蒙的文件操作模块是 @kit.CoreFileKit(旧版为 @ohos.file.fs)。
我们主要打交道的是 fs 对象。操作流程通常是:打开(Open) -> 读/写(Read/Write) -> 关闭(Close)。
注意:文件操作非常消耗资源,一定要记得 Close!否则会造成文件句柄泄露,导致 App 崩溃。
3. 实战:封装 LogManager (日志记录器)
光说不练假把式。我们来封装一个 日志管理器,实现将 App 的运行日志写入到 filesDir/app.log 文件中,并支持读取显示。

第一步:编写工具类 (FileUtil.ets)
我们使用 fs.openSync 等同步方法简化逻辑(文件流操作建议用异步,但简单读写同步更直观)。
// utils/FileUtil.ets
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class FileUtil {
private static instance: FileUtil;
// 上下文,用于获取路径
private context: common.Context | null = null;
private constructor() {}
public static get(): FileUtil {
if (!FileUtil.instance) {
FileUtil.instance = new FileUtil();
}
return FileUtil.instance;
}
// 初始化上下文
init(context: common.Context) {
this.context = context;
}
/**
* 追加写入日志
* @param fileName 文件名 (如 app.log)
* @param content 内容
*/
writeLog(fileName: string, content: string) {
if (!this.context) return;
// 1. 拼接完整路径: /data/storage/.../files/app.log
let filePath = this.context.filesDir + '/' + fileName;
let file: fs.File | null = null;
try {
// 2. 打开文件
// fs.OpenMode.READ_WRITE: 读写模式
// fs.OpenMode.CREATE: 不存在则创建
// fs.OpenMode.APPEND: 追加到末尾 (这是写日志的关键)
file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.APPEND);
// 3. 写入内容 (加个时间戳和换行)
let logStr = `[${new Date().toLocaleTimeString()}] ${content}\n`;
fs.writeSync(file.fd, logStr);
console.info('FileUtil: 日志写入成功');
} catch (error) {
console.error('FileUtil: 写入失败', JSON.stringify(error));
} finally {
// 4. 必须关闭文件!
if (file) {
fs.closeSync(file);
}
}
}
/**
* 读取所有日志
*/
readLog(fileName: string): string {
if (!this.context) return '';
let filePath = this.context.filesDir + '/' + fileName;
let content = '';
// 检查文件是否存在
if (!fs.accessSync(filePath)) {
return '暂无日志文件';
}
try {
// 读取为文本
content = fs.readTextSync(filePath);
} catch (error) {
console.error('FileUtil: 读取失败', JSON.stringify(error));
}
return content;
}
/**
* 清空日志
*/
clearLog(fileName: string) {
if (!this.context) return;
let filePath = this.context.filesDir + '/' + fileName;
try {
fs.unlinkSync(filePath); // 删除文件
} catch (e) {}
}
}
第二步:UI 演练 (LogPage.ets)
我们做一个简易的“开发者控制台”,可以写入日志,也可以回显日志。
import { FileUtil } from '../utils/FileUtil';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct LogPage {
@State logContent: string = '';
@State inputMsg: string = '';
private readonly LOG_FILE = 'app.log';
aboutToAppear() {
// 1. 初始化工具类 (传入 Context)
// 依然使用防御性初始化
let context = getContext(this) as common.Context;
FileUtil.get().init(context);
// 2. 加载已有日志
this.refreshLog();
}
refreshLog() {
this.logContent = FileUtil.get().readLog(this.LOG_FILE);
}
build() {
Column({ space: 15 }) {
Text('App 日志查看器')
.fontSize(20)
.fontWeight(FontWeight.Bold)
// 输入区
Row({ space: 10 }) {
TextInput({ placeholder: '输入日志内容...', text: this.inputMsg })
.layoutWeight(1)
.onChange(val => this.inputMsg = val)
Button('写入')
.onClick(() => {
if (this.inputMsg) {
FileUtil.get().writeLog(this.LOG_FILE, this.inputMsg);
this.inputMsg = ''; // 清空输入
this.refreshLog(); // 刷新显示
}
})
}
.width('100%')
// 操作栏
Row({ space: 10 }) {
Button('刷新读取').onClick(() => this.refreshLog())
Button('清空日志')
.backgroundColor('#FF4040')
.onClick(() => {
FileUtil.get().clearLog(this.LOG_FILE);
this.refreshLog();
})
}
// 显示区
Text('日志内容:')
.width('100%')
.textAlign(TextAlign.Start)
.fontColor('#999')
Scroll() {
Text(this.logContent)
.fontSize(14)
.fontFamily('Monospace') // 等宽字体看起来像控制台
.backgroundColor('#F0F0F0')
.width('100%')
.padding(10)
}
.layoutWeight(1)
.align(Alignment.TopStart)
.width('100%')
.border({ width: 1, color: '#DDD' })
}
.padding(20)
.height('100%')
}
}
4. 进阶:读取 RawFile (只读资源)
除了读写自己创建的文件,有时候我们需要读取打包在 HAP 包里的初始资源(比如 rawfile/init_data.json)。
这不能用 fs 直接打开,需要用 resourceManager。
// 读取 RawFile 的工具方法
// 需要导入 import { util } from '@kit.ArkTS';
async function readRawFile(context: common.Context, fileName: string): Promise<string> {
try {
let uint8Array = await context.resourceManager.getRawFileContent(fileName);
// 将二进制转为字符串
let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });
return textDecoder.decodeWithStream(uint8Array);
} catch (error) {
console.error('RawFile读取失败', error);
return '';
}
}
5. 全系列总结
至此,我们的**《HarmonyOS 数据持久化:拒绝失忆》**系列就圆满结束了!
我们一起回顾一下这四种武器的使用场景:
| 存储方式 | 核心类 | 适用场景 | 对应生活中的例子 |
|---|---|---|---|
| 首选项 | Preferences |
简单的开关、Token、字体大小 | 便利贴 / 记事本 |
| 持久化状态 | PersistentStorage |
记住 UI 状态、历史记录 (自动) | 肌肉记忆 |
| 关系型数据库 | RelationalStore |
复杂的结构化数据、需要搜索排序 | Excel 表格 |
| 文件系统 | FileIO (fs) |
图片、文档、日志、大文件 | 档案柜 |
掌握了这四招,你的 App 就拥有了完整的“记忆能力”,无论是用户偏好、业务数据还是文件资产,都能妥善安放。
感谢大家的陪伴!江湖路远,我们代码里见!
📚 充电时间
如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧!初高级证书还没获取的,点这里:
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信中提出,非常感谢您的支持。
更多推荐



所有评论(0)