Picker 选的文件下次为什么打不开?FileShare 持久化授权实践【鸿蒙心迹】

大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
前言
应用中经常会有这样的场景:用户通过系统的文件选择器(Picker)选中了一份 PDF 文档或者一张图片,应用将这个文件的 URI 保存到了本地数据库(比如 Preferences 或关系型数据库中)。
当时读取一切正常,但当应用完全退出并重新启动后,再次通过保存的 URI 去尝试打开这个文件时,程序却抛出了权限异常(如 13900001 Permission denied)。
很多开发者在这个阶段会感到困惑:明明是用户主动选择的文件,应用也成功读取过,为什么重启后系统就不认账了?
这实际上是 HarmonyOS 出于安全和隐私保护所设计的临时授权机制。为了让应用能够长期访问用户通过 Picker 选择的文件,我们需要完成一套标准化的“持久化授权”流程。
一、Picker URI 的权限机制与生命周期
在 HarmonyOS 的安全模型中,秉持着“最小权限原则”。
当用户通过 DocumentViewPicker 或 PhotoViewPicker 等系统组件选择文件时,系统并不会直接把这个文件的永久读写权限赋予应用。相反,系统只是基于本次 Picker 的调用,向应用的当前生命周期颁发了一个临时 URI 权限。
这种临时权限具有以下特征:
- 立即可用:Picker 返回 URI 后,当前进程可以直接通过
@kit.CoreFileKit中的fs模块对文件进行操作。 - 随进程消亡:一旦应用进程被完全销毁(例如用户在多任务列表划掉应用,或系统资源回收),这个临时权限也会随之失效。
如果不做额外处理,应用重启后,即便手里握着正确的 URI 字符串,由于权限凭证已经过期,系统底层(底层基于沙箱和访问控制列表)会直接拒绝应用的访问请求。
二、先把官方规则弄清楚
为了解决“下次还能打开”的问题,HarmonyOS 官方在 @kit.CoreFileKit(早期版本为 @ohos.file.fileShare)中提供了文件共享与权限持久化能力。
核心涉及三个关键 API:
fileShare.persistPermission:将临时 URI 权限转化为持久化权限。fileShare.checkPersistentPermission:检查当前是否已经拥有该 URI 的持久化权限。fileShare.revokePermission:主动撤销不再需要的持久化权限(每个应用能够持久化的 URI 数量是有系统上限的,官方限制一般为 500 个,超出会导致新授权失败)。
前置条件:
- 不需要额外申请任何动态权限(如
ohos.permission.READ_MEDIA等),因为 Picker 选择行为本身就是用户授予权限的动作。 - 需要在 API 11/12 及以上的 Stage 模型中使用。
三、搭一个最小实践
为了验证这一机制,我们设计一个包含以下三个步骤的最小实践场景:
- 用户选择文件:拉起 Picker 获取临时 URI。
- 主动固化权限:拿到 URI 后,立即调用
persistPermission将其持久化。 - 应用重启后读取:模拟应用重启,通过
checkPersistentPermission检查并读取。
核心代码实现
按照官方接口定义,可以按下面方式组织这段逻辑:
import { picker, fileShare, fs } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class FileAccessHelper {
/**
* 1. 拉起 Picker 选择文件并持久化授权
*/
public static async pickAndPersistFile(): Promise<string | undefined> {
try {
let documentPicker = new picker.DocumentViewPicker();
let options = new picker.DocumentSelectOptions();
// 限制只能选一个文件
options.maxSelectNumber = 1;
let uris: Array<string> = await documentPicker.select(options);
if (!uris || uris.length === 0) {
return undefined;
}
let targetUri = uris[0];
// 2. 构造权限策略,请求读权限持久化
let policyInfo: fileShare.PolicyInfo = {
uri: targetUri,
operationMode: fileShare.OperationMode.READ_MODE
};
// 执行持久化
await fileShare.persistPermission([policyInfo]);
console.info(`[FileAccess] 权限持久化成功: ${targetUri}`);
// 注意:这里在实际业务中应该将 targetUri 存入数据库或 Preferences
return targetUri;
} catch (err) {
let error = err as BusinessError;
console.error(`[FileAccess] 操作失败, Code: ${error.code}, Message: ${error.message}`);
return undefined;
}
}
/**
* 3. 应用重启以后读取文件
* @param savedUri 从数据库中读取出的 URI
*/
public static async readFileAfterRestart(savedUri: string): Promise<void> {
try {
// 检查是否具备持久化读权限
let policyInfo: fileShare.PolicyInfo = {
uri: savedUri,
operationMode: fileShare.OperationMode.READ_MODE
};
let checkResult: Array<boolean> = await fileShare.checkPersistentPermission([policyInfo]);
if (!checkResult || !checkResult[0]) {
console.warn(`[FileAccess] 权限已丢失或未授权,需要提示用户重新选择`);
return;
}
// 具备权限,尝试打开文件
let file = fs.openSync(savedUri, fs.OpenMode.READ_ONLY);
console.info(`[FileAccess] 文件打开成功,文件描述符: ${file.fd}`);
// 读取文件逻辑...
fs.closeSync(file);
} catch (err) {
let error = err as BusinessError;
// 重点:处理文件已被用户在外部删除的情况兜底
if (error.code === 13900002) {
console.error(`[FileAccess] 文件不存在(可能已被删除),请清理本地无效 URI 记录`);
} else {
console.error(`[FileAccess] 读取异常, Code: ${error.code}`);
}
}
}
}
四、几个关键点拆开看
1. 写权限怎么处理?
上面的示例使用的是 fileShare.OperationMode.READ_MODE。如果你的应用不仅仅需要读取这个文件,还需要在将来修改并覆盖它(例如文档编辑器应用),你应该在持久化时同时申请写权限:
let policyInfo: fileShare.PolicyInfo = {
uri: targetUri,
// 使用按位或运算符组合读写权限
operationMode: fileShare.OperationMode.READ_MODE | fileShare.OperationMode.WRITE_MODE
};
在执行 persistPermission 时请求读写模式,之后再次检查或打开文件(fs.openSync 传入 fs.OpenMode.READ_WRITE)就不会遇到阻碍。
2. 授权失败的隐藏边界
这往往是线上业务容易踩坑的地方。persistPermission 并不是无限容量的。
HarmonyOS 明确限制了单个应用可以持久化的 URI 数量上限。如果用户的应用长期重度使用,选了成百上千个文件并且全都执行了持久化,当达到系统阈值时,persistPermission 会抛出异常(通常为 13900001 或特定超出配额错误)。
解决思路:必须在业务层维护一张 URI 表。当不再需要访问某个历史文件时,及时调用 fileShare.revokePermission([policyInfo]) 进行释放,保持“有借有还”。
3. 文件已经删除时怎么兜底?
这里比较容易出现一个理解偏差:持久化权限≠文件本身持久存在。
有了持久化权限,系统允许你访问那个路径。但如果用户在这期间打开了系统的“文件管理”App,把那个文件给删除了,应用依然会报错。
从接口设计来看,此时 checkPersistentPermission 可能依然返回 true(因为权限规则还在),但随后调用 fs.openSync 时会抛出 13900002 (No such file or directory) 错误。
如果在实际项目中没有对此做 Catch 处理,应用会直接崩溃。因此,13900002 错误码的捕获是 URI 访问链路中必不可少的兜底逻辑。发生此错误时,应当主动清理本地数据库中的这个 URI 记录,并给用户友好的提示。
五、实际项目中怎么排查这类问题
如果你的项目里依然遇到了“无法打开 Picker 文件”的情况,建议按照以下清单排查:
-
先看生命周期:报错是发生在使用 Picker 的当次运行,还是应用重启之后?如果是重启后,大概率是没有调用
persistPermission。 -
再看授权模式:当前业务是想要写入文件,但持久化时是否只申请了
READ_MODE? -
确认错误码:
13900001-> 权限问题(未持久化、或者权限配额已满)。13900002-> 文件不存在问题(物理文件已被删除或移动,权限可能没问题)。13900020-> 无效的 URI 格式(检查数据库读取 URI 字符串时是否被截断或拼接错误)。
-
检查是否主动调用了 revoke:是否有其他业务逻辑意外调用了
revokePermission导致权限被收回。
六、最后整理一下
- Picker 获取的仅仅是临时 URI 权限,跨进程生命周期必须使用
fileShare.persistPermission主动固化。 - 权限持久化是有数量上限的,不用的 URI 要及时通过
revokePermission回收。 - 即使权限校验通过,也要做好防范物理文件被外部删除引发的
13900002异常兜底。 - 按需申请操作模式,明确区分
READ_MODE和WRITE_MODE,严格遵守最小权限原则。
如果你正在做涉及文件管理的业务,可以重点观察一下自己的应用在异常断电或者被系统清理后台后,曾经编辑过的文件是否还能正常加载。这类问题往往比单纯的首次读取更考验客户端的健壮性。
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐


所有评论(0)