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










