应用文件介绍

在设备上应用所使用及存储的数据,以文件、键值对、数据库等形式保存在一个专属目录类,该目录称为应用文件目录。应用文件目录是应用数据的核心存储区域,其内部数据以多种文件格式存放,这些文件统称为应用文件应用文件是指归属于应用的文件集合,包括应用安装文件、应用资源文件、应用缓存文件等

在应用开发中,以下数据通常需要通过文件的形式保存到App中:

  • 用户生成内容:用户撰写的笔记或评论、创作的图片或视频、分享的文档等。
  • 地图数据:地图导航类App的离线地图,供用户离线查看和导航。
  • 语言包数据:支持多语言的App的语言包数据。
  • 学习资料数据:教育类App中,一些离线学习资料。
  • 游戏资源:包括游戏中的图片、音频、视频、模型等素材资源。
  • ...

应用沙箱介绍

概述

对于每个应用,系统会在内部存储空间映射出一个专属的“应用沙箱目录”,它是“应用文件目录”与一部分系统文件所在的目录组成的集合。应用沙箱限制了应用可见的数据范围,在“应用沙箱目录”中,应用仅能看到自己的应用文件以及少量的系统文件(少量的系统文件是指应用运行必需的少量系统文件)。

因此,本应用的文件也不为其他应用可见,从而保护了应用文件的安全。

应用沙箱是一种以安全防护为目的的隔离机制,避免数据受到恶意路径穿越访问。在这种沙箱的保护机制下,应用可见的目录范围即为“应用沙箱目录"。

  • 在应用沙箱保护机制下,应用无法获知除自身应用文件目录之外的其他应用或用户的数据目录位置及存在。
  • 所有应用的目录可见范围均经过权限隔离与文件路径挂载隔离,形成了独立的路径视图,屏蔽了实际物理路径。实际物理路径与沙箱路径并非1:1的映射关系,沙箱路径总是少于系统进程视角可见的物理路径,部分调试进程视角下的物理路径在对应的应用沙箱目录下没有对应路径。

应用沙箱目录

应用文件目录结构图

三级目录代表不同文件加密类型。详细介绍如下:

EL1:设备级加密区。EL1加密类型提供了保护设备上的所有文件的基础安全能力,在设备开机后,不需要用户先完成身份认证即可访间EL1保护的文件。对于私有文件,如闹铃、壁纸等,应用可以将这些文件放到设备级加密分区 (EL1) 中,以保证在用户输入密码前就可以被访问。

EL2:用户级加密区。EL2在EL1的基础上,增加首次认证后的文件保护能力,设备开机后,用户在首次通过认证后,通过EL2能力保护的文件才能被访问。对于更敏感的文件,如个人隐私信息等,应用可以将这些文件放到更高级别的加密分区 (EL2) 中,以保证更高的安全性。

EL3:EL3在EL2的基础上,增加设备在锁屏时的文件保护能力,锁屏下可创建新的文件,但无法读取。需要在锁屏时访问锁屏前打开的文件和创建新文件,放在EL3加密区比较合适。对于应用中的记录步数、文件下载、音乐播放,放在(EL3)的加密分区比较合适,

EL4:EL4与EL3整体能力类似,在用户锁屏时无法读取文件,也不能创建文件,建议存放对于用户安全信息相关的文件。对于用户安全信息相关的文件,锁屏时不需要读写文件、也不能创建文件,放在(EL4)的加密分区更合适。

EL5:EL5主要存储用户隐私等敏感数据文件,锁屏后默认不可读写。对于用户隐私敏感数据文件,锁屏后默认不可读写,如果锁屏后需要读写文件,则锁屏前可以调用Access接口申请继续读写文件。

如果锁屏后也需要创建新文件且可读可写的应用,如无特殊需要,应将数据存放在el2加密目录下,以尽可能保证数据安全。

四级、五级目录:

通过ApplicationContext可以获取distributedfiles目录或base下的files、cache、 preferences、temp等目录的应用文件路径,应用全局信息可以存放在这些目录下。

四级目录说明如下:

五级目录说明如下:

七级目录

七级目录含义和五级目录类似,但其代表HAP级别应用文件路径

通过UIAbilityContext、AbilityStageContext、ExtensionContext可以获取HAP级别应用文件路径。HAP信息可以存放在这些目录下,存放在此目录的文件会跟随HAP的卸载而删除,不会影响App级别目录下的文件。

应用沙箱路径和物理路径

在应用沙箱路径下读写文件,经过映射转换,实际读写的是真实物理路径中的应用文件,应用沙箱路径与真实物理路径对应关系如下表所示。其中<USERID>为当前用户ID,从100开始递增。

应用文件访问和操作

操作文件常用API

获取沙箱路径

打开文件

open(path: string, mode?: number): Promise<File> 用于打开文件,使用Promise异步返回。支持使用URl打开文件。

读文件

read(fd: number, buffer: ArrayBuffer, options?: ReadOptions): Promisecnumber>从文件读取数据,使用Promise异步返回。

写文件

