Android Kivy 应用中正确申请存储权限的完整实践指南

8次阅读

Android Kivy 应用中正确申请存储权限的完整实践指南

本文详解在 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 版本演进对存储模型的影响,合理选择存储路径策略,是构建健壮跨平台移动应用的关键一步。

text=ZqhQzanResources