一、背景说明

小艺 AI 快捷栏本月中旬起将分批推送至 HarmonyOS 7 机型,鸿蒙生态的终端规模持续扩大,而应用侧无论多智能的交互,最终都绕不开最基础的一件事——本地文件读写。本文聚焦一个具体技术点:使用 Core File Kit 的 @ohos.file.fs 模块,在应用沙箱内实现文件的创建、写入、读取、流式拷贝与目录管理的完整闭环,全部代码可直接编译运行。

二、技术原理

鸿蒙采用沙箱隔离机制,每个应用的文件都在 /data/app/el2/100/base/<包名>/ 下独立成域,应用内通过 Context 拿到沙箱路径即可读写,无需任何权限。@ohos.file.fs(导入名 fileIo)提供三种访问方式:

  • fd 方式:open() 打开得到 File 对象(含 fd 文件描述符),配合 read/write 按偏移量读写,适合随机访问与固定缓冲区操作;
  • Stream 流方式:createStream() / createReadStream() / createWriteStream() 打开流,适合大文件分块与管道式拷贝(流不支持并发读写);
  • 快捷方式:readTextSync() 一次读出全文、copyFile() 整文件复制、listFile() 列目录、stat() 查属性。

关键概念:OpenMode 用位或组合声明打开行为(READ_WRITE | CREATE | TRUNC 等);ReadOptions/WriteOptions 的 offset 是显式偏移——不传时偏移指针停在上次读写的终止位置,这是最常见的"读到一半数据错位"的原因。数据流:

context.filesDir 获取沙箱路径 → openSync(路径, OpenMode) → writeSync/readSync(或 Stream 分块)→ closeSync 释放句柄。

三、实现步骤

工程为 Stage 模型(API 12+),沙箱内读写不需要申请任何权限,module.json5 保持默认。

