1. 从一次 Failed to find provider info 说起:ContentProvider 跨进程共享到底难在哪
如果你正在搜 Android ContentProvider 与 ContentResolver 实战配置,大概率已经踩过这个坑:代码照着书写完了,query()也调了,Logcat 里却冷冷地甩出一行Failed to find provider info for com.example.xxx.provider。我试过在模拟器上反复重装两个模块,最后发现问题根本不在 Java 代码,而在AndroidManifest.xml里少了一个<queries>标签。
ContentProvider 是 Android 四大组件里最“低调”的一个。Activity 管界面、Service 管后台、BroadcastReceiver 管事件,而 ContentProvider 管的是跨应用数据共享。你可以把它想成一个餐厅的服务员:后厨(数据库)不直接对外开放,服务员(Provider)拿着菜单(Uri)把菜端给客人(其他应用)。客人不关心后厨怎么炒菜,只关心“我要一份 ID=1 的购物车数据”能不能拿到。
ContentResolver 则是客人的角色。它不直接 new 一个 Provider,而是通过getContentResolver()拿到系统给的“点餐入口”,再用 Uri 告诉系统“我要访问哪个应用、哪张表、哪条记录”。系统在中间做路由和权限校验,这就是跨进程通信(IPC)的底层逻辑。
这套机制适合谁?适合需要把自家数据开放给其他 App 的场景,比如通讯录、媒体库、购物车同步、插件化模块间通信。不适合什么?不适合应用内部单纯读写自己的数据库——那种场景直接用 Room 就够了,套一层 Provider 纯属自找麻烦。
本文要交付的是一条完整闭环:在 chapter06 里自定义一个ShoppingCarProvider,在 chapter07 里用ContentResolver完成增删改查,最后用adb shell content命令在真机或模拟器上验证数据真的写进去了。中间会给出可复制的 Manifest 片段、UriMatcher 匹配规则、CRUD 代码,以及 401、SecurityException、NullPointerException 这些真实报错的排查路径。如果你在团队里用统一 Key 通道管理模型调用,TaoToken 那套思路同样适用于这里——把“凭证”和“调用”解耦,Provider 只管数据,Resolver 只管请求。
2. TaoToken 统一 Key 通道前置:为什么跨模块调试也需要一个统一入口
在正式写 Provider 之前,先聊一个容易被忽略的前置问题:调试凭证和配置的分散管理。chapter06 和 chapter07 是两个独立模块,各自有AndroidManifest.xml、各自的applicationId、各自的数据库实例。当你反复在两者之间切换调试时,最烦的不是代码写错,而是“我到底改的是哪个模块的配置”。
这跟 TaoToken 解决的是同一类问题。TaoToken 是一个统一的大模型 API 通道,官网在https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,它的核心价值不是“多一个 API 地址”,而是把 Base URL、API Key、Model ID 这三件套收敛到一个入口。你在 Android 里做跨进程共享时,同样需要这种“收敛”思维:Provider 的authorities就是全局唯一的“Key”,所有访问方都必须用同一个 authority 去请求,否则系统根本找不到目标。
具体到操作层面,你需要先准备好三样东西:
第一,一个可用的 TaoToken API Key。进入控制台https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite,在 API Keys 页面创建一个新 Key。这个 Key 后面会用在你的调试脚本或辅助工具里,比如用模型对话能力帮你生成测试数据、解释报错日志。
第二,确认你的 Base URL。TaoToken 的 API 入口是https://taotoken.net/api,注意这个地址不带 UTM 参数,是纯接口地址。如果你用 Claude Code 或 Cline 这类编码工具,Base URL 就填这个。
第三,选一个 Model ID。比如你想让模型帮你分析Failed to find provider info的日志,可以用模型对话页面https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite先试一下模型是否正常响应。
这里要强调一个安全边界:TaoToken 是合规的 API 通道,不是让你绕过任何网络限制的工具。它的使用场景是“你在正常网络环境下,需要一个统一的模型调用入口”。同样,ContentProvider 的exported="true"也不是让你无限制暴露数据,而是配合<queries>做精确声明。两者都遵循同一个原则:最小权限 + 显式声明。
如果你打算长期做 Android 跨模块开发,建议把调试用的 Key 和配置写进local.properties或环境变量,不要硬编码进build.gradle。这一点和 TaoToken 的 Coding Plan 思路一致——把长期编码任务和临时调试分开管理,Coding Plan 入口在https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite,适合需要持续调用模型的 Agent 场景。
前置准备做完,接下来进入真正的配置环节。记住一个判断标准:如果 chapter07 能通过 authority 找到 chapter06 的 Provider,说明你的“统一 Key 通道”打通了;如果报Failed to find provider info,说明通道没建好,先别怀疑 Java 代码。
3. 可复制配置:AndroidManifest 注册片段与 UriMatcher 匹配规则
这一节是全文的核心操作区。我会把 chapter06(数据提供方)和 chapter07(数据访问方)的配置拆开讲,每一段都可以直接复制。
3.1 chapter06 注册 ShoppingCarProvider
在chapter06/src/main/AndroidManifest.xml的<application>标签内添加:
<provider android:name=".provider.ShoppingCarProvider" android:authorities="com.example.chapter06.provider.ShoppingCarProvider" android:exported="true" />三个属性的含义必须记牢:android:name是 Provider 类的全路径(相对包名可省略前缀);android:authorities是全局唯一标识,相当于这个 Provider 的“身份证号”,其他应用必须用这个字符串来定位;android:exported="true"表示允许其他应用访问。如果你设成false,chapter07 调用时会直接抛SecurityException。
3.2 chapter07 声明包可见性
从 Android 11(API 30)开始,应用默认看不到其他应用。你必须在chapter07/src/main/AndroidManifest.xml的<application>之前添加<queries>:
<queries> <package android:name="com.example.chapter06" /> <provider android:authorities="com.example.chapter06.provider.ShoppingCarProvider" /> </queries>两种声明方式可以同时写,也可以只写一种。<package>是按包名声明,<provider>是按 authority 精确声明。推荐两个都写,兼容性最好。注意<queries>必须放在<application>外面、<manifest>里面,放错位置不生效。
3.3 UriMatcher 匹配规则
Provider 内部需要用UriMatcher区分“查全部”和“查单条”。在ShoppingCarProvider.java里定义:
private static final String AUTHORITY = "com.example.chapter06.provider.ShoppingCarProvider"; private static final int SHOPPING_CAR_ALL = 1; private static final int SHOPPING_CAR_ITEM = 2; private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { uriMatcher.addURI(AUTHORITY, "shoppingcar", SHOPPING_CAR_ALL); uriMatcher.addURI(AUTHORITY, "shoppingcar/#", SHOPPING_CAR_ITEM); }#是通配符,匹配任意数字。所以content://.../shoppingcar命中SHOPPING_CAR_ALL,content://.../shoppingcar/1命中SHOPPING_CAR_ITEM。在query()里用switch (uriMatcher.match(uri))分流即可。
3.4 如果你用 Cline MCP 或 Codex 辅助调试
有些同学会用 Cline 的 MCP 能力或 Codex 来生成测试代码。这时候三件套要写全:Base URL 填https://taotoken.net/api,API Key 填你在控制台创建的那串,Model ID 填你选的模型。Codex 的auth.json里对应字段是base_url、api_key、model。Cline MCP 的配置里则是baseUrl、apiKey、modelId。三者缺一不可,少一个就会报 401 或local proxy failed。
配置写完后,先别急着跑。用adb shell content命令做一次静态验证,比直接跑 App 更快定位问题。
4. 验证请求:adb shell content 命令与 CRUD 调用闭环
配置对不对,跑一次就知道。这一节给出从命令行到代码的完整验证路径。
4.1 用 adb shell content 直接查询
先安装并运行 chapter06,确保数据库初始化过。然后执行:
adb shell content query --uri content://com.example.chapter06.provider.ShoppingCarProvider/shoppingcar如果返回Row: 0 _id=1, name=测试商品, price=99.99, count=1,说明 Provider 注册成功、authority 匹配、数据可读。如果返回Error: Failed to find provider info,回到第 3.2 节检查<queries>。
插入一条数据:
adb shell content insert --uri content://com.example.chapter06.provider.ShoppingCarProvider/shoppingcar \ --bind name:s:adb测试商品 \ --bind price:f:88.88 \ --bind count:i:2name:s:表示字符串,price:f:表示浮点,count:i:表示整数。类型写错会报IllegalArgumentException。
4.2 ContentResolver 查询代码
在 chapter07 的ProviderActivity.java里:
ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://com.example.chapter06.provider.ShoppingCarProvider/shoppingcar"); Cursor cursor = resolver.query(uri, null, null, null, null); if (cursor != null) { while (cursor.moveToNext()) { int id = cursor.getInt(cursor.getColumnIndexOrThrow("_id")); String name = cursor.getString(cursor.getColumnIndexOrThrow("name")); float price = cursor.getFloat(cursor.getColumnIndexOrThrow("price")); Log.d("ProviderTest", "id=" + id + ", name=" + name + ", price=" + price); } cursor.close(); }注意用getColumnIndexOrThrow而不是getColumnIndex,列名写错时会直接抛异常,比返回 -1 更容易排查。
4.3 插入与单条查询
ContentValues values = new ContentValues(); values.put("name", "测试商品"); values.put("price", 99.99f); values.put("count", 1); Uri newUri = resolver.insert(uri, values); Log.d("ProviderTest", "inserted uri=" + newUri); Uri itemUri = Uri.withAppendedPath(uri, "1"); Cursor itemCursor = resolver.query(itemUri, null, null, null, null); if (itemCursor != null && itemCursor.moveToFirst()) { String name = itemCursor.getString(itemCursor.getColumnIndexOrThrow("name")); Log.d("ProviderTest", "item name=" + name); itemCursor.close(); }Uri.withAppendedPath(uri, "1")等价于手动拼content://.../shoppingcar/1。插入成功后返回的newUri通常带新记录的 ID,可以直接用于后续查询。
4.4 成功结果的判断标准
一次完整闭环成功的标志是:adb 命令行能查到数据 → App 内query()返回非空 Cursor →insert()返回的 Uri 末尾带数字 ID → 再次查询能看到新插入的记录。四个环节全过,说明 Provider 和 Resolver 的通道彻底打通。
如果中间任何一步断了,进入下一节的排错流程。
5. 本篇常见错排查:401、SecurityException 与 NullPointerException 对照
这一节按真实报错逐条拆解。每条都给出错误信息、根因、修复动作。
5.1 Failed to find provider info
错误信息:
Failed to find provider info for com.example.chapter06.provider.ShoppingCarProvider根因:Android 11+ 包可见性限制,chapter07 没有声明要访问 chapter06。修复:在 chapter07 的AndroidManifest.xml中添加<queries>标签,同时声明<package>和<provider>。另外确认 chapter06 已安装且至少运行过一次。
5.2 SecurityException: Permission Denial
错误信息:
java.lang.SecurityException: Permission Denial: opening provider ... requires ... or ...根因:chapter06 的<provider>里android:exported="false",或者没有设置android:grantUriPermissions。修复:改成android:exported="true"。如果只想临时授权,可以用Intent.FLAG_GRANT_READ_URI_PERMISSION,但跨应用长期共享还是直接 exported 更简单。
5.3 NullPointerException: shoppingCarDao() on a null object
错误信息:
NullPointerException: Attempt to invoke virtual method '...shoppingCarDao()' on a null object reference根因:ContentProvider 的onCreate()执行时机早于 Application 的onCreate(),此时MyApplication.shoppingDatabase还没初始化。修复:延迟初始化,在真正需要 Dao 时再取:
private ShoppingCarDao getShoppingCarDao() { if (shoppingCarDao == null) { MyApplication app = (MyApplication) getContext().getApplicationContext(); shoppingCarDao = app.getShoppingDB().shoppingCarDao(); } return shoppingCarDao; }5.4 401 与 local proxy failed
如果你在用 Cline MCP 或 Codex 辅助调试时遇到:
401 Unauthorized local proxy failed根因:Base URL、API Key、Model ID 三件套没写全,或者 Key 已失效。修复:Base URL 用https://taotoken.net/api,Key 去控制台重新生成,Model ID 确认拼写。Codex 的auth.json里三个字段都要有;Cline MCP 配置里baseUrl、apiKey、modelId一个都不能少。
5.5 reading choices 报错
错误信息:
error reading choices: unexpected end of JSON input根因:模型返回的响应体不完整,通常是网络中断或 Base URL 写错。修复:确认 Base URL 是https://taotoken.net/api,不要多加斜杠或路径。如果持续出现,换一个 Model ID 重试。
5.6 OAuth 相关报错
如果你用 Claude Code 接入,遇到 OAuth 回调失败,检查 deep link 配置。Claude Code 的 Anthropic 兼容入口在https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite,按页面说明配置回调地址即可。注意 OAuth 只用于工具授权,不影响 ContentProvider 本身的调试。
排错的核心原则:先看 Logcat 第一行报错,再对照本节定位。90% 的问题集中在 Manifest 配置和初始化时机,Java 业务代码反而不是重灾区。
6. 语义一致收尾:把统一 Key 通道的思路带回 Android 开发
写到这里,chapter06 和 chapter07 的闭环应该已经跑通了。回头看,ContentProvider 和 ContentResolver 的关系,本质上就是“统一入口 + 显式声明”。Provider 用authorities作为全局唯一的 Key,Resolver 用 Uri 作为请求地址,系统在中间做路由。这套设计和 TaoToken 的统一 Key 通道是同一个思路:把分散的凭证收敛到一个入口,调用方只需要知道“入口在哪”和“我要什么”。
如果你后续要做更复杂的跨进程共享,比如多张表、批量操作、事务,可以在 UriMatcher 里加更多匹配规则,在 Provider 里用SQLiteDatabase的beginTransaction()包住批量插入。如果要做权限控制,可以在<provider>里加android:readPermission和android:writePermission,配合自定义权限声明。
调试工具方面,adb shell content是最轻量的验证手段,不用改代码就能测 CRUD。模型辅助方面,遇到看不懂的报错,可以把 Logcat 贴到模型对话页面https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite让模型帮你分析。长期做 Android 跨模块开发的话,Coding Planhttps://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite适合把重复性的配置生成、日志分析交给 Agent 处理。
最后留一个实用技巧:在 chapter07 里加一个ContentObserver,监听 chapter06 的数据变化。这样 chapter06 插入新数据时,chapter07 能实时收到通知,不用轮询查询。registerContentObserver(uri, true, observer)注册,onChange()回调里重新查询即可。这一步做完,你的跨进程数据共享就从“能读”升级到“能实时同步”了。