[鸿蒙从零到一] HarmonyOS 文件管理与沙箱访问实战:从应用目录到用户文档

[鸿蒙从零到一] HarmonyOS 文件管理与沙箱访问实战:从应用目录到用户文档

在 HarmonyOS 应用开发中,文件操作是一项基础而高频的能力。无论是存储用户配置、缓存网络数据,还是读写用户选择的文档和媒体文件,都需要清晰理解 HarmonyOS 的文件系统结构、沙箱机制与权限模型。

本文将从应用沙箱目录入手,逐步深入到用户目录访问、文件选择器、媒体库协同,最终落地可复用的文件工具封装与错误处理模式。

---

一、HarmonyOS 文件系统与沙箱模型

1.1 应用沙箱目录结构

HarmonyOS 为每个应用分配了独立的沙箱目录,应用只能访问自己的沙箱范围,无法直接访问其他应用或系统敏感目录。

典型的沙箱目录结构如下:

`
/data/storage/el1/bundle/{bundleName}/
├── base/
│ ├── files/ # 持久化用户数据
│ ├── cache/ # 可清理缓存
│ ├── temp/ # 临时文件
│ └── preferences/ # KV 存储目录
└── database/ # 数据库文件
`

其中:

- files:存放需要持久化的用户数据,如下载文件、导出报告等
- cache:存放可被系统回收的缓存数据,如图片缓存、离线数据
- temp:存放临时文件,生命周期更短
- preferences:存放 Preferences KV 数据
- database:存放关系型数据库文件

1.2 获取沙箱目录路径

通过 getContext() 获取应用上下文,再调用对应的目录方法:

`typescript
import { common } from '@kit.AbilityKit';

const context = getContext(this) as common.UIAbilityContext;

// 持久化文件目录
const filesDir = context.filesDir;

// 缓存目录
const cacheDir = context.cacheDir;

// 临时目录
const tempDir = context.tempDir;

// 数据库目录
const databaseDir = context.databaseDir;
`

这些路径都是绝对路径字符串,可以直接拼接文件名进行读写。

---

二、沙箱内文件读写实战

2.1 文件基础操作

HarmonyOS 提供 @kit.CoreFileKit 模块进行文件读写,支持同步和异步两种模式。

同步写入文本文件:

`typescript
import { fileIo } from '@kit.CoreFileKit';

function writeTextFile(path: string, content: string): void {
const file = fileIo.openSync(path, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
fileIo.writeSync(file.fd, content);
fileIo.closeSync(file);
}

// 使用示例
const context = getContext(this) as common.UIAbilityContext;
const filePath = ${context.filesDir}/user_config.txt;
writeTextFile(filePath, 'Hello HarmonyOS');
`

异步读取文本文件:

`typescript
async function readTextFile(path: string): Promise {
const file = await fileIo.open(path, fileIo.OpenMode.READ_ONLY);
const stat = await fileIo.stat(path);
const buffer = new ArrayBuffer(stat.size);
await fileIo.read(file.fd, buffer);
await fileIo.close(file);
return String.fromCharCode(...new Uint8Array(buffer));
}

// 使用示例
const content = await readTextFile(filePath);
console.info('文件内容:', content);
`

2.2 文件状态查询与删除

`typescript
import { fileIo } from '@kit.CoreFileKit';

// 检查文件是否存在
function fileExists(path: string): boolean {
try {
fileIo.accessSync(path);
return true;
} catch {
return false;
}
}

// 获取文件信息
async function getFileInfo(path: string) {
const stat = await fileIo.stat(path);
return {
size: stat.size,
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
mtime: stat.mtime
};
}

// 删除文件
async function deleteFile(path: string): Promise {
await fileIo.unlink(path);
}
`

2.3 目录操作

`typescript
import { fileIo } from '@kit.CoreFileKit';

// 创建目录
async function createDir(path: string): Promise {
await fileIo.mkdir(path);
}

// 列出目录内容
async function listDir(path: string): Promise {
return await fileIo.listFile(path);
}

// 递归删除目录
async function removeDir(path: string): Promise {
await fileIo.rmdir(path);
}
`

---

三、访问用户目录与文件选择器

3.1 沙箱外的用户文件

应用无法直接通过路径访问用户的相册、下载、文档等公共目录。需要通过以下两种方式间接访问:

