
本文详解在 kivy + python for android(p4a)环境中,如何合规、可靠地申请并使用外部存储读写权限,解决 permissionerror: [errno 13] permission denied 问题,并避免因权限调用时机不当导致的崩溃或静默失败。
本文详解在 kivy + python for android(p4a)环境中,如何合规、可靠地申请并使用外部存储读写权限,解决 permissionerror: [errno 13] permission denied 问题,并避免因权限调用时机不当导致的崩溃或静默失败。
在 Android 6.0(API 23)及以上版本中,READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 属于危险权限(dangerous permissions),仅在 buildozer.spec 中声明是远远不够的——必须在运行时显式请求用户授权,且必须在任何访问外部存储的操作之前完成授权。你遇到的 PermissionError 正是因为代码在未获授权前就尝试创建目录(folder_path.mkdir(…)),而此时系统已拒绝访问 /storage/emulated/0/。
✅ 正确的权限申请时机:启动早期 + 异步等待
关键原则:权限请求必须发生在 UI 初始化之前,且需确保授权完成后再执行文件操作。你原代码中将 request_permissions(…) 放在 build() 方法内存在两大缺陷:
- build() 在 Kivy 主循环启动后才执行,此时部分底层 Android API 可能尚未就绪;
- request_permissions() 是异步操作,立即返回,但实际授权结果需通过回调处理;而你的 mkdir 和 open() 紧随其后同步执行,必然失败。
正确的做法是:在 MyApp().run() 调用前,先发起权限请求,并利用回调驱动后续逻辑。
✅ 推荐实现方案(含错误处理与兼容性)
import kivy from kivy.app import App from kivy.uix.label import Label from kivy.utils import platform from pathlib import Path import os # 仅在 Android 平台导入 Android 模块(避免桌面端报错) if platform == "android": from android.permissions import request_permissions, check_permission, Permission from android.storage import primary_external_storage_path from jnius import autoclass PythonActivity = autoclass('org.kivy.android.PythonActivity') class MyApp(App): def build(self): # 权限检查:若已授权,直接构建 UI;否则显示提示或延迟加载 if platform == "android": if not self._check_storage_permissions(): # 权限未授予,返回占位 Label 提示用户等待 return Label(text="正在请求存储权限...n请稍候", halign="center", valign="middle") # ✅ 此处确保权限已就绪,可安全访问存储 text = f"1{App.get_running_app().user_data_dir}n" self.label = Label(text=text, halign="left", valign="top") self.label.text_size = (self.label.width * 0.9, None) if platform == "android": folder_path = Path(primary_external_storage_path(), "my_kivy_app_folder") try: folder_path.mkdir(parents=True, exist_ok=True) file_path = folder_path / "test.txt" with open(file_path, "w", encoding="utf-8") as f: f.write("Hello from Kivy on Android! ?n") self.label.text += f"✅ 已写入: {file_path}n" except PermissionError as e: self.label.text += f"❌ 权限被拒绝: {e}n" except Exception as e: self.label.text += f"⚠️ 写入异常: {e}n" else: file_path = Path.home() / "test_kivy.txt" with open(file_path, "w", encoding="utf-8") as f: f.write("Hello from desktop!n") self.label.text += f"✅ 已写入: {file_path}n" return self.label def _check_storage_permissions(self): """检查是否已拥有读写外部存储权限""" return ( check_permission(Permission.READ_EXTERNAL_STORAGE) and check_permission(Permission.WRITE_EXTERNAL_STORAGE) ) # === 主入口:权限请求前置 === if __name__ == '__main__': if platform == "android": # ? 关键:在 App.run() 前请求权限 def on_permissions_granted(permissions, grant_results): # grant_results 是布尔列表,对应 permissions 的授权状态 if all(grant_results): print("✅ 所有存储权限已授予") MyApp().run() else: print("❌ 用户拒绝了部分权限,应用将无法访问外部存储") # 发起请求(注意:此调用会触发系统弹窗) request_permissions( [Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE], on_permissions_granted ) else: MyApp().run()
⚠️ 重要注意事项
- Android 10+(API 29)及更高版本限制:从 Android 10 开始,WRITE_EXTERNAL_STORAGE 对应用私有目录外的路径不再提供宽泛写入能力(Scoped Storage)。如需访问公共目录(如 DCIM/, Downloads/),应改用 MediaStore API 或申请 MANAGE_EXTERNAL_STORAGE(需 Google Play 审核批准)。对于应用专属文件,推荐优先使用 app_storage_path() 或 get_files_dir()(通过 android.storage 或 jnius 获取),无需额外权限。
- buildozer.spec 配置需匹配:确保 android.permissions 包含对应权限(注意大小写和命名空间):
android.permissions = READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE - 不要忽略回调:永远不要假设 request_permissions() 同步成功;务必通过回调函数判断授权结果。
- 测试建议:在真机上测试,模拟器可能权限行为不一致;首次安装后手动进入「设置 → 应用 → 权限」验证状态。
✅ 总结
解决 Kivy Android 存储权限问题的核心在于:声明 + 运行时请求 + 回调驱动 + 时机前置。将权限请求移至 App.run() 之前,并严格依据授权结果决定后续流程,即可彻底规避 PermissionError。同时,关注 Android 版本演进对存储模型的影响,合理选择存储路径策略,是构建健壮跨平台移动应用的关键一步。