Kivy+Buildozer安卓应用加载本地存储图片显示黑屏问题
问题核心成因
- 安卓权限适配缺失:安卓6.0以上危险权限需要运行时动态申请,仅在buildozer.spec里声明不会自动生效;安卓10以上默认开启分区存储,安卓13以上
READ_EXTERNAL_STORAGE权限废弃,读取公共存储图片需要单独申请READ_MEDIA_IMAGES权限,未正确授权时App根本读不到存储内的图片文件。 - 文件路径解析错误:调用系统文件选择器返回的
selection[0]在高版本安卓上是content://开头的Uri,不是本地文件系统的绝对路径,直接传给PIL.Image.open()或者Kivy Image的source属性无法读取到真实文件流。 - 临时文件保存路径非法:你注释了拼接程序运行目录的代码,直接使用相对路径保存临时文件,安卓打包后App的源码目录是只读的,相对路径写入会直接失败,且你没有做异常捕获,感知不到写入失败的问题。
- 组件未触发重载:Kivy Image组件在移动端动态替换source时,不会自动刷新纹理资源,没有主动调用
reload()方法时,就算文件路径正确也可能显示黑屏。 - 加载器配置顺序错误:
os.environ['KIVY_IMAGE'] = 'pil,sdl2'如果放在Kivy模块导入之后设置,Pil图片加载器不会生效,也会导致加载失败。
修复步骤
1. 修改buildozer.spec配置
调整权限和兼容配置,适配全版本安卓:
# 补充安卓13需要的图片读取权限,开启安卓10旧版存储兼容 android.permissions = INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE,READ_MEDIA_IMAGES android.requestLegacyExternalStorage = True # 依赖不需要调整,确认包含pillow、android、jnius即可 requirements = kivy,python3,pillow,plyer,jnius,android,datetime source.include_exts = py,png,jpg,kv,atlas
2. 修正初始化逻辑
- 把
os.environ['KIVY_IMAGE'] = 'pil,sdl2'放在所有Kivy相关导入的最顶部 - App启动时主动申请运行时权限
- 不要使用源码相对路径读写文件,统一使用安卓提供的App私有目录
参考导入和初始化代码:
# 注意:这行必须放在所有kivy导入之前 import os os.environ['KIVY_IMAGE'] = 'pil,sdl2' from kivy.app import App from kivy.uix.floatlayout import FloatLayout from android.storage import app_storage_path from android.permissions import request_permissions, Permission from jnius import autoclass import PIL.Image as PIL_Image from kivy.clock import Clock from io import BytesIO import random, string from datetime import datetime # 启动时申请运行时权限 request_permissions([ Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE, Permission.READ_MEDIA_IMAGES ]) class MainPage(FloatLayout): def __init__(self, **kwargs): super().__init__(**kwargs) self.image_selected.source = "" self.label_selected.text = "" # 打包后随包安装的资源可正常加载,这部分逻辑不需要改 self.image_selected.source = "Raindowntothestreet.png"
3. 重写图片选中处理逻辑
区分content Uri和普通文件路径,将图片复制到App私有缓存目录后再加载,主动触发组件重载:
def selected(self, selection): if not selection: return selected_uri = selection[0] # 获取App私有缓存目录,该目录App自带读写权限,不需要额外申请 cache_dir = os.path.join(app_storage_path(), "cache") os.makedirs(cache_dir, exist_ok=True) # 生成唯一临时文件名 current_dt = datetime.now() date_str = current_dt.strftime("%Y%m%d") time_str = current_dt.strftime("%H%M%S") c1 = random.choice(string.ascii_letters) c2 = random.choice(string.ascii_letters) temp_path = os.path.join(cache_dir, f"temp{date_str}{time_str}{c1}{c2}.png") # 处理content://格式的Uri if selected_uri.startswith("content://"): Uri = autoclass("android.net.Uri") PythonActivity = autoclass("org.kivy.android.PythonActivity") activity = PythonActivity.mActivity resolver = activity.getContentResolver() input_stream = resolver.openInputStream(Uri.parse(selected_uri)) # 从输入流读取图片字节 buffer = BytesIO() read_chunk = bytearray(4096) while True: read_len = input_stream.read(read_chunk) if read_len == -1: break buffer.write(read_chunk[:read_len]) input_stream.close() buffer.seek(0) img = PIL_Image.open(buffer) else: # 普通文件路径直接读取 img = PIL_Image.open(selected_uri) # 保存图片到私有缓存目录 img.save(temp_path) # 清空原有资源后重新赋值,主动触发重载避免黑屏 self.image_selected.source = "" def refresh_img(dt): self.image_selected.source = temp_path self.image_selected.reload() Clock.schedule_once(refresh_img, 0.05) self.label_selected.text = f"Source: {selected_uri}"
4. 额外注意事项
- 所有文件IO操作建议加
try-except捕获异常并打印日志,方便定位读写失败问题 - 临时文件不用时记得定期清理,避免占用用户存储空间
- 如果需要适配安卓13以上的分区存储,不需要开启
requestLegacyExternalStorage,直接使用READ_MEDIA_IMAGES权限+MediaStore API访问即可
内容的提问来源于stack exchange,提问作者杰多舍
相关产品推荐
相关产品推荐

