ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

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 的唯一标识通常写成包名.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 就不再是四大组件里最神秘的那个了。
返回列表