1. 文件管理类 FileManager.ets
import { fileIo as fs, ReadOptions } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class FileManager {
  private filesDir: string;

  constructor(context: common.UIAbilityContext) {
    this.filesDir = context.filesDir; // 应用沙箱 files 目录
  }

  // 写入文本文件(覆盖写)
  writeText(fileName: string, content: string): void {
    const filePath = `${this.filesDir}/${fileName}`;
    // 读写打开 + 不存在则创建 + 存在则清空
    const file = fs.openSync(filePath,
      fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
    try {
      fs.writeSync(file.fd, content);
    } finally {
      fs.closeSync(file); // 无论成败都必须关闭句柄
    }
  }

  // 读取文本文件(指定 offset 与长度)
  readText(fileName: string, offset: number = 0): string {
    const filePath = `${this.filesDir}/${fileName}`;
    if (!fs.accessSync(filePath)) {
      return '';
    }
    const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
    try {
      const buf = new ArrayBuffer(4096);
      const options: ReadOptions = { offset: offset, length: buf.byteLength };
      const readLen = fs.readSync(file.fd, buf, options);
      // 只解码实际读到的字节,避免缓冲区尾部脏数据
      return new util.TextDecoder().decodeToString(new Uint8Array(buf.slice(0, readLen)));
    } finally {
      fs.closeSync(file);
    }
  }

  // 追加写入
  appendText(fileName: string, content: string): void {
    const filePath = `${this.filesDir}/${fileName}`;
    const file = fs.openSync(filePath,
      fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.APPEND);
    try {
      fs.writeSync(file.fd, content);
    } finally {
      fs.closeSync(file);
    }
  }

  // 列出目录下全部文件名
  listFiles(dirName: string = ''): string[] {
    const dirPath = dirName ? `${this.filesDir}/${dirName}` : this.filesDir;
    return fs.listFileSync(dirPath);
  }

  // 查询文件大小
  fileSize(fileName: string): number {
    const stat = fs.statSync(`${this.filesDir}/${fileName}`);
    return stat.size;
  }

  // 删除文件
  deleteFile(fileName: string): void {
    fs.unlinkSync(`${this.filesDir}/${fileName}`);
  }
}
2. 流式拷贝(大文件场景)

Stream 方式按 4KB 分块读写,内存占用恒定:

import { fileIo as fs, ReadOptions } from '@kit.CoreFileKit';

async function copyWithStream(filesDir: string, srcName: string, destName: string): Promise<void> {
  let inputStream: fs.Stream | null = null;
  let outputStream: fs.Stream | null = null;
  try {
    inputStream = fs.createStreamSync(`${filesDir}/${srcName}`, 'r');
    outputStream = fs.createStreamSync(`${filesDir}/${destName}`, 'w+');
    const bufSize = 4096;
    let readSize = 0;
    const buf = new ArrayBuffer(bufSize);
    const options: ReadOptions = { offset: readSize, length: bufSize };
    let readLen = await inputStream.read(buf, options);
    while (readLen > 0) {
      readSize += readLen;
      // 不足一块时按实际长度切片写入
      const writeBuf = readLen < bufSize ? buf.slice(0, readLen) : buf;
      await outputStream.write(writeBuf);
      options.offset = readSize;
      readLen = await inputStream.read(buf, options);
    }
    console.info(`copied ${readSize} bytes`);
  } finally {
    // 流使用完必须关闭,否则句柄泄漏
    inputStream?.closeSync();
    outputStream?.closeSync();
  }
}
3. UI 调用示例 Index.ets
import { common } from '@kit.AbilityKit';
import { FileManager } from '../model/FileManager';

@Entry
@Component
struct Index {
  @State logText: string = '';
  private fileManager: FileManager =
    new FileManager(getContext(this) as common.UIAbilityContext);

  build() {
    Column({ space: 12 }) {
      Button('写入笔记').onClick(() => {
        this.fileManager.writeText('note.txt', `第一次写入 ${new Date().toLocaleString()}\n`);
        this.fileManager.appendText('note.txt', '追加的第二行\n');
        this.logText = '已写入 note.txt';
      })

      Button('读取笔记').onClick(() => {
        this.logText = this.fileManager.readText('note.txt');
      })

      Button('查看文件列表').onClick(() => {
        this.logText = this.fileManager.listFiles().join('\n');
      })

      Button('删除笔记').onClick(() => {
        this.fileManager.deleteFile('note.txt');
        this.logText = '已删除 note.txt';
      })

      Text(this.logText || '操作日志')
        .fontSize(13)
        .width('90%')
        .height(120)
        .borderRadius(8)
        .backgroundColor('#F2F3F5')
        .padding(8)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}
四、应用场景示例

缓存目录的使用——cacheDir 下的文件系统可随时回收,适合存放临时数据,与 filesDir(持久目录)区分:

// 下载临时文件写入缓存目录,验证成功后再 moveFile 移入 files 目录
const cachePath = `${context.cacheDir}/download.tmp`;
const finalPath = `${context.filesDir}/resource.zip`;
// ...写 cachePath...
if (verify(cachePath)) {
  fs.moveFileSync(cachePath, finalPath); // 同一沙箱内移动是原子重命名
}
五、技术要点
1. 同步与异步接口的选择

readSync/writeSync 在 UI 线程处理大文件会阻塞渲染,超过几十 KB 的读写一律用异步接口(read/write Promise 版),或把整个文件操作放进 TaskPool 子线程。

2. 句柄必须释放

File 与 Stream 都是系统句柄,泄漏到一定数量后 open 会直接报 EMFILE(too many open files)——写入 finally 块是底线要求。

3. 错误处理
try {
  fs.writeSync(file.fd, content);
} catch (err) {
  const e = err as BusinessError;
  if (e.code === 13900042) {
    console.error('磁盘空间不足');
  } else if (e.code === 13900002) {
    console.error('文件不存在');
  } else {
    console.error(`file error code=${e.code}, msg=${e.message}`);
  }
}
4. 使用限制与路径规则
  • 沙箱路径不能跨应用直接访问;要分享给其他应用须用 fileUri 生成 URI 并申请临时授权;
  • offset 不传时从当前偏移指针继续,需要随机访问时必须显式传 offset;
  • 文件路径不支持 .. 回溯到沙箱外,系统会直接拦截。
六、扩展建议
  1. 安全原子写入:API 26.0.1 新增 AtomicFile 能力,配置文件写入过程中断电也不会留下半截文件,替代"先写临时文件再 rename"的手工方案;
  2. 文件监听:fs.Watcher 可监听文件/目录变更,日志轮转、配置热更新场景直接订阅回调;
  3. 跨进程共享:需要给其他应用提供文件时,走 File Share 沙箱共享机制:
import { fileUri } from '@kit.CoreFileKit';
// 把沙箱路径转成对外可授权的 URI,配合 Want 的临时读写权限分享
const uri: string = fileUri.getUriFromPath(`${filesDir}/share.png`);
结语

本文用 Core File Kit 的 @ohos.file.fs 完成了鸿蒙应用沙箱内文件从写入、读取、追加、列表管理到大文件流式拷贝的完整实现,核心代码约 150 行,均可直接编译运行。掌握 fd、Stream 两种访问方式与沙箱路径规则后,应用内的日志、缓存、离线资源管理就有了稳定可靠的落地方案。

Logo

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

更多推荐