
本文详解如何在 kivy + python for android 项目中正确申请并使用外部存储读写权限,解决 permissionerror: [errno 13] permission denied 问题,涵盖声明、运行时请求、路径适配及最佳实践。
本文详解如何在 kivy + python for android 项目中正确申请并使用外部存储读写权限,解决 permissionerror: [errno 13] permission denied 问题,涵盖声明、运行时请求、路径适配及最佳实践。
在 Android 6.0(API 23)及以上系统中,即使已在 buildozer.spec 中声明了 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 权限,仍需在应用运行时显式请求用户授权——这是导致你的 Kivy 应用启动即崩溃的根本原因。错误日志 PermissionError: [Errno 13] Permission denied 明确表明:代码尝试在未获授权时直接访问 /storage/emulated/0/(即公共外部存储),而 Android 拒绝了该操作。
✅ 正确做法:运行时权限请求必须早于任何文件 I/O 操作
关键原则是:权限请求必须在 App 实例创建和任何文件系统调用之前完成,且需处理异步授权结果。你原代码中将 request_permissions() 放在 build() 方法内(即 UI 构建阶段)存在两大缺陷:
- build() 可能被多次调用,但权限只需请求一次;
- 更严重的是:mkdir() 和 open() 在 request_permissions() 返回前就已执行(该函数是异步的),此时权限尚未授予,必然失败。
以下是修正后的标准实现:
import kivy from kivy.app import App from kivy.uix.label import Label from kivy.utils import platform from pathlib import Path # 仅在 Android 平台导入并请求权限(避免桌面端报错) if platform == "android": from android.permissions import request_permissions, Permission, check_permission from android.storage import primary_external_storage_path class MyApp(App): def build(self): text = f"User data dir: {App.get_running_app().user_data_dir}n" self.label = Label(text=text) if platform == "android": # ✅ 步骤1:检查是否已获权限(可选,提升体验) if not (check_permission(Permission.READ_EXTERNAL_STORAGE) and check_permission(Permission.WRITE_EXTERNAL_STORAGE)): # ✅ 步骤2:在 build() 开头请求权限(但注意:仍需异步处理) # ⚠️ 注意:request_permissions 是异步的,后续操作必须等待回调! self.request_storage_permissions() else: self.create_file_safely() else: self.create_file_safely() return self.label def request_storage_permissions(self): """安全地请求存储权限,并在授权后执行文件操作""" from android.permissions import request_permissions, Permission from android import mActivity def callback(permissions, results): # permissions 和 results 是并行列表,results[i] 表示 permissions[i] 是否被授予 if all(results): self.create_file_safely() else: self.label.text += "❌ 权限被拒绝,无法创建文件。n" # 异步请求,callback 将在用户响应后触发 request_permissions( [Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE], callback ) def create_file_safely(self): """在确认获得权限后执行文件操作""" try: folder_path = Path(primary_external_storage_path(), "my_kivy_app") 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 Exception as e: self.label.text += f"❌ 文件操作失败:{e}n" # ✅ 最佳实践:主入口处提前请求权限(推荐方案) if __name__ == '__main__': if platform == "android": from android.permissions import request_permissions, Permission # 在 App 启动前同步请求(阻塞至用户响应完毕) # ⚠️ 注意:此方式会短暂显示权限弹窗再启动 UI,用户体验更可控 request_permissions([Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE]) MyApp().run()
? 配置补充说明(buildozer.spec)
确保你的 buildozer.spec 包含以下配置(注意格式与权限名称大小写):
# 必须声明(用于生成 AndroidManifest.xml) android.permissions = READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE, INTERNET # 推荐添加(适配 Android 11+ Scoped Storage 的过渡兼容) android.api = 33 android.ndk = 25c p4a.branch = develop # 使用较新版本以更好支持新 API
? 重要提示(Android 11+):从 Android 11(API 30)起,WRITE_EXTERNAL_STORAGE 对公共目录(如 /sdcard/Download)的访问被大幅限制。若需长期存储,建议改用 app_storage_path()(应用私有目录,无需权限)或使用 MediaStore API。本方案适用于兼容旧版及调试场景。
? 总结要点
- 权限声明 ≠ 权限授予:buildozer.spec 中的配置仅生成清单,运行时请求不可或缺;
- 请求时机决定成败:权限请求必须在任何 open()、mkdir() 等 I/O 操作之前完成,且需通过回调确认结果;
- 避免主线程阻塞:request_permissions() 是异步的,切勿假设调用后立即生效;
- 路径选择有讲究:优先使用 app_storage_path()(无需权限,数据随 App 卸载清除);若必须用共享存储,请确保目标路径符合 Scoped Storage 规则;
- 始终异常捕获:Android 权限模型动态性强,务必用 try/except 包裹文件操作。
遵循以上方案,你的 Kivy Android 应用即可稳定实现文件读写功能。