鸿蒙操作系统使用 Core File Kit 实现沙箱文件读写与日志管理实战
一、背景说明
应用本地文件的读写是几乎所有应用的基础能力:配置缓存、离线数据、运行日志都落在沙箱目录中。HarmonyOS 26.0.1 Beta1(11 月 10 日面向开发者发布)的 Core File Kit 又新增了卷管理能力,文件服务持续增强,而应用侧最通用的文件操作入口始终是 @kit.CoreFileKit 的 fileIo(即 ohos.file.fs)。本文以"本地日志文件管理"为例,完整实现"沙箱目录定位 → 同步/异步读写 → 文件列表遍历 → 日志轮转清理"全流程,代码可在 API 9 及以上(含 26.0.x)工程中运行。
二、技术原理
应用沙箱是安全隔离机制:每个应用在内部存储空间映射出一个专属的"应用沙箱目录",应用默认只能看到自己的文件与少量系统文件,杜绝路径穿越攻击。常用目录:
- filesDir:
context.filesDir,应用持久化文件目录,随应用卸载删除; - cacheDir:
context.cacheDir,缓存目录,系统存储紧张时可能被回收,适合临时数据; - 加密分级 el1~el5:沙箱二级目录 storage/ 下的加密等级,EL1 开机即可访问,EL2 需首次解锁后可访问(推荐默认),EL4 在锁屏时也保护文件。写入敏感数据时可用 context.area 切换加密等级。
fileIo 接口分三档:同步接口(openSync/readSync/writeSync,简单场景)、异步接口(read/write 返回 Promise,耗时操作推荐)、流接口(createStream,适合大文件分段处理)。每个接口配对使用 open/close,通过 fd 操作文件。
数据流为:获取 context → 拼接沙箱路径 → openSync(OpenMode.CREATE|READ_WRITE)→ writeSync 写入 / readSync 读取 → closeSync 释放 fd。
三、实现步骤
无需申请任何 ohos.permission 权限(沙箱内文件访问不受权限管控)。工程要求 Stage 模型、API 9 及以上。
新建 src/main/ets/file/LogManager.ets(完整代码,日志写入 + 轮转清理封装):
import { fileIo, ReadOptions, Filter } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { buffer } from '@kit.ArkTS';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'LogManager';
const MAX_LOG_SIZE: number = 512 * 1024; // 单文件上限 512KB
const MAX_LOG_COUNT: number = 5; // 最多保留 5 份
export class LogManager {
private logDir: string;
constructor(context: common.UIAbilityContext) {
this.logDir = context.filesDir + '/logs';
if (!fileIo.accessSync(this.logDir)) {
fileIo.mkdirSync(this.logDir);
}
}
// 追加一条日志;超过单文件上限自动轮转
append(line: string): void {
const ts = new Date().toISOString();
const filePath = this.logDir + '/app_' + this.today() + '.log';
try {
const stat = fileIo.statSync(filePath);
if (stat.size > MAX_LOG_SIZE) {
this.rotate(filePath);
}
} catch (err) {
// 文件不存在,无需轮转
}
const file = fileIo.openSync(filePath,
fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.APPEND);
try {
fileIo.writeSync(file.fd, `[${ts}] ${line}\n`);
} finally {
fileIo.closeSync(file);
}
}
// 轮转:按时间重命名旧文件并清理超量日志
private rotate(current: string): void {
const rotated = current.replace('.log',
`_${Date.now()}.log`);
fileIo.renameSync(current, rotated);
this.cleanOldLogs();
}
// 遍历日志目录,删除超出保留数量的最旧文件
private cleanOldLogs(): void {
try {
const names: string[] = fileIo.listFileSync(this.logDir);
if (names.length <= MAX_LOG_COUNT) { return; }
const files = names.map(n => ({
name: n,
size: fileIo.statSync(this.logDir + '/' + n).size
})).sort((a, b) => a.name.localeCompare(b.name));
for (let i = 0; i < files.length - MAX_LOG_COUNT; i++) {
fileIo.unlinkSync(this.logDir + '/' + files[i].name);
}
} catch (err) {
const e = err as Error;
hilog.error(TAG, 'cleanOldLogs failed: %{public}s', e.message);
}
}
// 读取最新日志内容(异步接口示例)
async readLatest(maxBytes: number = 4096): Promise<string> {
const filePath = this.logDir + '/app_' + this.today() + '.log';
const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
try {
const stat = fileIo.statSync(filePath);
const readLen = Math.min(stat.size, maxBytes);
const ab = new ArrayBuffer(readLen);
const opt: ReadOptions = { offset: stat.size - readLen, length: readLen };
const len = await fileIo.read(file.fd, ab, opt);
return buffer.from(ab, 0, len).toString('utf-8');
} finally {
fileIo.closeSync(file);
}
}
private today(): string {
const d = new Date();
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
}
}
四、应用场景示例
在 Index.ets 中使用日志管理器:
import { LogManager } from '../file/LogManager';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct Index {
@State log: string = '就绪';
build() {
Column({ space: 12 }) {
Text(this.log).fontSize(14)
Button('写入日志').onClick(() => {
const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
const lm = new LogManager(ctx);
lm.append('用户点击了写入按钮');
this.log = '已写入一条日志';
})
Button('查看最新日志').onClick(async () => {
const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
const lm = new LogManager(ctx);
this.log = await lm.readLatest();
})
}
.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
写入的文件位于沙箱 filesDir/logs/app_20261115.log,DevEco Studio 的 Device File Browser 可直接查看。
五、技术要点
- 同步与异步选择:readSync/writeSync 简单可靠但阻塞 UI 线程,官方明确建议耗时读写用异步接口,避免应用卡顿甚至崩溃;本例 append 用同步(内容小),readLatest 演示异步;
- OpenMode 组合:CREATE 只在不存在时新建,已存在文件追加内容必须带
OpenMode.APPEND,否则写入会从头覆盖;读写过的文件偏移指针停在上次操作结束位置,显式传 offset 更安全; - fd 必须配对关闭:open 得到的 File 对象在 finally 中 closeSync,日志轮转场景高频开关文件,泄漏 fd 会耗尽句柄;
- 流接口适合大文件:createStream 打开的 Stream 支持分段 write/read 且不依赖 offset 参数,拷贝大文件时按 4KB 缓冲循环读写;流不支持并发读写,异步流接口不要与同步混用;
- 加密等级选择:默认 EL2(首次解锁后可用);锁屏期间仍需记录的日志应放入 EL1 目录(
context.area切换),否则锁屏下写入会失败; - listFileSync 的返回:仅返回文件名(不含路径),stat/unlink 前需自行拼接目录前缀。
六、扩展建议
若日志需要导出分享或让用户选择保存位置,用文件 picker 拿用户目录 fd 写入:
import { picker } from '@kit.CoreFileKit';
const doc = new picker.DocumentSaveOptions();
doc.newFile = 'export.log';
const saver = new picker.DocumentViewPicker();
saver.save(doc).then(uris => {
const file = fileIo.openSync(uris[0], fileIo.OpenMode.READ_WRITE);
fileIo.writeSync(file.fd, logContent);
fileIo.closeSync(file);
});
26.0.1 Beta1 新增的卷管理模块还可查询外置存储磁盘与卷设备信息,多存储设备场景(如 2in1)可据此选择落盘位置。
结语
本文用约 120 行 ArkTS 实现了沙箱文件从目录创建、追加写入、异步读取到日志轮转清理的完整闭环,核心是 filesDir 定位、OpenMode 语义与 fd 生命周期管理。掌握同步/异步/流三档接口的取舍,即可支撑配置缓存、日志、离线数据等绝大多数本地文件场景。
更多推荐




所有评论(0)