te-quanbzhang/skills-pool

android-gallery-bugfix-skill

Diagnose and fix Android gallery, photo-picker, MediaStore, and image-loading issues — including permissions (Android 9–14+), Scoped Storage, URI access, EXIF orientation, Glide/Coil/Picasso errors, OOM, and list/preview/save/delete failures.

查看源码
仓库原始内容

按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。

Android Gallery Bugfix Skill

适用范围

用于 Android 项目中 Gallery / 相册 / 图片选择 / 图片浏览 / 图片保存相关问题的排查、修复和验证,包括但不限于:

  • 相册打不开
  • 图片列表为空
  • 图片加载慢
  • 缩略图显示异常
  • 图片方向错误
  • 图片保存失败
  • 图片删除失败
  • 图片扫描不到
  • 图片选择失败
  • Android 10+ 分区存储问题
  • MediaStore 查询异常
  • Glide / Coil / Picasso 加载异常
  • OOM / Bitmap 内存问题
  • 大图预览卡顿
  • EXIF 信息异常
  • 视频和图片混合列表异常
  • 权限问题

Bugfix 总体原则

相册类问题通常和以下因素相关:

  1. 存储权限
  2. Android 版本差异
  3. Scoped Storage / 分区存储
  4. MediaStore 查询
  5. URI 权限
  6. 图片解码
  7. 缩略图缓存
  8. EXIF 方向
  9. 文件路径和 content uri 混用
  10. 大图内存优化
  11. 异步加载和 RecyclerView 复用

处理时必须区分:

  • 文件不存在
  • 没有权限
  • MediaStore 未扫描
  • URI 不可访问
  • 解码失败
  • 缩略图加载失败
  • UI 绑定错误

常见问题分类

1. 相册列表为空

重点检查:

  • 是否有 READ 权限
  • Android 13+ 是否申请 READMEDIAIMAGES / READMEDIAVIDEO
  • Android 10+ 是否适配 Scoped Storage
  • MediaStore 查询条件是否过窄
  • selection / sortOrder 是否错误
  • 查询的是 Images 还是 Files
  • 是否只查外部存储
  • 是否被 MIME type 过滤掉
  • 文件是否已被 MediaScanner 扫描

权限差异:

text
Android 12 及以下:
- READ_EXTERNAL_STORAGE

Android 13+:
- READ_MEDIA_IMAGES
- READ_MEDIA_VIDEO
- READ_MEDIA_AUDIO

Android 14+:
- READ_MEDIA_VISUAL_USER_SELECTED

2. 图片加载失败

重点检查:

  • URI 是否有效
  • 文件是否存在
  • 是否有 URI 权限
  • 是否在后台线程解码
  • 图片是否损坏
  • 是否为 HEIC / WEBP / RAW 等特殊格式
  • Glide / Coil / Picasso 是否配置正确
  • RecyclerView 复用导致错图

常见日志关键字:

text
FileNotFoundException
SecurityException
BitmapFactory
ImageDecoder
GlideException
Coil
Picasso
openInputStream
decodeStream

3. 图片方向错误

重点检查:

  • 是否读取 EXIF orientation
  • 是否对 content uri 支持 EXIF 读取
  • 是否只处理 file path,未处理 input stream
  • 前置摄像头图片是否镜像
  • 压缩后是否丢失 EXIF

常用类:

kotlin
ExifInterface
Matrix
BitmapFactory
ImageDecoder

修复建议:

  • 从 InputStream 读取 EXIF
  • 保存压缩图时保留必要 EXIF
  • 显示层优先让图片加载库处理 orientation
  • 避免重复旋转

4. 图片保存失败

重点检查:

  • Android 版本
  • 是否直接写公共目录
  • 是否使用 MediaStore
  • 是否设置 RELATIVE_PATH
  • 是否设置 IS_PENDING
  • OutputStream 是否 close
  • 文件名是否非法
  • 存储空间是否不足
  • 是否需要触发媒体扫描

