[鸿蒙从零到一] 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
await this.writeText(path, JSON.stringify(data, null, 2));
}
static async readJson
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 文件管理的核心能力:从沙箱内的目录结构、文件读写,到沙箱外的文件选择器、缓存管理,再到可复用的工具封装与错误处理。
在实际项目中,文件操作往往与网络请求、数据持久化、媒体处理等能力协同使用。掌握了文件管理的底层逻辑,你就能更灵活地设计应用的数据存储架构,构建更健壮的文件处理能力。

浙公网安备 33010602011771号