1. Android 文件三兄弟Uri、path、file 到底谁是谁做 Android 开发的同学几乎都在这三个东西上栽过跟头Uri、path、File。它们看起来都在描述一个文件在哪但语义完全不同混用一次就是FileNotFoundException、SecurityException或者空指针。我先把概念掰开揉碎讲清楚后面所有转换代码都建立在这个理解上。File是 Java 层的文件抽象它只认绝对路径字符串比如/storage/emulated/0/DCIM/Camera/IMG_001.jpg。它不关心 Android 的权限模型你new File(path)永远能创建对象但能不能读写是另一回事。path就是一个普通字符串本身没有类型含义。它可能是文件系统路径也可能是 URL 的一部分甚至是content://协议里的某段。很多 bug 就出在把 path 当成万能钥匙上。Uri是 Android 的统一资源标识符它可以是file://、content://、http://等等。从 Android 7.0API 24开始file://的 Uri 不能直接跨应用传递了必须走FileProvider生成content://的 Uri。这是转换逻辑里最大的分水岭。为什么这件事和 TaoToken 扯上关系因为现在很多 Android 项目会接入大模型能力做图片理解、文档解析、OCR 校对。你在相册选一张图拿到的是content://Uri你要把图片内容发给模型 API需要读成字节流或者转成 base64而 SDK 里配置的模型通道、Key、Base URL 又需要统一管理。TaoToken 提供的就是这样一条统一 Key 的 API 通道官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。你在这条通道上调用模型对话、做图片理解前置工作就是把本地 Uri 正确转成可读的 File 或字节数组。转换错了请求体就是空的模型返回的choices里啥也没有你还以为是 Key 配错了。所以这篇内容的定位很明确给正在做 Android 文件选择、拍照、分享、以及选图发给大模型的开发者一套能直接复制、能跑通、能排错的 Uri/path/file 互转方案顺带把 TaoToken 统一 Key 的配置串起来。适合谁适合已经能写 Activity、会用startActivityForResult或ActivityResultLauncher、但一遇到content://就头大的初中级 Android 开发者。下面每一段都有代码、有配置、有验证动作你可以边看边在项目里试。2. 接入前的准备TaoToken 统一 Key 与 Android 工程配置在写转换代码之前先把通道这件事定下来。很多同学把文件转换和网络请求混在一起调出了问题分不清是 Uri 转错了还是 Key 失效了。我的建议是先让 API 通道独立跑通再接文件转换。这样排障时变量只有一个。TaoToken 的定位是统一 Key 的模型 API 通道你注册后在控制台生成一个 Key就能在多个模型之间切换调用不用每个模型单独申请。对 Android 项目来说好处是配置项收敛一个 Base URL、一个 Key、一个 Model ID三件套写进BuildConfig或者本地local.properties不硬编码进仓库。先看控制台和 Key 的入口。生成 Key 的页面在 https://taotoken.net/api-keys 接入文档在 https://taotoken.net/doc 模型对话的在线调试在 https://taotoken.net/model-chat 。如果你后面要做长期编码或者 Agent 类功能可以看 Coding Planhttps://taotoken.net/coding-plan 。Android 工程里的配置我习惯放在local.properties不进版本库然后在build.gradle里读出来注入BuildConfig// app/build.gradle android { defaultConfig { buildConfigField String, TAOTOKEN_BASE_URL, \https://taotoken.net/api\ buildConfigField String, TAOTOKEN_MODEL, \你的模型ID\ } } def localProps new Properties() def localFile rootProject.file(local.properties) if (localFile.exists()) { localProps.load(new FileInputStream(localFile)) } android.defaultConfig.buildConfigField String, TAOTOKEN_KEY, \${localProps[TAOTOKEN_KEY] ?: }\local.properties里加一行TAOTOKEN_KEYsk-你的实际Key这里有个坑要提前说BuildConfig的字符串字段一定要用转义引号包住否则 Gradle 同步会报Expected string but was ...。另外 Key 千万别提交到 Git.gitignore里确认有local.properties。网络层用 OkHttp 就够了不需要引入重型框架。请求体里如果要带图片先把本地文件读成 base64。这一步就依赖第 3 节的转换代码——你得先拿到一个能读的File或InputStream。所以顺序是配置三件套 → 写转换工具类 → 读文件 → 发请求。再补一个 Manifest 配置Android 7.0 以上必须声明 FileProvider否则file转uri直接崩provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileProvider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /providerres/xml/file_paths.xml?xml version1.0 encodingutf-8? paths external-path nameexternal_files path. / cache-path namecache_files path. / files-path namefiles path. / /paths注意authorities必须和代码里FileProvider.getUriForFile传的第二个参数完全一致多一个字符就是IllegalArgumentException: Failed to find configured root。这个报错我在第 5 节会专门拆。3. 可复制配置Uri、path、file 六向转换工具类这一节是全文的核心直接给一个完整的 Kotlin 工具类六个方向的转换都覆盖并且处理了 API 24 分水岭、content://查询、以及异常兜底。你可以整个文件复制进项目包名改一下就能用。先看整体结构。工具类叫FileUriConverter所有方法都是静态的参数校验放在最前面避免空指针往下传。object FileUriConverter { // 1. path - File fun pathToFile(path: String?): File? { if (path.isNullOrBlank()) return null return File(path) } // 2. path - Uri仅限文件系统路径且注意 API 24 限制 fun pathToUri(path: String?): Uri? { if (path.isNullOrBlank()) return null return Uri.fromFile(File(path)) } // 3. File - path fun fileToPath(file: File?): String? { return file?.absolutePath } // 4. File - Uri区分 API 24 fun fileToUri(context: Context, file: File?): Uri? { if (file null) return null return if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { FileProvider.getUriForFile( context.applicationContext, ${context.packageName}.fileProvider, file ) } else { Uri.fromFile(file) } } // 5. Uri - Filecontent:// 走查询file:// 直接转 fun uriToFile(context: Context, uri: Uri?): File? { if (uri null) return null return when (uri.scheme) { ContentResolver.SCHEME_FILE - uri.path?.let { File(it) } ContentResolver.SCHEME_CONTENT - { val path getRealPathFromUri(context, uri) path?.let { File(it) } } else - null } } // 6. Uri - path fun uriToPath(context: Context, uri: Uri?): String? { return uriToFile(context, uri)?.absolutePath } // 核心content:// 转真实路径 private fun getRealPathFromUri(context: Context, uri: Uri): String? { var result: String? null val projection arrayOf(MediaStore.Images.ImageColumns.DATA) context.contentResolver.query(uri, projection, null, null, null)?.use { cursor - if (cursor.moveToFirst()) { val idx cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA) if (idx 0) result cursor.getString(idx) } } if (result.isNullOrBlank()) { result uri.path } return result } }这段代码有几个设计取舍要讲清楚。第一uriToFile里对scheme做了分支file://直接取uri.pathcontent://才走ContentResolver查询。原 excerpt 里那种不判断 scheme 直接 query 的写法遇到file://会返回 null cursor然后 fallback 到uri.getPath()逻辑上能跑但不够清晰。第二getRealPathFromUri里用了use扩展函数自动关闭 cursor避免忘记close()导致的内存泄漏。原 excerpt 里是手动cursor.close()如果中间抛异常就漏了。第三MediaStore.Images.ImageColumns.DATA这个字段在 Android 10API 29之后对分区存储有限制查询相册图片可能拿不到真实路径。这时候更稳的做法是把content://直接读成InputStream而不是强求转成 File。所以工具类之外我再给一个Uri 直接读字节的方法专门给发 API 用fun uriToBytes(context: Context, uri: Uri): ByteArray? { return try { context.contentResolver.openInputStream(uri)?.use { it.readBytes() } } catch (e: Exception) { e.printStackTrace() null } }这个方法绕过了路径转换直接拿流在分区存储下最可靠。你发图片给模型时优先用它。再看一个实际调用示例选图后转 base64 并发请求val launcher registerForActivityResult(ActivityResultContracts.GetContent()) { uri - uri ?: returnregisterForActivityResult val bytes FileUriConverter.uriToBytes(this, uri) ?: returnregisterForActivityResult val base64 Base64.encodeToString(bytes, Base64.NO_WRAP) // 组装请求体调用 TaoToken 通道 } launcher.launch(image/*)配置片段里请求头三件套这样写val request Request.Builder() .url(${BuildConfig.TAOTOKEN_BASE_URL}/v1/chat/completions) .addHeader(Authorization, Bearer ${BuildConfig.TAOTOKEN_KEY}) .addHeader(Content-Type, application/json) .post(body) .build()注意 Base URL 是https://taotoken.net/api拼接路径时不要再重复/api。Model ID 填你在控制台看到的那个别自己猜。4. 验证请求从选图到模型返回的完整链路代码写完不算完得验证。我一般分三步验证先验证转换结果对不对再验证文件能不能读最后验证 API 返回是否正常。三步都过才算这条链路通了。第一步验证转换结果。在选图回调里打日志把 Uri、path、File 的绝对路径都打出来对比Log.d(UriCheck, 原始 Uri: $uri) Log.d(UriCheck, scheme: ${uri.scheme}) val file FileUriConverter.uriToFile(this, uri) Log.d(UriCheck, 转换后 File: ${file?.absolutePath}) Log.d(UriCheck, 文件存在: ${file?.exists()}, 大小: ${file?.length()})预期结果是scheme为contentFile路径指向/storage/emulated/0/...或应用缓存目录exists()为 truelength()大于 0。如果exists()是 false说明路径转换错了或者分区存储下拿不到真实路径这时候改用uriToBytes。第二步验证字节读取。把读到的字节数打出来val bytes FileUriConverter.uriToBytes(this, uri) Log.d(UriCheck, 读取字节数: ${bytes?.size})一张普通照片应该是几十万到几百万字节。如果是 0 或者 null检查openInputStream是否抛了FileNotFoundException通常是 Uri 权限没拿到。第三步验证 API 返回。用 OkHttp 发一个最小的请求把返回体完整打出来override fun onResponse(call: Call, response: Response) { val bodyStr response.body?.string() Log.d(ApiCheck, HTTP ${response.code}) Log.d(ApiCheck, 响应体: $bodyStr) }正常返回的 JSON 里应该有choices数组choices[0].message.content是模型输出。如果choices是空数组或者字段缺失先看 HTTP 状态码401 是 Key 问题400 多半是请求体格式问题200 但choices空检查model字段是否拼错。我实测下来最容易出问题的是请求体里 base64 拼接格式。正确写法是{ model: 你的模型ID, messages: [ { role: user, content: [ {type: text, text: 描述这张图片}, {type: image_url, image_url: {url: data:image/jpeg;base64,你的base64}} ] } ] }data:image/jpeg;base64,这个前缀不能少少了模型解析不了。base64 字符串本身不能带换行用Base64.NO_WRAP。验证通过后你可以把日志删掉但建议保留一个开关方便线上排查。另外如果你要长期做这类图片理解功能建议把 Key 管理、请求封装、重试逻辑收敛到一个ApiClient类里别散落在各个 Activity。5. 常见报错排查401、FileProvider、choices 为空怎么修这一节按真实报错来每个报错给出触发原因和修复动作。你遇到问题时直接对号入座。报错一java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/...触发场景调用FileProvider.getUriForFile时传入的 File 路径不在file_paths.xml声明的范围内。比如你传了一个/storage/emulated/0/Download/xxx.pdf但file_paths.xml里只声明了cache-path。修复在file_paths.xml里补上对应的根。外部存储用external-path nameexternal_files path. /下载目录可以细化成external-path namedownload pathDownload/ /。注意path.表示整个根范围越大越方便但也越宽按需收窄。报错二SecurityException: Permission Denial: opening provider ... requires ... or grantUriPermission()触发场景把content://Uri 通过 Intent 传给另一个应用比如分享、拍照回传但没给临时权限。修复Intent 里加FLAG_GRANT_READ_URI_PERMISSIONintent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)如果是拍照回传还要在onActivityResult里确认 Uri 是 FileProvider 生成的不是Uri.fromFile。报错三HTTP 401响应体{error:{message:Invalid API key}}触发场景Key 没读到、读成空字符串、或者带了多余空格。修复先打日志确认BuildConfig.TAOTOKEN_KEY非空且以sk-开头。local.properties里等号两边不要加空格值不要加引号。Gradle 同步后如果还是空检查buildConfigField是否写在defaultConfig里以及buildFeatures { buildConfig true }是否开启AGP 8.0 之后默认关闭。报错四HTTP 200 但choices为空或报reading choices相关解析错误触发场景请求体 JSON 结构不对或者model字段值不存在。Gson 解析时字段对不上就会抛JsonSyntaxException或者拿到 null。修复先把原始响应体字符串打出来别直接反序列化。确认choices字段存在且是数组。如果响应里是{error:...}说明请求根本没成功只是 HTTP 层返回了 200。检查model是否和控制台一致messages是否是数组content类型是否匹配。报错五FileNotFoundException: /storage/emulated/0/...: open failed: EACCES (Permission denied)触发场景Android 10 以上没申请存储权限或者分区存储下直接按路径读文件。修复优先用contentResolver.openInputStream(uri)读流不要强转路径。如果必须用路径申请READ_MEDIA_IMAGESAPI 33或READ_EXTERNAL_STORAGE并在设置里引导用户授权。报错六URISyntaxException触发场景用new URI(uri.toString())转 FileUri 字符串里带空格或中文没编码。修复别用java.net.URI绕直接用uriToFile里的 scheme 分支处理。content://走查询file://走uri.path。这几个报错覆盖了 90% 的转换问题。剩下 10% 多半是厂商 ROM 的路径差异比如华为、小米的相册 Uri 格式略有不同处理方式还是走ContentResolver查询别硬编码路径。6. 把转换逻辑接进你的项目下一步动作到这里转换工具类、配置三件套、验证链路、排错清单都齐了。你现在可以做的动作很具体把FileUriConverter复制进项目补上file_paths.xml和 Manifest 的 provider 声明然后在选图回调里跑一遍第 4 节的三步验证。日志里看到exists()为 true、字节数正常、API 返回choices有内容这条链路就算通了。如果你还没生成 Key去 https://taotoken.net/api-keys 建一个接入细节看 https://taotoken.net/doc 想先在网页上试试模型返回格式用 https://taotoken.net/model-chat 最快。做长期编码或 Agent 类功能的Coding Plan 在 https://taotoken.net/coding-plan 。最后留一个我踩过的坑FileProvider的authorities用了${applicationId}.fileProvider但代码里写的是context.packageName .fileProvider如果项目有applicationIdSuffix比如 debug 包加了.debug这两个值就不一致运行时报Failed to find configured root。统一用context.packageName拼或者统一用BuildConfig.APPLICATION_ID别混着来。
阅读完成 · 觉得有帮助?