
1. 小米相册选图为什么偏偏它崩从 ACTION_PICK 到 Uri 为空的真实链路小米手机相册选图返回空指针这个问题的核心检索词就是 ACTION_PICK、ACTION_GET_CONTENT、Uri 为空、cursor.moveToFirst() 空指针异常。它到底是什么简单说你在 onActivityResult 里拿到data.getData()返回的 Uri然后拿这个 Uri 去ContentResolver.query()结果 cursor 是 null紧接着cursor.moveToFirst()就抛了 NullPointerException。能做什么把这条链路拆开用统一的 Uri 解析骨架把「图片编码」还原成可用路径或直接读流。适合谁正在做 Android 相册选图、被小米机型单独坑到、又不想为每个厂商写一套分支的开发者。我先把现象说清楚。同样的代码在别的品牌、别的 Android 版本上都正常唯独小米崩。异常栈指向cursor.moveToFirst()说明 cursor 本身是 null也就是 query 没查到任何东西。根因在于Android 4.4KITKAT之后ACTION_PICK返回的 Uri 不再保证是包含绝对路径的file://而是一个content://编码 Uri。小米在这个行为上更激进——不论版本高低ACTION_PICK拿到的往往只是一个图片编码 Uri直接按老办法查_data列就会查空。所以真正要解决的不是「小米有 bug」而是「不要假设 Uri 一定是文件路径」。正确姿势是优先用ACTION_GET_CONTENT或ACTION_OPEN_DOCUMENT拿到 Uri 后分版本、分 authority 去解析能拿到路径就拿路径拿不到就直接用ContentResolver.openInputStream()读字节流。下面这套骨架配合 TaoToken 统一 Key 通道的配置方式可以让你在真机上稳定拿到相册图片 Uri 并落地使用。2. TaoToken 前置统一 Key 通道与 settings.json / config.toml 骨架在动手改 Intent 之前先把「统一 Key 通道」这件事讲明白。TaoToken 提供的是统一的模型调用入口官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。它的价值在于你不需要为每个模型、每个工具单独维护一套 Key 和地址而是用一份统一配置贯穿开发、调试、编码助手等场景。为什么排查相册问题时也要提它因为很多同学是在用 AI 编码助手比如 Claude Code 这类辅助写 Android 代码的助手要能稳定调用模型就得有一份正确的配置骨架。我把它拆成两份最常见的配置文件settings.json和config.toml。前者多用于编辑器/插件类工具后者多用于命令行编码助手。先看settings.json的骨架重点是base_url指向统一 API 入口api_key用你在控制台生成的 Key{ provider: taotoken, base_url: https://taotoken.net/api, api_key: sk-你的统一Key, model: claude-sonnet-4-5, timeout: 60, max_tokens: 8192 }再看config.toml的骨架字段含义和上面一一对应只是 TOML 语法[provider] name taotoken base_url https://taotoken.net/api api_key sk-你的统一Key [model] default claude-sonnet-4-5 max_tokens 8192 [request] timeout 60这里有个关键点base_url只写到/api不要自己拼/v1/chat/completions之类的后缀具体路径由客户端按协议补全。Key 的生成入口在控制台的 API Keys 页面地址是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。如果你只是想先验证模型通不通可以直接用模型对话页面 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 发一条消息试试。注意配置文件里的 Key 不要提交到 Git 仓库建议用环境变量注入或者放进.gitignore覆盖的本地文件里。3. 可复制配置Intent 选择、权限声明与 Uri 解析骨架这一节是全文的技术核心直接给可复制的片段。先看权限声明。从 Android 6.0 起读相册需要运行时权限从 Android 13API 33起读图片要用READ_MEDIA_IMAGES替代旧的READ_EXTERNAL_STORAGE。所以AndroidManifest.xml里要同时声明按版本取用uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES /然后是发起选择的 Intent。这里我建议统一用ACTION_GET_CONTENT并显式指定image/*同时加上CATEGORY_OPENABLE保证返回的 Uri 是可读的public static final int REQ_PICK_IMAGE 1001; private void pickImage() { Intent intent new Intent(Intent.ACTION_GET_CONTENT); intent.setType(image/*); intent.addCategory(Intent.CATEGORY_OPENABLE); // 部分小米机型对 EXTRA_ALLOW_MULTIPLE 处理不一致单选先不开启 startActivityForResult(Intent.createChooser(intent, 选择图片), REQ_PICK_IMAGE); }接下来是 onActivityResult 里的解析骨架。核心思路先判空再判 schemecontent://走 query 解析file://直接取 pathquery 返回 null 时不要硬调moveToFirst()而是回退到读流Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode ! REQ_PICK_IMAGE || resultCode ! RESULT_OK) return; if (data null || data.getData() null) return; Uri uri data.getData(); String path UriResolver.resolvePath(this, uri); if (path ! null) { // 拿到真实路径可直接给裁剪/上传用 Log.d(PickImage, path path); } else { // 拿不到路径退化为读流避免空指针 try (InputStream in getContentResolver().openInputStream(uri)) { Bitmap bmp BitmapFactory.decodeStream(in); Log.d(PickImage, bitmap (bmp ! null)); } catch (Exception e) { Log.e(PickImage, open stream failed, e); } } }下面是UriResolver工具类的骨架把 excerpt 里那套MiPictureHelper的思路整理成更稳的版本重点是每个分支都做 null 判断query 结果为空时返回 null 而不是抛异常public final class UriResolver { public static String resolvePath(Context context, Uri uri) { if (uri null) return null; if (file.equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } if (content.equalsIgnoreCase(uri.getScheme())) { if (isMediaDocument(uri)) { return queryMediaPath(context, uri); } return queryDataColumn(context, uri); } return null; } private static String queryDataColumn(Context context, Uri uri) { Cursor cursor null; final String column MediaStore.Images.Media.DATA; try { cursor context.getContentResolver().query( uri, new String[]{column}, null, null, null); if (cursor ! null cursor.moveToFirst()) { int idx cursor.getColumnIndex(column); if (idx 0) return cursor.getString(idx); } } catch (Exception e) { Log.e(UriResolver, query failed, e); } finally { if (cursor ! null) cursor.close(); } return null; } private static String queryMediaPath(Context context, Uri uri) { String docId DocumentsContract.getDocumentId(uri); String[] split docId.split(:); Uri contentUri MediaStore.Images.Media.EXTERNAL_CONTENT_URI; String selection _id?; String[] args new String[]{split[1]}; return queryDataColumn(context, contentUri, selection, args); } private static boolean isMediaDocument(Uri uri) { return com.android.providers.media.documents.equals(uri.getAuthority()); } }注意queryDataColumn里我加了一个重载版本用于带 selection 的查询实际写的时候把 selection 和 selectionArgs 透传进去即可。这套骨架的关键改动是任何一步 query 返回 null 都不再直接moveToFirst()而是返回 null 让上层走读流兜底。4. 验证请求真机上确认 Uri 与路径是否拿到配置写完怎么验证分三步。第一步在onActivityResult里把 Uri 的 scheme、authority、以及解析出的 path 全部打出来Log.d(PickImage, scheme uri.getScheme() authority uri.getAuthority() path path);第二步在小米真机上跑一遍观察日志。正常情况你会看到schemecontent、authoritycom.android.providers.media.documents或com.miui.gallery...path 要么是真实路径要么是 null此时走读流分支。如果 path 为 null 但 bitmap 能解出来说明兜底生效没有崩。第三步如果你在用 AI 编码助手辅助排查可以顺手验证一下统一 Key 通道是否通。用模型对话页面发一条「帮我解释 Android ACTION_GET_CONTENT 返回 content Uri 的解析流程」能正常返回就说明 Key 和 base_url 配置没问题。地址是 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。长期做 Android 编码、需要助手持续参与的话可以看 Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。验证成功的标志很明确小米真机上选图后不再崩日志里能看到 Uri 和 path或 bitmap 解码成功并且换其他品牌机型同样不崩。这就说明你的解析骨架是厂商无关的。5. 本篇常见错排查空指针、权限、Uri 权限与版本坑第一个坑也是最常见的cursor为 null 直接moveToFirst()。这是本篇的原始报错。修法就是上面说的query 后先判cursor ! null再判moveToFirst()返回值任何一步失败都返回 null 走兜底。第二个坑SecurityException: Permission Denial。这通常是运行时权限没申请或者 Android 13 上还在用READ_EXTERNAL_STORAGE。检查你的权限申请逻辑是否按Build.VERSION.SDK_INT 33分支请求READ_MEDIA_IMAGES。第三个坑Uri 拿到了但读不了报FileNotFoundException。这多半是 Intent 没加CATEGORY_OPENABLE或者用了ACTION_PICK拿到一个临时授权 Uri出了回调就失效。统一改用ACTION_GET_CONTENT能规避大部分这类问题。第四个坑_data列被弃用。从 Android 10 起MediaStore.Images.Media.DATA属于分区存储下的受限列query 可能返回 null 或抛异常。所以路径解析只能当「尽力而为」真正的兜底必须是openInputStream()读流这一点在骨架里已经体现。第五个坑DocumentsContract.getDocumentId()在非 document Uri 上调用会抛IllegalArgumentException。所以调用前一定要先DocumentsContract.isDocumentUri(context, uri)判断别无条件调。提示如果你在排查时想快速确认某个 Uri 的 authority 属于哪类 provider把 authority 打印出来对照com.android.externalstorage.documents、com.android.providers.downloads.documents、com.android.providers.media.documents三类即可。6. 把统一 Key 通道接进你的 Android 工作流回到开头那个问题小米相册选图返回空指针本质是「不要假设 Uri 是文件路径」。这套骨架落地后你的选图逻辑就与厂商解耦了。而 TaoToken 统一 Key 通道在这里扮演的角色是让你的编码助手、模型对话、长期 Agent 任务共用一份配置不用在多个工具之间反复换 Key 和地址。具体接入时排障和接入相关的入口优先看 API Keys 和接入文档Key 在 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。验证模型是否通用模型对话页面。长期编码和 Agent 场景用 Coding Plan。把settings.json或config.toml里的base_url固定为https://taotoken.net/apiKey 用环境变量注入这套配置就能跟着你的 Android 项目走。最后留一个我踩过的坑早期我在onActivityResult里直接对data.getData()的结果调getPath()以为能拿到路径结果小米上返回的是编码 UrigetPath()出来一堆无意义字符串。后来统一改成「先解析、解析不到就读流」再没崩过。你可以先把上面UriResolver的骨架抄进去在小米真机上跑一遍看日志里 path 和 bitmap 哪个先出来基本就能定位你当前代码卡在哪一步了。