1. 文件选择器(FilePicker):让用户主动选择文件或目录,系统授予临时访问权限
2. 媒体库(PhotoAccessHelper):访问相册和媒体文件

3.2 使用文件选择器读取文件

`typescript
import { picker } from '@kit.CoreFileKit';
import { fileUri } from '@kit.CoreFileKit';
import { fileIo } from '@kit.CoreFileKit';

async function pickAndReadFile(): Promise {
const documentPicker = new picker.DocumentViewPicker();
const result = await documentPicker.select({
maxSelectNumber: 1
});

if (result.length === 0) {
throw new Error('未选择文件');
}

const uri = result[0];
const file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
const stat = await fileIo.stat(uri);
const buffer = new ArrayBuffer(stat.size);
await fileIo.read(file.fd, buffer);
await fileIo.close(file);

return String.fromCharCode(...new Uint8Array(buffer));
}
`

3.3 使用文件选择器保存文件

`typescript
import { picker } from '@kit.CoreFileKit';
import { fileIo } from '@kit.CoreFileKit';

async function saveFileWithPicker(fileName: string, content: string): Promise {
const documentPicker = new picker.DocumentViewPicker();
const result = await documentPicker.save({
newFileNames: [fileName]
});

if (!result) {
throw new Error('保存取消');
}

const file = await fileIo.open(result, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);
await fileIo.write(file.fd, content);
await fileIo.close(file);
}

// 使用示例
await saveFileWithPicker('export.json', JSON.stringify({ data: 'test' }));
`

---

四、文件工具封装与错误处理

4.1 统一错误处理

`typescript
export enum FileErrorCode {
NOT_FOUND = 'FILE_NOT_FOUND',
PERMISSION_DENIED = 'PERMISSION_DENIED',
IO_ERROR = 'IO_ERROR',
UNKNOWN = 'UNKNOWN'
}

export class FileError extends Error {
constructor(public code: FileErrorCode, message: string) {
super(message);
this.name = 'FileError';
}
}

function wrapFileError(error: Error): FileError {
const message = error.message.toLowerCase();
if (message.includes('no such file') || message.includes('not exist')) {
return new FileError(FileErrorCode.NOT_FOUND, '文件不存在');
}
if (message.includes('permission')) {
return new FileError(FileErrorCode.PERMISSION_DENIED, '权限不足');
}
return new FileError(FileErrorCode.IO_ERROR, 文件操作失败: ${error.message});
}
`

4.2 文件工具类封装

`typescript
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

export class FileUtil {
static async writeText(path: string, content: string): Promise {
try {
const file = await fileIo.open(path, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
await fileIo.write(file.fd, content);
await fileIo.close(file);
} catch (error) {
throw wrapFileError(error as Error);
}
}

static async readText(path: string): Promise {
try {
const file = await fileIo.open(path, fileIo.OpenMode.READ_ONLY);
const stat = await fileIo.stat(path);
const buffer = new ArrayBuffer(stat.size);
await fileIo.read(file.fd, buffer);
await fileIo.close(file);
return String.fromCharCode(...new Uint8Array(buffer));
} catch (error) {
throw wrapFileError(error as Error);
}
}

static async writeJson(path: string, data: T): Promise {
await this.writeText(path, JSON.stringify(data, null, 2));
}

static async readJson(path: string): Promise {
const content = await this.readText(path);
return JSON.parse(content) as T;
}

static async exists(path: string): Promise {
try {
await fileIo.access(path);
return true;
} catch {
return false;
}
}

static async delete(path: string): Promise {
try {
await fileIo.unlink(path);
} catch (error) {
throw wrapFileError(error as Error);
}
}

static async copyFile(src: string, dest: string): Promise {
try {
await fileIo.copyFile(src, dest);
} catch (error) {
throw wrapFileError(error as Error);
}
}

static getAppFilePath(context: common.UIAbilityContext, fileName: string): string {
return ${context.filesDir}/${fileName};
}

static getAppCachePath(context: common.UIAbilityContext, fileName: string): string {
return ${context.cacheDir}/${fileName};
}
}
`

4.3 使用示例

