一、背景说明

应用本地文件的读写是几乎所有应用的基础能力:配置缓存、离线数据、运行日志都落在沙箱目录中。HarmonyOS 26.0.1 Beta1(11 月 10 日面向开发者发布)的 Core File Kit 又新增了卷管理能力,文件服务持续增强,而应用侧最通用的文件操作入口始终是 @kit.CoreFileKit 的 fileIo(即 ohos.file.fs)。本文以"本地日志文件管理"为例,完整实现"沙箱目录定位 → 同步/异步读写 → 文件列表遍历 → 日志轮转清理"全流程,代码可在 API 9 及以上(含 26.0.x)工程中运行。

二、技术原理

应用沙箱是安全隔离机制:每个应用在内部存储空间映射出一个专属的"应用沙箱目录",应用默认只能看到自己的文件与少量系统文件,杜绝路径穿越攻击。常用目录:

  1. filesDir:context.filesDir,应用持久化文件目录,随应用卸载删除;
  2. cacheDir:context.cacheDir,缓存目录,系统存储紧张时可能被回收,适合临时数据;
  3. 加密分级 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 可直接查看。

五、技术要点
  1. 同步与异步选择:readSync/writeSync 简单可靠但阻塞 UI 线程,官方明确建议耗时读写用异步接口,避免应用卡顿甚至崩溃;本例 append 用同步(内容小),readLatest 演示异步;
  2. OpenMode 组合:CREATE 只在不存在时新建,已存在文件追加内容必须带 OpenMode.APPEND,否则写入会从头覆盖;读写过的文件偏移指针停在上次操作结束位置,显式传 offset 更安全;
  3. fd 必须配对关闭:open 得到的 File 对象在 finally 中 closeSync,日志轮转场景高频开关文件,泄漏 fd 会耗尽句柄;
  4. 流接口适合大文件:createStream 打开的 Stream 支持分段 write/read 且不依赖 offset 参数,拷贝大文件时按 4KB 缓冲循环读写;流不支持并发读写,异步流接口不要与同步混用;
  5. 加密等级选择:默认 EL2(首次解锁后可用);锁屏期间仍需记录的日志应放入 EL1 目录(context.area 切换),否则锁屏下写入会失败;
  6. 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 生命周期管理。掌握同步/异步/流三档接口的取舍,即可支撑配置缓存、日志、离线数据等绝大多数本地文件场景。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