《HarmonyOS技术精讲-Core File Kit》第5篇:沙箱路径获取与权限控制

HarmonyOS技术精讲-Core File Kit:第5篇 沙箱路径获取与权限控制
开篇:一个常见的困惑
很多刚接触 HarmonyOS NEXT 的开发者,在第一次调 context.filesDir 或 context.cacheDir 时,会习惯性地以为这些路径跟 Android 一样可以随意访问。
真正跑起来才发现,fail: errCode: 13900001, permission denied 这种错误来得毫无征兆。
这个功能本身并不复杂,但在实际项目里,路径选错、权限判断疏忽,轻则功能异常,重则数据丢失。这篇就来系统讲清楚沙箱路径的获取方式、各目录用途,以及权限模型的边界。
它解决的核心问题
应用沙箱 是 HarmonyOS Core File Kit 提供的一套文件隔离机制。每个应用只能访问自己的沙箱目录,不能随意读写其他应用的数据。这在多进程、多应用环境下,能有效防止恶意篡改和数据泄露。
场景说明:
- 适合:存储用户头像、缓存网络图片、存放 SQLite 数据库文件、保存临时下载包
- 不适合:跨应用共享大文件(需要
FilePicker或Share Kit)、访问相册(需photoAccessHelper)
与 Android 的差异:
| 对比项 | HarmonyOS | Android |
|---|---|---|
| 数据根目录 | /data/storage/el2/base/haps/entry/ |
/data/data/包名/ |
| 沙箱隔离粒度 | 按应用 + HAP 双重隔离 | 仅按应用包名 |
| 文件访问限制 | 默认私有,跨目录需 AbilityInfo.FLAG_OPENLINK |
MODE_WORLD_READABLE 已废弃 |
在 HarmonyOS NEXT 上,沙箱路径的获取统一通过 UIAbilityContext 或 ExtensionContext 提供,路径值在应用安装时由系统动态分配,不能硬编码。
环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(真机或模拟器)
核心实现:获取所有沙箱路径并打印
第一步:在 Ability 中获取上下文
这段代码用于在 EntryAbility 中获取 UIAbilityContext,然后将其传递给页面组件。
// EntryAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'SandboxDemo', 'Ability onCreate');
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'SandboxDemo', 'Ability onNewWant');
}
}
注意:
UIAbilityContext在onCreate之后才可用- 不要在
aboutToAppear之前调用this.context,否则返回undefined
第二步:获取沙箱路径并写入文件
这一段代码演示如何获取 filesDir、cacheDir、databaseDir,并在各自目录下创建文件,观察路径差异。
// pages/Index.ets
import { common } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct Index {
@State sandboxPaths: string[] = [];
@State logOutput: string = '';
build() {
Column() {
Text('沙箱路径获取与权限测试')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
List() {
ForEach(this.sandboxPaths, (item: string, index: number) => {
ListItem() {
Text(item)
.fontSize(12)
.fontColor(Color.Gray)
.width('100%')
}
})
}
.height(150)
Text(this.logOutput)
.fontSize(14)
.fontColor(Color.Blue)
.margin({ top: 20 })
Button('获取沙箱路径')
.onClick(() => this.getAndPrintSandboxPaths())
.margin(10)
.width('80%')
.backgroundColor(Color.Orange)
Button('尝试跨目录访问')
.onClick(() => this.testCrossDirectoryPermission())
.margin(10)
.width('80%')
.backgroundColor(Color.Red)
}
.width('100%')
.height('100%')
.padding(16)
}
getAndPrintSandboxPaths(): void {
const context = getContext(this) as common.UIAbilityContext;
if (!context) {
this.logOutput = 'Error: context is undefined';
return;
}
const filesDir: string = context.filesDir;
const cacheDir: string = context.cacheDir;
const databaseDir: string = context.databaseDir;
const tempDir: string = context.tempDir;
const distributedDir: string = context.distributedFilesDir;
const paths = [
`filesDir: ${filesDir}`,
`cacheDir: ${cacheDir}`,
`databaseDir: ${databaseDir}`,
`tempDir: ${tempDir}`,
`distributedFilesDir: ${distributedDir}`
];
this.sandboxPaths = paths;
this.logOutput = '路径已获取,见上方列表';
// 验证各目录可写
try {
// 在 filesDir 下创建文件
const fileUri = fileIo.getUriFromPath(`${filesDir}/demo.txt`);
const file = fileIo.openSync(fileUri, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
fileIo.writeSync(file.fd, 'Hello from filesDir');
fileIo.closeSync(file);
hilog.info(0x0000, 'SandboxDemo', 'Write to filesDir success');
} catch (e) {
this.logOutput = `写 filesDir 失败: ${(e as BusinessError).message}`;
}
}
testCrossDirectoryPermission(): void {
const context = getContext(this) as common.UIAbilityContext;
// 尝试直接访问 cacheDir 下的文件列表
const cacheDir: string = context.cacheDir;
try {
const files = fileIo.listFileSync(cacheDir);
this.logOutput = `cacheDir 列表: ${files.length} 个文件`;
} catch (e) {
this.logOutput = `访问 cacheDir 失败: ${(e as BusinessError).message}`;
}
// 尝试访问其他应用沙箱(模拟)
const otherAppPath = '/data/storage/el2/base/haps/com.other.app/entry/files';
try {
fileIo.accessSync(otherAppPath);
this.logOutput = `居然能访问其他应用? ${otherAppPath}`;
} catch (e) {
this.logOutput = `访问其他应用沙箱被拒绝: ${(e as BusinessError).message}`;
}
}
}
说明:
getContext(this)在组件内使用,返回ComponentContext,需要强转为UIAbilityContext才能获取全部沙箱路径fileIo.getUriFromPath将物理路径转为 URI,这在后续文件操作中是推荐的写法- 跨目录测试时,除非应用有
FLAG_OPENLINK,否则会抛出13900001错误

权限控制:沙箱内的文件私有性
在 HarmonyOS Core File Kit 中,沙箱内文件默认私有,即只有创建该文件的应用可以读写。这与 Linux 文件权限不同,它由 ArkTS 运行时和系统服务共同维护。
如何设置跨目录访问权限?
如果需要让其他应用或同一个应用的不同 HAP 访问文件,必须在 module.json5 中声明 data 目录的 distributed 属性,或者通过 ability 的 exported + FLAG_OPENLINK 机制。
// module.json5 片段
{
"module": {
"name": "entry",
"type": "entry",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true,
"skills": [
{
"actions": ["ohos.want.action.openlink"],
"uris": [
{
"scheme": "sandbox",
"path": "/data/storage/el2/base/haps/entry/files"
}
]
}
]
}
]
}
}
然后通过 startAbility 传递 URI 来触发访问。这种方式适用于应用间免登录共享小文件。
踩坑记录
坑1:context.filesDir 路径会随 HAP 版本变化
现象:应用升级后,以前存储在 filesDir 下的文件丢失了。
原因:filesDir 的实际路径包含 entry 这个 HAP 名称。如果开发者在 module.json5 中修改了 module.name 或新增了 HAP 实例,路径会改变,导致旧文件找不到。
解法:不要在代码中拼接 filesDir 的完整路径,而是始终通过 context.filesDir 动态获取。如果必须持久化引用,请保存相对于 filesDir 的相对路径。
坑2:databaseDir 的权限不是默认可写
现象:试图直接在 databaseDir 下创建 .db 文件,却报权限错误。
原因:databaseDir 是为关系型数据库预留的目录,系统建议通过 relationalStore.getRdbStore 来操作。直接使用 fileIo 创建文件,虽然路径正确,但写行为受 DataShareHelper 的权限约束。
解法:始终使用 relationalStore 或 preferences 操作数据库文件。不要手动操作 databaseDir 下的文件。
// 推荐方式
import { relationalStore } from '@kit.ArkData';
const store = await relationalStore.getRdbStore(context, {
name: 'mydb.db',
securityLevel: relationalStore.SecurityLevel.S1
});
坑3:AsyncCallback 与 Promise 混用导致沙箱操作误判
现象:fileIo.access 的回调还未执行,后续代码就已经读到 undefined。
原因:文件操作 API 同时提供了 AsyncCallback 和 Promise 两种版本,如果在同一个流程中混用,可能导致竞态条件。
解法:统一使用基于 Promise 的异步版本,结合 async/await 保证执行顺序。
async function checkAndWrite(context: common.UIAbilityContext): Promise<void> {
const uri = fileIo.getUriFromPath(`${context.filesDir}/config.json`);
try {
await fileIo.access(uri);
// 文件存在,后续操作
} catch (e) {
// 文件不存在,创建
const file = fileIo.openSync(uri, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
fileIo.closeSync(file);
}
}
最佳实践
-
使用
fileIo.getUriFromPath而不是直接拼接字符串。getUriFromPath会自动处理 URI 编码和路径规范问题,避免因路径中含特殊字符导致的permission denied。 -
不要在
build()中频繁调用沙箱路径获取。
因为getContext(this)在组件重建后会返回新的对象引用,如果在build()中频繁调用,会触发@State更新循环。 -
大数据文件不要放
cacheDir。cacheDir在系统存储不足时可能被自动清理。建议把用户主动产生的文件(如头像、下载的文档)放在filesDir,把临时缓存图片、日志放在cacheDir。 -
跨 HAP 访问必须显式声明权限。
如果应用有entry和feature两个 HAP,需要共享文件时,在module.json5中设置"data" -> "distributed"为true,或者通过want传递 URI。
FAQ
Q:真机调试正常,但 Release 包文件写入失败?
A:检查 module.json5 中是否声明了 ohos.permission.WRITE_STORAGE 或 ohos.permission.READ_STORAGE。沙箱访问原则上不需要这些权限,但如果当前环境不是 NEXT(如 HarmonyOS 3.x),后台可能仍需要申请。
Q:页面返回后,用 fileIo 读取上一次写入的文件,fileIo.stat 返回 undefined?
A:确认页面的 @State 是否在 aboutToDisappear 时被清除。文件本身没丢,只是状态引用了旧路径。建议在 aboutToAppear 中重新获取 context.filesDir。
Q:为什么 cacheDir 和 tempDir 大小不能超过 256MB?
A:这是系统为了保护存储空间设置的上限。如果应用需要大量临时文件,建议放在 filesDir 下自行管理,或者使用 FilePicker 让用户选择外部目录。
完整项目入口
// AppScope/entry/src/main/ets/entryability/EntryAbility.ets
// 见上方代码
示例代码地址:项目地址
总结
沙箱路径的获取是 HarmonyOS Core File Kit 最基础的能力。只要掌握 context.*Dir 的正确获取方式,理解各目录的生命周期和权限边界,大部分文件管理问题都能覆盖。
重点记住:
filesDir:持久化业务数据cacheDir:可清理的临时文件databaseDir:数据库专用,别用fileIo碰它- 跨目录访问统一走 URI +
startAbility流程
官方文档对这个行为描述得比较概括,建议结合实际运行效果一起验证。不同设备上(手机 vs 平板 vs 折叠屏)的路径前缀可能会小幅变化,用真机测试最稳妥。
更多推荐


所有评论(0)