`typescript
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct FileDemo {
@State message: string = '';

async saveConfig() {
try {
const context = getContext(this) as common.UIAbilityContext;
const path = FileUtil.getAppFilePath(context, 'config.json');

await FileUtil.writeJson(path, {
theme: 'dark',
language: 'zh-CN',
timestamp: Date.now()
});

this.message = '配置保存成功';
} catch (error) {
if (error instanceof FileError) {
this.message = 保存失败: ${error.message};
}
}
}

async loadConfig() {
try {
const context = getContext(this) as common.UIAbilityContext;
const path = FileUtil.getAppFilePath(context, 'config.json');

if (await FileUtil.exists(path)) {
const config = await FileUtil.readJson<{ theme: string, language: string }>(path);
this.message = 主题: ${config.theme}, 语言: ${config.language};
} else {
this.message = '配置文件不存在';
}
} catch (error) {
if (error instanceof FileError) {
this.message = 读取失败: ${error.message};
}
}
}

build() {
Column({ space: 16 }) {
Text(this.message)
.fontSize(16)

Button('保存配置')
.onClick(() => this.saveConfig())

Button('读取配置')
.onClick(() => this.loadConfig())
}
.width('100%')
.height('100%')
.padding(16)
}
}
`

---

五、缓存管理与清理策略

5.1 缓存大小计算

`typescript
import { fileIo } from '@kit.CoreFileKit';

async function calculateDirSize(path: string): Promise {
let totalSize = 0;
const files = await fileIo.listFile(path);

for (const file of files) {
const filePath = ${path}/${file};
const stat = await fileIo.stat(filePath);

if (stat.isDirectory()) {
totalSize += await calculateDirSize(filePath);
} else {
totalSize += stat.size;
}
}

return totalSize;
}

// 格式化大小
function formatSize(bytes: number): string {
if (bytes < 1024) return ${bytes} B;
if (bytes < 1024 1024) return ${(bytes / 1024).toFixed(2)} KB;
if (bytes < 1024
1024 * 1024) return ${(bytes / 1024 / 1024).toFixed(2)} MB;
return ${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB;
}
`

5.2 缓存清理

`typescript
import { fileIo } from '@kit.CoreFileKit';

async function clearDirectory(path: string): Promise {
const files = await fileIo.listFile(path);

for (const file of files) {
const filePath = ${path}/${file};
const stat = await fileIo.stat(filePath);

if (stat.isDirectory()) {
await clearDirectory(filePath);
await fileIo.rmdir(filePath);
} else {
await fileIo.unlink(filePath);
}
}
}

// 清理应用缓存
async function clearAppCache(context: common.UIAbilityContext): Promise {
await clearDirectory(context.cacheDir);
}
`

---

六、总结与最佳实践

核心要点

1. 沙箱目录是应用的主战场:优先使用 filesDir 存储用户数据,cacheDir 存储可清理的缓存
2. 用户目录需要用户授权:通过文件选择器或媒体库间接访问,不能直接拼接路径
3. 统一错误处理:将底层 IO 错误映射为业务可理解的错误码,提升用户体验
4. 封装工具类:将高频操作(读写文本、JSON、检查存在性)封装为可复用的工具方法
5. 注意资源释放:打开文件后务必关闭,避免文件句柄泄漏

常见陷阱

- ❌ 直接拼接用户目录路径(如 /storage/media/100/local/files/Download/test.txt
- ❌ 忘记关闭文件句柄,导致资源泄漏
- ❌ 未捕获文件不存在的异常,导致应用崩溃
- ❌ 在 UI 线程执行大文件读写,导致卡顿

进阶方向

- 使用 Worker 或 TaskPool 在后台线程处理大文件读写
- 实现文件上传下载的断点续传机制
- 结合媒体库(PhotoAccessHelper)访问相册图片和视频
- 使用文件 URI 在不同应用间共享文件

---

通过本文的实战演练,你已经掌握了 HarmonyOS 文件管理的核心能力:从沙箱内的目录结构、文件读写,到沙箱外的文件选择器、缓存管理,再到可复用的工具封装与错误处理。

在实际项目中,文件操作往往与网络请求、数据持久化、媒体处理等能力协同使用。掌握了文件管理的底层逻辑,你就能更灵活地设计应用的数据存储架构,构建更健壮的文件处理能力。

posted @ 2026-08-11 11:15  天总会晴的  阅读(44)  评论(0)    收藏  举报