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 就拥有了完整的“记忆能力”,无论是用户偏好、业务数据还是文件资产,都能妥善安放。

感谢大家的陪伴!江湖路远,我们代码里见!

📚 充电时间

如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧!初高级证书还没获取的,点这里:

🔗 HarmonyOS第一课:官方认证培训

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信中提出,非常感谢您的支持。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