Android 10+ 推荐:

kotlin
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
ContentValues
RELATIVE_PATH
DISPLAY_NAME
MIME_TYPE
IS_PENDING
contentResolver.openOutputStream(uri)

保存后:

kotlin
IS_PENDING = 0

5. 删除失败

重点检查:

  • Android 10+ 是否需要用户授权删除
  • 是否使用 ContentResolver.delete
  • 是否捕获 RecoverableSecurityException
  • 是否处理系统确认弹窗
  • 是否有写入权限
  • 是否删除后刷新列表

Android 11+ 常见方式:

kotlin
MediaStore.createDeleteRequest(...)

6. OOM / 加载卡顿

重点检查:

  • 是否直接加载原图
  • 是否根据 View 尺寸采样
  • 是否在主线程解码
  • RecyclerView 是否复用
  • 是否开启过大的缓存
  • 是否一次性查询或加载全部图片
  • 是否大图预览没有 tile / subsampling

修复建议:

  • 使用 Glide / Coil 缩放加载
  • RecyclerView 分页
  • 使用 Paging
  • 大图使用 subsampling
  • 避免 Bitmap 常驻内存
  • onViewRecycled 中取消加载
  • 使用缩略图优先

推荐排查流程

Step 1:确认复现条件

记录:

  • 设备型号
  • Android 版本
  • 是否 Android 10+
  • 是否 Android 13+
  • 是否首次安装
  • 权限是否授权
  • 是图片、视频还是混合媒体
  • 来源:相机拍摄 / 下载 / 微信 / 浏览器 / 系统截图
  • 文件格式:JPG / PNG / HEIC / WEBP / GIF / RAW
  • 是列表页、选择页、预览页还是保存页
  • 复现概率

Step 2:收集日志

推荐命令:

bash
adb logcat -v time > gallery_bug.log
adb shell dumpsys package your.package.name > package_info.txt
adb shell dumpsys media.provider > media_provider.txt

检查文件:

bash
adb shell ls -l /sdcard/DCIM/
adb shell ls -l /sdcard/Pictures/
adb shell ls -l /sdcard/Download/

检查权限:

bash
adb shell appops get your.package.name
adb shell dumpsys package your.package.name | grep permission

Step 3:定位层级

按照以下顺序判断:

  1. App 是否有读取权限
  2. MediaStore 是否能查询到数据
  3. 查询条件是否正确
  4. URI 是否可访问
  5. 图片是否可解码
  6. 缩略图是否正确加载
  7. RecyclerView 绑定是否正确
  8. 图片预览是否处理方向和大图
  9. 保存 / 删除是否符合 Android 版本要求

MediaStore 查询检查项

重点检查:

kotlin
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
MediaStore.Files.getContentUri("external")
ContentResolver.query(...)

必须确认:

  • projection 不为空或字段存在
  • selection 正确
  • sortOrder 正确
  • cursor 正确 close
  • _ID 正确拼接 content uri
  • DATA 字段在 Android 10+ 不应强依赖
  • 不要直接依赖绝对路径

推荐使用:

kotlin
ContentUris.withAppendedId(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    id
)

Android 版本适配重点

Android 9 及以下

常见权限:

xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Android 10

重点:

  • Scoped Storage
  • MediaStore 写入
  • 不建议直接使用 file path 访问公共目录
  • 可临时使用 requestLegacyExternalStorage,但不推荐长期依赖

Android 11 / 12

重点:

  • 更严格的公共目录访问
  • 删除 / 修改他人媒体需要用户授权
  • MANAGEEXTERNALSTORAGE 不应滥用

Android 13+

重点权限:

xml
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

Android 14+

重点:

xml
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

需要处理用户只授权部分照片的情况。


图片加载库检查

Glide

重点检查:

kotlin
Glide.with(context)
    .load(uri)
    .thumbnail(...)
    .override(width, height)
    .into(imageView)

注意:

  • Fragment / View 生命周期
  • RecyclerView 复用
  • 清理请求
  • 加载 content uri
  • placeholder / error 图

