在这里插入图片描述

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

开篇:一个常见的困惑

很多刚接触 HarmonyOS NEXT 的开发者,在第一次调 context.filesDircontext.cacheDir 时,会习惯性地以为这些路径跟 Android 一样可以随意访问。

真正跑起来才发现,fail: errCode: 13900001, permission denied 这种错误来得毫无征兆。

这个功能本身并不复杂,但在实际项目里,路径选错、权限判断疏忽,轻则功能异常,重则数据丢失。这篇就来系统讲清楚沙箱路径的获取方式、各目录用途,以及权限模型的边界。

它解决的核心问题

应用沙箱HarmonyOS Core File Kit 提供的一套文件隔离机制。每个应用只能访问自己的沙箱目录,不能随意读写其他应用的数据。这在多进程、多应用环境下,能有效防止恶意篡改和数据泄露。

场景说明

  • 适合:存储用户头像、缓存网络图片、存放 SQLite 数据库文件、保存临时下载包
  • 不适合:跨应用共享大文件(需要 FilePickerShare Kit)、访问相册(需 photoAccessHelper

与 Android 的差异

对比项 HarmonyOS Android
数据根目录 /data/storage/el2/base/haps/entry/ /data/data/包名/
沙箱隔离粒度 按应用 + HAP 双重隔离 仅按应用包名
文件访问限制 默认私有,跨目录需 AbilityInfo.FLAG_OPENLINK MODE_WORLD_READABLE 已废弃

在 HarmonyOS NEXT 上,沙箱路径的获取统一通过 UIAbilityContextExtensionContext 提供,路径值在应用安装时由系统动态分配,不能硬编码。

环境说明

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');
    }
}

注意

  • UIAbilityContextonCreate 之后才可用
  • 不要在 aboutToAppear 之前调用 this.context,否则返回 undefined
第二步:获取沙箱路径并写入文件

这一段代码演示如何获取 filesDircacheDirdatabaseDir,并在各自目录下创建文件,观察路径差异。

// 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 属性,或者通过 abilityexported + 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 的权限约束。

解法:始终使用 relationalStorepreferences 操作数据库文件。不要手动操作 databaseDir 下的文件。

// 推荐方式
import { relationalStore } from '@kit.ArkData';
const store = await relationalStore.getRdbStore(context, {
    name: 'mydb.db',
    securityLevel: relationalStore.SecurityLevel.S1
});
坑3:AsyncCallbackPromise 混用导致沙箱操作误判

现象fileIo.access 的回调还未执行,后续代码就已经读到 undefined

原因:文件操作 API 同时提供了 AsyncCallbackPromise 两种版本,如果在同一个流程中混用,可能导致竞态条件。

解法:统一使用基于 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);
    }
}

最佳实践

  1. 使用 fileIo.getUriFromPath 而不是直接拼接字符串
    getUriFromPath 会自动处理 URI 编码和路径规范问题,避免因路径中含特殊字符导致的 permission denied

  2. 不要在 build() 中频繁调用沙箱路径获取
    因为 getContext(this) 在组件重建后会返回新的对象引用,如果在 build() 中频繁调用,会触发 @State 更新循环。

  3. 大数据文件不要放 cacheDir
    cacheDir 在系统存储不足时可能被自动清理。建议把用户主动产生的文件(如头像、下载的文档)放在 filesDir,把临时缓存图片、日志放在 cacheDir

  4. 跨 HAP 访问必须显式声明权限
    如果应用有 entryfeature 两个 HAP,需要共享文件时,在 module.json5 中设置 "data" -> "distributed"true,或者通过 want 传递 URI。

FAQ

Q:真机调试正常,但 Release 包文件写入失败?
A:检查 module.json5 中是否声明了 ohos.permission.WRITE_STORAGEohos.permission.READ_STORAGE。沙箱访问原则上不需要这些权限,但如果当前环境不是 NEXT(如 HarmonyOS 3.x),后台可能仍需要申请。

Q:页面返回后,用 fileIo 读取上一次写入的文件,fileIo.stat 返回 undefined
A:确认页面的 @State 是否在 aboutToDisappear 时被清除。文件本身没丢,只是状态引用了旧路径。建议在 aboutToAppear 中重新获取 context.filesDir

Q:为什么 cacheDirtempDir 大小不能超过 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 折叠屏)的路径前缀可能会小幅变化,用真机测试最稳妥。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