write(fd: number, buffer: ArrayBuffer | string, options?: WriteOptions): Promise<number>将数据写入文件,使用Promise异步返回。

文件压缩

compressFile(inFile: string, outFile: string, options: Options): Promise<void>

压缩文件,压缩的结果,使用Promise异步返回。成功时返回null,失败时返回错误码。

文件解压缩

decompressFile(inFile: string, outFile: string, options?: Options): Promise<void>

解压文件,解压的结果,使用Promise异步返回,成功时返回null,失败时返回错误码。

文件预览

openPreview(context: Context, files: Array<PreviewInfo>, index?: number): Promise<void>

通过传入多个文件预览信息以及选择展示的文件信息下标,打开预览窗口。1秒内重复调用无效。使用Promise方式异步返回结果。

文件预览信息需要包括文件标题名、文件url和文件资源类型。

应用空间统计

getCurrentBundleStats(): Promise<BundleStats>

异步获取当前应用存储空间大小(单位为Byte),以Promise方式返回。

系统空间统计

get TotalSize(path: string): Promise<number>

异步方法获取指定文件系统总字节数,以Promise形式返回结果。

接口说明

应用需要对应用文件目录下的应用文件进行查看、创建、读写、删除、移动、复制、获取属性等访问操作,下文介绍具体方法。

开发者通过基础文件操作接口(ohos.file.fs)实现应用文件访问能力,主要功能如下表所示。

表1 基础文件操作接口功能

接口名 功能 接口类型 支持同步 支持异步
access 检查文件是否存在 方法
close 关闭文件 方法
copyFile 复制文件 方法
createStream 基于文件路径打开文件流 方法
listFile 列出文件夹下所有文件名 方法
mkdir 创建目录 方法
moveFile 移动文件 方法
open 打开文件 方法
read 从文件读取数据 方法
rename 重命名文件或文件夹 方法
rmdir 删除整个目录 方法
stat 获取文件详细属性信息 方法
unlink 删除单个文件 方法
write 将数据写入文件 方法
Stream.close 关闭文件流 方法
Stream.flush 刷新文件流 方法
Stream.write 将数据写入流文件 方法
Stream.read 从流文件读取数据 方法
File.fd 获取文件描述符 属性 - -
OpenMode 设置文件打开标签 属性 - -
Filter 设置文件过滤配置项 类型 - -

注意

使用基础文件操作接口时,耗时较长的操作,例如:read、write等,建议使用异步接口,避免应用崩溃。

文件流读写和普通读写区别

在鸿蒙开发中,“文件流读写”是面向大规模数据、内存敏感场景的推荐方法,而“普通读写”则适合小文件或一次性操作。它们的主要区别在于数据加载方式和适用的文件大小。

下面的表格清晰地对比了这两种方式的核心区别:

对比维度 文件流读写 (Stream I/O) 普通读写 (常规 I/O)
数据加载方式 分块读取/写入,按需加载数据块至内存。 整体读取/写入,一次性将整个文件内容加载到内存。
内存占用 低且可控,与文件大小无关。 ,直接取决于文件大小。
主要适用场景 大文件(如视频、大型日志)、需要实时处理或网络传输的数据流。 配置文件、用户偏好设置、小型文本或JSON文件。
性能特点 适用于大文件,避免内存溢出,I/O操作更平滑。 适用于小文件,操作简单直接。
关键接口/模块 createReadStream()createWriteStream() (来自 @ohos.file.fs)。 readSync()writeSync()readText() (来自 @ohos.file.fs)。

开发示例

在对应用文件开始访问前,开发者需要获取应用文件路径。以从UIAbilityContext获取HAP级别的文件路径为例进行说明,UIAbilityContext的获取方式请参见获取UIAbility的上下文信息

下面介绍几种常用操作示例。

新建并读写一个文件

// pages/xxx.ets
import { fileIo as fs, ReadOptions } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { buffer } from '@kit.ArkTS';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

function createFile(): void {
  // 文件不存在时创建并打开文件,文件存在时打开文件
  let file = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  // 写入一段内容至文件
  let writeLen = fs.writeSync(file.fd, "Try to write str.");
  console.info("The length of str is: " + writeLen);
  // 创建一个大小为1024字节的ArrayBuffer对象,用于存储从文件中读取的数据
  let arrayBuffer = new ArrayBuffer(1024);
  // 设置读取的偏移量和长度
  let readOptions: ReadOptions = {
    offset: 0,
    length: arrayBuffer.byteLength
  };
  // 读取文件内容到ArrayBuffer对象中,并返回实际读取的字节数
  let readLen = fs.readSync(file.fd, arrayBuffer, readOptions);
  // 将ArrayBuffer对象转换为Buffer对象,并转换为字符串输出
  let buf = buffer.from(arrayBuffer, 0, readLen);
  console.info("the content of file: " + buf.toString());
  // 关闭文件
  fs.closeSync(file);
}

读取文件内容并写入到另一个文件

// pages/xxx.ets
import { fileIo as fs, ReadOptions, WriteOptions } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

