1. 为什么你的 ContentProvider 总是“查不到数据”如果你写过 Android 四大组件里的 ContentProvider大概率遇到过这种场景AndroidManifest.xml里 provider 也注册了ContentResolver.query()也调了结果要么抛IllegalArgumentException: Unknown URI要么返回一个空 Cursor要么跨应用访问直接SecurityException。更让人抓狂的是数据明明插进去了另一个界面却完全没反应——因为没人通知它。ContentProvider 是 Android 四大组件里最“低调”的一个。Activity 管界面Service 管后台BroadcastReceiver 管广播而 ContentProvider 管的是跨应用数据共享。它把数据封装成类似content://authority/path/id的 Uri让别的应用通过 ContentResolver 用统一接口增删改查再配合 ContentObserver 实现数据变化的实时监听。听起来很完整但真正落地时Uri 拼错一个字符、authority 和包名对不上、exported没开、权限没声明链路就断在某一环。这篇就围绕Uri、ContentResolver、ContentObserver三个热词把 ContentProvider 从配置骨架到验证路径完整跑一遍。我会给出可以直接复制的AndroidManifest.xml与 provider 配置片段、ContentResolver 的 CRUD 调用示例、ContentObserver 的注册与注销代码最后用 adb 和 Logcat 做验证。适合已经会写 Activity、想搞懂跨应用数据共享的 Android 开发者也适合被Unknown URI折磨过、想系统梳理一遍的同学。2. 先把 Uri 和 authority 这两件事说透ContentProvider 的入口就是 Uri。它的结构是content://authority/path/id拆开看四段content://是协议固定不变authority 是这个 provider 的唯一标识通常写成包名.providerpath 指向具体的数据表或资源id 是可选的单条记录标识。比如content://com.example.myapp.provider/users/123authority 是com.example.myapp.providerpath 是usersid 是123。这里最容易踩的坑是authority 必须和 Manifest 里android:authorities完全一致一个字符都不能差。我见过有人 Manifest 写com.example.myapp.provider代码里 Uri 写成com.example.myapp.providers多了个 s结果就是Unknown URI。另一个坑是 UriMatcher 的注册顺序users/#这种带 id 的规则要放在users之后匹配否则users会先命中id 永远解析不出来。在动手写代码前建议先把这套“数据共享链路”想清楚provider 端负责定义 Uri 规则、实现 CRUD、在数据变化时notifyChangeclient 端通过 ContentResolver 发起请求、通过 ContentObserver 订阅变化。两端靠 authority 和 Uri 对齐靠权限控制谁能读谁能写。下面这张表可以先存着配置时逐项对照。配置项作用常见错误android:authoritiesprovider 唯一标识与代码 Uri 不一致android:exported是否允许跨应用访问忘了设 true跨应用直接拒绝android:readPermission读权限权限名拼写不一致android:writePermission写权限protectionLevel 设错grantUriPermissions临时授权需要精细控制时未开启如果你在本地调试时想快速验证模型对 Android 代码的理解或者让 AI 帮你补全 provider 的样板代码可以先用 TaoToken 模型对话 把思路理一遍再落到工程里。真正跑链路还是得靠本地 adb 和 Logcat。3. 可复制的 provider 配置与 CRUD 骨架3.1 AndroidManifest.xml 里的 provider 声明先看 Manifest。provider 必须声明在application内部authorities和代码里的AUTHORITY常量保持一致。如果只是自己应用内部用exported设 false 更安全要跨应用共享才设 true并且配上读写权限。application !-- 声明 ContentProvider -- provider android:name.provider.MyContentProvider android:authoritiescom.example.myapp.provider android:enabledtrue android:exportedtrue android:readPermissioncom.example.myapp.permission.READ_USERS android:writePermissioncom.example.myapp.permission.WRITE_USERS android:grantUriPermissionstrue !-- 针对路径的细粒度权限 -- path-permission android:pathPrefix/users android:readPermissioncom.example.myapp.permission.READ_USERS / grant-uri-permission android:path/users/public/* / /provider !-- 声明自定义权限 -- permission android:namecom.example.myapp.permission.READ_USERS android:protectionLevelnormal / permission android:namecom.example.myapp.permission.WRITE_USERS android:protectionLeveldangerous / /application注意protectionLevel用normal时对方安装即授予用dangerous需要运行时申请。调试阶段如果一直被SecurityException拦住可以先临时把权限去掉、exported设 true确认链路通了再逐步加回权限。3.2 ContentProvider 核心方法实现provider 端的关键是 UriMatcher 和四个 CRUD 方法。下面这段可以直接作为骨架重点看query里的setNotificationUri和insert/update/delete里的notifyChange——它们是 ContentObserver 能收到通知的前提。public class MyContentProvider extends ContentProvider { public static final String AUTHORITY com.example.myapp.provider; public static final Uri CONTENT_URI Uri.parse(content:// AUTHORITY /users); private static final int USERS 1; private static final int USER_ID 2; private static final UriMatcher sUriMatcher new UriMatcher(UriMatcher.NO_MATCH); static { sUriMatcher.addURI(AUTHORITY, users, USERS); sUriMatcher.addURI(AUTHORITY, users/#, USER_ID); } private SQLiteDatabase mDatabase; Override public boolean onCreate() { DatabaseHelper helper new DatabaseHelper(getContext()); mDatabase helper.getWritableDatabase(); return mDatabase ! null; } Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder builder new SQLiteQueryBuilder(); builder.setTables(users); int match sUriMatcher.match(uri); switch (match) { case USERS: break; case USER_ID: builder.appendWhere(_id uri.getLastPathSegment()); break; default: throw new IllegalArgumentException(Unknown URI: uri); } Cursor cursor builder.query(mDatabase, projection, selection, selectionArgs, null, null, sortOrder); // 关键绑定通知 Uri数据变化时观察者才能收到 cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } Override public Uri insert(Uri uri, ContentValues values) { if (sUriMatcher.match(uri) ! USERS) { throw new IllegalArgumentException(Unknown URI: uri); } long rowId mDatabase.insert(users, null, values); if (rowId 0) { Uri newUri ContentUris.withAppendedId(CONTENT_URI, rowId); getContext().getContentResolver().notifyChange(newUri, null); return newUri; } throw new SQLException(Failed to insert row into uri); } Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int count; int match sUriMatcher.match(uri); if (match USER_ID) { String where _id uri.getLastPathSegment(); if (selection ! null) where AND ( selection ); count mDatabase.update(users, values, where, selectionArgs); } else if (match USERS) { count mDatabase.update(users, values, selection, selectionArgs); } else { throw new IllegalArgumentException(Unknown URI: uri); } if (count 0) { getContext().getContentResolver().notifyChange(uri, null); } return count; } Override public int delete(Uri uri, String selection, String[] selectionArgs) { int count; int match sUriMatcher.match(uri); if (match USER_ID) { String where _id uri.getLastPathSegment(); if (selection ! null) where AND ( selection ); count mDatabase.delete(users, where, selectionArgs); } else if (match USERS) { count mDatabase.delete(users, selection, selectionArgs); } else { throw new IllegalArgumentException(Unknown URI: uri); } if (count 0) { getContext().getContentResolver().notifyChange(uri, null); } return count; } Override public String getType(Uri uri) { switch (sUriMatcher.match(uri)) { case USERS: return vnd.android.cursor.dir/vnd.com.example.users; case USER_ID: return vnd.android.cursor.item/vnd.com.example.user; default: throw new IllegalArgumentException(Unknown URI: uri); } } }3.3 ContentResolver 的增删改查调用client 端通过 ContentResolver 操作代码里不用关心底层是 SQLite 还是网络。查询时记得在 finally 里cursor.close()否则容易泄漏。public class UserRepository { private static final Uri CONTENT_URI Uri.parse(content://com.example.myapp.provider/users); // 查询 public ListUser queryAll(Context context) { ListUser list new ArrayList(); Cursor cursor context.getContentResolver().query( CONTENT_URI, null, null, null, created_at DESC); if (cursor ! null) { try { while (cursor.moveToNext()) { User u new User(); u.id cursor.getLong(cursor.getColumnIndexOrThrow(_id)); u.name cursor.getString(cursor.getColumnIndexOrThrow(name)); u.email cursor.getString(cursor.getColumnIndexOrThrow(email)); list.add(u); } } finally { cursor.close(); } } return list; } // 插入 public Uri insert(Context context, String name, String email) { ContentValues values new ContentValues(); values.put(name, name); values.put(email, email); return context.getContentResolver().insert(CONTENT_URI, values); } // 更新 public int update(Context context, long id, String newName) { ContentValues values new ContentValues(); values.put(name, newName); Uri uri ContentUris.withAppendedId(CONTENT_URI, id); return context.getContentResolver().update(uri, values, null, null); } // 删除 public int delete(Context context, long id) { Uri uri ContentUris.withAppendedId(CONTENT_URI, id); return context.getContentResolver().delete(uri, null, null); } }3.4 ContentObserver 监听数据变化ContentObserver 是“数据变了通知我”的机制。注册时传入 Uriprovider 端notifyChange后onChange就会被回调。注意注册和注销要成对出现通常在onResume注册、onPause注销避免界面不可见时还在收通知。public class UserListActivity extends AppCompatActivity { private static final Uri CONTENT_URI Uri.parse(content://com.example.myapp.provider/users); private ContentObserver mObserver; Override protected void onResume() { super.onResume(); mObserver new ContentObserver(new Handler(Looper.getMainLooper())) { Override public void onChange(boolean selfChange, Uri uri) { super.onChange(selfChange, uri); Log.d(Observer, 数据变化: uri); // 重新查询刷新 UI reloadData(); } }; getContentResolver().registerContentObserver( CONTENT_URI, true, mObserver); } Override protected void onPause() { super.onPause(); if (mObserver ! null) { getContentResolver().unregisterContentObserver(mObserver); mObserver null; } } private void reloadData() { // 调用上面的 queryAll 刷新列表 } }注意registerContentObserver第二个参数notifyForDescendants设为 true 时子路径的变化也会触发。如果你只想监听精确 Uri设 false。4. 用 adb 和 Logcat 验证整条链路配置写完了怎么确认真的通了分三步验证。第一步确认 provider 被系统识别。安装应用后执行adb shell dumpsys activity providers | grep com.example.myapp.provider如果能看到 provider 信息说明 Manifest 注册成功。如果什么都没有检查android:name路径和authorities是否写对。第二步直接用 adb 操作 provider绕过 UI 验证 CRUD# 查询 adb shell content query --uri content://com.example.myapp.provider/users # 插入 adb shell content insert --uri content://com.example.myapp.provider/users \ --bind name:s:张三 --bind email:s:zhangsantest.com # 更新 adb shell content update --uri content://com.example.myapp.provider/users/1 \ --bind name:s:李四 # 删除 adb shell content delete --uri content://com.example.myapp.provider/users/1content query能返回行数据说明 provider 的 query 和 UriMatcher 都正常。如果报Unknown URI回到第 2 节检查 authority 和 path。第三步验证 ContentObserver。在 Activity 里注册观察者后用 adb 插入一条数据观察 Logcatadb logcat -s Observer如果看到数据变化: content://com.example.myapp.provider/users/2说明notifyChange和onChange链路打通。如果没反应检查 provider 的 insert 里有没有调用notifyChange以及 query 里有没有setNotificationUri。实测下来最容易断的是setNotificationUri这一步——很多人只写了notifyChange但 query 返回的 Cursor 没绑定通知 Uri观察者就收不到。这两个要成对出现。5. 本篇常见错排查报错一IllegalArgumentException: Unknown URI九成是 authority 或 path 不匹配。先对比 Manifest 的android:authorities和代码里的AUTHORITY常量再检查 UriMatcher 注册的 path 和实际请求的 path。带 id 的规则users/#要放在users后面。报错二SecurityException: Permission Denial跨应用访问时权限没配对。检查 provider 的readPermission/writePermission和调用方 Manifest 里的uses-permission是否一致。调试阶段可以先去掉权限、exported设 true 确认链路再逐步加回。报错三Cursor 返回空但数据库有数据常见于query里selection拼接错误或者projection列名写错。用adb shell content query直接查一次排除是 provider 问题还是 client 问题。另外确认onCreate里数据库真的创建成功了。报错四ContentObserver 不回调三个检查点provider 的 insert/update/delete 有没有notifyChangequery 返回的 Cursor 有没有setNotificationUri观察者注册的 Uri 和 notify 的 Uri 是否匹配。三者缺一不可。报错五跨应用访问时exported相关报错Android 12 以后声明了 intent-filter 的组件必须显式设置android:exported。provider 如果没设安装时可能直接失败。跨应用共享必须设 true仅内部使用设 false。如果你在排查过程中想让 AI 帮你分析 Logcat 或补全某段 provider 代码可以用 TaoToken API Keys 配好密钥把报错贴进去让它给排查思路。接入细节可以对照 TaoToken 接入文档 走一遍。6. 把这条链路沉淀成可复用的模板ContentProvider 这套东西配一次通一次之后就是复制粘贴改 authority 和表名。建议你把第 3 节的 Manifest 片段、provider 骨架、ContentResolver 封装、ContentObserver 注册注销这四块整理成一个模板工程下次新项目直接改包名和字段就能用。如果你长期在写 Android 或者做 Agent 相关的编码工作需要频繁让模型补全这类样板代码可以看看 TaoToken Coding Plan把 provider、Resolver、Observer 的代码生成和排障交给它自己专注在业务逻辑上。真正跑通链路的关键还是那三步Manifest 对齐 authority、provider 里 notifyChange 和 setNotificationUri 成对、adb 验证 CRUD 和观察者回调。这三步过了ContentProvider 就不再是四大组件里最神秘的那个了。
企业数字化 ERP 产品动态
相关推荐
Windows Server 镜像下载与安装避坑指南:官方渠道、版本选择与校验实战 被问到“Windows Server 镜像去哪下”这个问题,已经不是一次两次了。上个月帮朋友在一台闲置的Dell R730xd上重新搭环境,他想装Windows Server 2016,结果给我发了一个不知名网站下载的压缩版ISO,文件大小才2个多G,我一… · 2026/9/26 14:11:45
软件测试进阶混沌工程:稳定性验证与职业跃迁路径 做软件测试这件事,入行门槛不算高,但天花板却分得很清楚。功能测试做三五年,点来点去就是那几条业务链路,自动化测试做到最后变成了维护脚本的“体力活”,性能测试一年也碰不上几次大型压测。很多测试从业者到了这个阶… · 2026/9/26 14:11:45
Dota 2玩家遥测二分类实战 从行为数据预测高水平玩家概率 这道 Kaggle 题目的核心不在游戏背景,而在行为遥测建模。任务要求根据 Dota 2 对局中的操作与交互数据,预测玩家属于高水平类别的概率,评估标准采用 AUC,重点考察排序能力而非固定阈值下的分类结果。
从技术实践看,这类题目非常适合用来训练结构化数据项目的完整闭环能力… · 2026/9/26 15:59:21
交易流水多标签分类实战 用 Kaggle 预测客户未来一周品类购买概率 这道 Kaggle 竞赛的价值,不在于做一次普通二分类练习,而在于把一年期交易流水还原成真实可用的客户意图预测任务。目标是针对 8 个商品类别,判断客户在未来 7 天内发生购买的概率,本质上属于零售金融场景下的多标签分类问题。
这类题目很适合作为从数据分析迈向机器学习建… · 2026/9/26 15:59:21
银行交易年龄分组预测实战 从交易流水到客户画像分类建模 这道 Kaggle 赛题聚焦银行客户年龄分组预测,输入并不是现成用户特征,而是近两千万条交易流水。真正的建模对象并非单笔消费,而是客户长期行为在金额、频次、品类和时间上的组合模式。
这类任务很接近真实金融与零售运营场景。年龄段标签看似简单,背后考验的是如何把明细表… · 2026/9/26 15:59:21
用 Kaggle 币价时序回归项目入门价格预测实战 CiVilium Price Prediction 是一道很适合做时序建模入门的 Kaggle 练习题。数据字段极少,只有时间戳与成交量,目标却是预测高频交易窗口下的加权价格,这种设定能够把注意力集中到任务理解、时间验证、特征工程和误差控制这些真正影响结果的核心环节。
这类题目的价值不只在… · 2026/9/26 15:59:21
高尔夫目标检测实战解析 从 Kaggle 赛题到视觉教练原型 BoolArt Golf Detection challenge 是一道典型的高尔夫场景目标检测任务,核心目标不是识别图像属于哪一类,而是在画面中准确找出目标位置并输出边界框结果。题目采用 YOLO 风格标注,评价指标围绕 mAP 展开,适合用来系统练习检测数据解析、训练验证和提交构建。
这类赛题的… · 2026/9/26 15:59:21
STM32裸机C++开发:从重载new到std::function中断封装 1. 这不是C教程,是嵌入式工程师的“手写代码”生存实录“看了三篇了,一行都没让我写呢”——这句话我第一次在STM32学习群看到时,手里的开发板差点掉进茶杯里。它不是吐槽,是精准的病理诊断:当前绝大多数所谓“STM32C”… · 2026/9/26 15:59:15
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46