鸿蒙操作系统使用 Core File Kit 实现沙箱文件读写与流式拷贝实战
一、背景说明
小艺 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;- 文件路径不支持
..回溯到沙箱外,系统会直接拦截。
六、扩展建议
- 安全原子写入:API 26.0.1 新增 AtomicFile 能力,配置文件写入过程中断电也不会留下半截文件,替代"先写临时文件再 rename"的手工方案;
- 文件监听:
fs.Watcher可监听文件/目录变更,日志轮转、配置热更新场景直接订阅回调; - 跨进程共享:需要给其他应用提供文件时,走 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 两种访问方式与沙箱路径规则后,应用内的日志、缓存、离线资源管理就有了稳定可靠的落地方案。
更多推荐


所有评论(0)