function readWriteFile(): void {
  // 打开文件
  let srcFile = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  let destFile = fs.openSync(filesDir + '/destFile.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  // 读取源文件内容并写入至目的文件
  let bufSize = 4096;
  let readSize = 0;
  let buf = new ArrayBuffer(bufSize);
  let readOptions: ReadOptions = {
    offset: readSize,
    length: bufSize
  };
  let readLen = fs.readSync(srcFile.fd, buf, readOptions);
  while (readLen > 0) {
    readSize += readLen;
    let writeOptions: WriteOptions = {
      length: readLen
    };
    fs.writeSync(destFile.fd, buf, writeOptions);
    readOptions.offset = readSize;
    readLen = fs.readSync(srcFile.fd, buf, readOptions);
  }
  // 关闭文件
  fs.closeSync(srcFile);
  fs.closeSync(destFile);
}

说明

使用读写接口时,需注意可选项参数offset的设置。对于已存在且读写过的文件,文件偏移指针默认在上次读写操作的终止位置

以流的形式读写文件

// pages/xxx.ets
import { fileIo as fs, ReadOptions } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

async function readWriteFileWithStream(): Promise<void> {
  // 创建并打开输入文件流
  let inputStream = fs.createStreamSync(filesDir + '/test.txt', 'r+');
  // 创建并打开输出文件流
  let outputStream = fs.createStreamSync(filesDir + '/destFile.txt', "w+");

  let bufSize = 4096;
  let readSize = 0;
  let buf = new ArrayBuffer(bufSize);
  let readOptions: ReadOptions = {
    offset: readSize,
    length: bufSize
  };
  // 以流的形式读取源文件内容并写入到目标文件
  let readLen = await inputStream.read(buf, readOptions);
  readSize += readLen;
  while (readLen > 0) {
    const writeBuf = readLen < bufSize ? buf.slice(0, readLen) : buf;
    await outputStream.write(writeBuf);
    readOptions.offset = readSize;
    readLen = await inputStream.read(buf, readOptions);
    readSize += readLen;
  }
  // 关闭文件流
  inputStream.closeSync();
  outputStream.closeSync();
}

说明

使用流接口时,需注意流的及时关闭。同时流的异步接口应严格遵循异步接口使用规范,避免同步、异步接口混用。流接口不支持并发读写。

查看文件列表

import { fileIo as fs, Filter, ListFileOptions } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

// 查看文件列表
function getListFile(): void {
  let listFileOption: ListFileOptions = {
    recursion: false,
    listNum: 0,
    filter: {
      suffix: [".png", ".jpg", ".txt"],
      displayName: ["test*"],
      fileSizeOver: 0,
      lastModifiedAfter: new Date(0).getTime()
    }
  };
  let files = fs.listFileSync(filesDir, listFileOption);
  for (let i = 0; i < files.length; i++) {
    console.info(`The name of file: ${files[i]}`);
  }
}

使用文件流

// pages/xxx.ets
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

function copyFileWithReadable(): void {
  // 创建文件可读流
  const rs = fs.createReadStream(`${filesDir}/read.txt`);
  // 创建文件可写流
  const ws = fs.createWriteStream(`${filesDir}/write.txt`);
  // 暂停模式拷贝文件。在拷贝数据时,将原始数据暂停,然后将数据复制到另一个位置,适用于对数据完整性和一致性要求较高的场景
  rs.on('readable', () => {
    const data = rs.read();
    if (!data) {
      return;
    }
    ws.write(data);
  });
}

function copyFileWithData(): void {
  // 创建文件可读流
  const rs = fs.createReadStream(`${filesDir}/read.txt`);
  // 创建文件可写流
  const ws = fs.createWriteStream(`${filesDir}/write.txt`);
  // 流动模式拷贝文件。数据的读取和写入是同时进行的,不需要暂停原始数据的访问,适用于对数据实时性要求较高的场景
  rs.on('data', (emitData) => {
    const data = emitData?.data;
    if (!data) {
      return;
    }
    ws.write(data as Uint8Array);
  });
}

使用文件哈希流

哈希流是一种数据传输和存储技术,可以将任意长度的数据转换为固定长度的哈希值来验证数据的完整性和一致性。以下代码演示了如何使用文件哈希处理接口(ohos.file.hash)来处理文件哈希流。

// pages/xxx.ets
import { fileIo as fs } from '@kit.CoreFileKit';
import { hash } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

function hashFileWithStream() {
  const filePath = `${filesDir}/test.txt`;
  // 创建文件可读流
  const rs = fs.createReadStream(filePath);
  // 创建哈希流
  const hs = hash.createHash('sha256');
  rs.on('data', (emitData) => {
    const data = emitData?.data;
    hs.update(new Uint8Array(data?.split('').map((x: string) => x.charCodeAt(0))).buffer);
  });
  rs.on('close', async () => {
    const hashResult = hs.digest();
    const fileHash = await hash.hash(filePath, 'sha256');
    console.info(`hashResult: ${hashResult}, fileHash: ${fileHash}`);
  });
}

用户文件访问和操作

Logo

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

更多推荐