Coil

重点检查:

kotlin
imageView.load(uri) {
    crossfade(true)
    size(width, height)
}

注意:

  • lifecycle
  • memory cache
  • disk cache
  • SVG / GIF / HEIC 支持

常见修复策略

  • 修复 Android 13+ 媒体权限
  • Android 14 处理部分照片授权
  • 不再依赖 MediaStore DATA 字段
  • 使用 content uri 替代 file path
  • 查询 Cursor 后及时 close
  • 图片保存改用 MediaStore
  • 删除媒体时使用系统授权流程
  • 图片解码增加采样
  • RecyclerView 中取消旧请求
  • 修复 EXIF 方向处理
  • 增加空列表、权限拒绝、加载失败 UI
  • 大图预览使用压缩或分块加载
  • 异步加载避免阻塞主线程

代码审查重点

修复 gallery bug 时,必须检查:

  • 是否兼容 Android 10+
  • 是否兼容 Android 13+
  • 是否兼容 Android 14 部分照片权限
  • 是否处理权限拒绝
  • 是否处理 URI 失效
  • 是否关闭 Cursor / InputStream / OutputStream
  • 是否避免主线程解码
  • 是否避免 OOM
  • 是否处理 RecyclerView 错图
  • 是否处理图片方向
  • 是否处理保存失败
  • 是否处理删除授权
  • 是否处理空数据状态

验证清单

至少验证:

  • 首次安装进入相册
  • 权限拒绝
  • 权限允许
  • Android 13 图片权限
  • Android 13 视频权限
  • Android 14 部分照片授权
  • 图片列表加载
  • 视频列表加载
  • 混合媒体列表
  • 大图预览
  • HEIC 图片
  • GIF 图片
  • 图片方向
  • 拍照后刷新相册
  • 保存图片
  • 删除图片
  • 存储空间不足
  • 快速滑动列表
  • 前后台切换
  • 横竖屏切换

输出 Bugfix 结论模板

text
Root Cause:
- 问题原因:

Fix:
- 修改内容:

Risk:
- 影响范围:

Verification:
- 已验证场景:
- 未覆盖场景:

Logs:
- 关键日志:

Heuristics

  • For "empty gallery" on Android 13+, always check the per-media-type granular permissions (READ_MEDIA_IMAGES / READ_MEDIA_VIDEO) before blaming MediaStore.
  • For "photo not visible after save" — suspect IS_PENDING = 1 still set, or MediaStore entry created but OutputStream not flushed/closed, or the app bypassed the media scanner.
  • For "wrong orientation" — check EXIF read path: ExifInterface works on InputStream/Uri path but some libraries silently drop EXIF when compressing/resizing.
  • For "blurry / wrong image in RecyclerView" — classic missing .into() after cancel, or Glide/Coil request not cancelled in onViewRecycled.
  • For "image load fails on Android 13+ but works on older" — suspect READ_EXTERNAL_STORAGE still used instead of READ_MEDIA_IMAGES, or content:// Uri permissions not persisted across process restart.
  • For "image visible in file manager but not in query results" — check MediaScanner has run, and that projection/sortOrder/selection aren't excluding it by MIME type or IS_PENDING flag.
  • For "OOM in gallery list" — always suspect: no sampling on decode, loading full-res for a thumbnail slot, no Paging, no per-item Glide/Coil override to view size.
  • For "Android 14 partial selection shows nothing" — the app did not handle READ_MEDIA_VISUAL_USER_SELECTED and didn't check Uri availability per item.

