news 2026/9/26 14:11:47

Android 四大组件——ContentProvider(内容提供者):从 Uri 到 ContentObserver 的完整配置与验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android 四大组件——ContentProvider(内容提供者):从 Uri 到 ContentObserver 的完整配置与验证

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 的唯一标识,通常写成包名.provider;path 指向具体的数据表或资源;id 是可选的单条记录标识。比如content://com.example.myapp.provider/users/123,authority 是com.example.myapp.provider,path 是users,id 是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、在数据变化时notifyChange;client 端通过 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:authorities="com.example.myapp.provider" android:enabled="true" android:exported="true" android:readPermission="com.example.myapp.permission.READ_USERS" android:writePermission="com.example.myapp.permission.WRITE_USERS" android:grantUriPermissions="true"> <!-- 针对路径的细粒度权限 --> <path-permission android:pathPrefix="/users" android:readPermission="com.example.myapp.permission.READ_USERS" /> <grant-uri-permission android:path="/users/public/*" /> </provider> <!-- 声明自定义权限 --> <permission android:name="com.example.myapp.permission.READ_USERS" android:protectionLevel="normal" /> <permission android:name="com.example.myapp.permission.WRITE_USERS" android:protectionLevel="dangerous" /> </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 List<User> queryAll(Context context) { List<User> 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 是“数据变了通知我”的机制。注册时传入 Uri,provider 端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:zhangsan@test.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/1

content query能返回行数据,说明 provider 的 query 和 UriMatcher 都正常。如果报Unknown URI,回到第 2 节检查 authority 和 path。

第三步,验证 ContentObserver。在 Activity 里注册观察者后,用 adb 插入一条数据,观察 Logcat:

adb 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 有没有notifyChange;query 返回的 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 就不再是四大组件里最神秘的那个了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 14:10:25

微信小程序+MySQL青少年心理健康系统:毕业设计源码与教程

简介&#xff1a;这份资源是面向高校学生与初学者的微信小程序毕业设计完整项目包&#xff0c;以青少年心理健康科普为主题&#xff0c;适合用作毕业设计、课程设计或自学练手。项目前端基于微信小程序&#xff0c;后端采用SpringBoot/SSM框架&#xff0c;数据库为MySQL&#x…

作者头像 李华
网站建设 2026/9/26 14:09:34

主动配电网故障恢复统一模型:重构与孤岛划分的Matlab实现

1. 为什么故障恢复非得把“重构”和“孤岛划分”塞进同一个模型1.1 传统两阶段做法的问题在哪做主动配电网故障恢复的人&#xff0c;几乎都经历过这种纠结&#xff1a;配电网某条馈线跳闸后&#xff0c;一部分负荷失电&#xff0c;手上有分布式电源&#xff08;DG&#xff09;&…

作者头像 李华
网站建设 2026/9/26 14:09:34

Guohua Diffusion实战:从扩散模型参数调优到国画风格批量生成落地

Guohua Diffusion这个词&#xff0c;最近在几个产区的设计圈里被反复提起。起初我以为又是个追着AI风口跑的概念包装&#xff0c;直到自己下场把整套流程跑通&#xff0c;从一线产区的高端定制打样&#xff0c;一路推到二线产区的批量生产规范&#xff0c;才意识到这其实是国画…

作者头像 李华
网站建设 2026/9/26 14:07:43

六自由度机械臂视觉伺服抓取:OpenCV与深度学习全链路实战

简介&#xff1a;这份资源面向工业自动化与智能物流分拣方向的开发者与学习者&#xff0c;提供一套基于视觉伺服控制的六自由度机械臂自主抓取系统方案&#xff0c;覆盖实时图像处理、目标识别、深度学习模型训练与位姿估计、ROS集成及运动规划等关键环节&#xff0c;适合具备P…

作者头像 李华
网站建设 2026/9/26 14:07:39

AI辅助开发实战:从零散代码到可运行项目的五个阶段

1. 从零散需求到可运行原型&#xff1a;AI辅助代码开发的整体思路拆解1.1 业余开发者的真实处境与核心痛点先说清楚这篇内容面向谁。如果你是一个有正职工作、利用晚上和周末写点小工具或者做副业项目的开发者&#xff0c;或者你压根不是科班出身、靠着AI对话工具硬啃代码的爱好…

作者头像 李华
网站建设 2026/9/26 14:06:32

PyTorch实战:DeepLabV3在Cityscapes上的语义分割训练与避坑指南

简介&#xff1a;这份资源面向计算机视觉方向的研究者、算法工程师与深度学习学习者&#xff0c;提供在Cityscapes数据集上训练DeepLabV3语义分割模型的完整PyTorch实现&#xff0c;帮助读者理解ASPP空洞空间金字塔池化与全局上下文模块的设计思路&#xff0c;并掌握从数据预处…

作者头像 李华