Anti-Patterns

  • Catching SecurityException around a storage call without first checking permission state — hides the real bug.
  • Using MediaStore.Images.ImageColumns.DATA (absolute path) on Android 10+ — the column is deprecated/unreliable under Scoped Storage.
  • Opening FileInputStream(uri.path) on a content:// Uri — must use contentResolver.openInputStream(uri).
  • Loading full-resolution bitmaps into RecyclerView thumbnails instead of sampling to view dimensions.
  • Holding Bitmap references in static/cache without bounds or eviction.
  • Calling ContentResolver.delete then refreshing the list without handling RecoverableSecurityException on Android 10+ / MediaStore.createDeleteRequest on Android 11+.
  • Setting IS_PENDING = 1 on MediaStore insert and forgetting to flip it to 0 after write — leaves entries invisible to other apps/scanners.
  • Depending on requestLegacyExternalStorage as a fix rather than a migration window on Android 10.
  • Logging user photo Uris verbatim — leaks private picture paths; redact to id + MIME type.
  • Treating gallery issues as "UI-only" and skipping permission + MediaStore + storage-layer checks.

CodeGraph Integration

CodeGraph helps ground the gallery diagnosis in the actual codebase structure. Run it before proposing a fix.

When to run CodeGraph:

  • After the triage narrows the symptom to a specific gallery sub-area (list load / save / delete / orientation / OOM / picker)
  • When tracing the data flow from MediaStore query → URI handling → image loading → UI binding
  • When the fix may change how multiple screens consume a shared image repository

How:

bash
codegraph explore "<GalleryRepository or MediaStore query entry>"
codegraph explore "<image-loading call site, e.g., Glide entry>"
codegraph explore "<save / delete code path>"

What to look for from CodeGraph results (gallery-focused):

  • Callers of the MediaStore query method — do all callers pass the same projection/selection, or are some paths using deprecated columns or wrong MIME filters?
  • Uri consumer chain — who receives the Uri from the query result, and does anyone convert it back to a file path (a Scoped Storage anti-pattern)?
  • Image-loading entry points — which Glide/Coil/Picasso call sites load the suspect Uri, and which lifecycle scopes them (Fragment vs Application)?
  • Write/save call sites — which callers set IS_PENDING, open the OutputStream, and flip IS_PENDING back to 0. A missing flip is a common root cause.
  • Delete paths — which callers use ContentResolver.delete vs MediaStore.createDeleteRequest, and whether RecoverableSecurityException is handled.
  • RecyclerView adapter binding — the on-bind handler that calls into the image loader; paired with onViewRecycled for cancellation.
  • Downstream impact — screens that share the same repository; a fix that changes Uri access semantics may break the preview or share flow.

Scope note: CodeGraph does not reliably index XML layouts, res/values, AndroidManifest.xml, or Gradle build scripts. For gallery issues rooted in manifest permissions, requestLegacyExternalStorage, or network_security_config, supplement with rg/find. Also scan settings.gradle for multi-module dependencies on the image loading library.

Fallback: If codegraph explore returns no meaningful results, proceed without it — CodeGraph is an enhancement, not a blocker. Fall back to rg/grep for MediaStore / image-loader / save-path call-site search.

Guardrails

  • Run the full triage classification before jumping to a code fix — gallery bugs are disproportionately driven by Android version + permission state.
  • Always check Android 10 / 13 / 14 permission differences for media access before editing code.
  • Always verify the fix with real devices on at least two API levels (e.g., API 29 and API 34+) — gallery behavior is highly version-sensitive.
  • Always confirm the MediaStore query projection is version-appropriate — DATA is deprecated under Scoped Storage.
  • Always confirm IS_PENDING toggling on MediaStore insert/update is correct.
  • Always close Cursor, InputStream, OutputStream — resource leaks show up as OOM / stale scans hours later.
  • Always use contentResolver.openInputStream(uri) on content:// Uris, never FileInputStream(uri.path).
  • Never log photo Uris or absolute paths verbatim — redact to id + MIME type to avoid PII leakage.
  • Do not mask storage errors with a broad try/catch — find and fix the missing permission / missing scan / deprecated column first.
  • If codegraph explore returns no meaningful results, proceed without it — CodeGraph is an enhancement, not a blocker. Fall back to rg/grep.
来自同一仓库

更多 Skills

全部 Skills