16、ContentProvider:跨进程数据共享、自定义ContentProvider、访问系统通讯录
说到 ContentProvider,我个人的理解是——它是 Android 四大组件里最「低调」的一个。Activity、Service、BroadcastReceiver 大家天天打交道,但 ContentProvider 呢?很多人只在面试题里见过它。其实,它是跨进程数据共享的基石。
说白了,ContentProvider 就是给别的 App 提供数据的接口。你想想看,你的 App 里有一堆联系人、照片、或者用户配置,别的 App 想读怎么办?直接读数据库?不安全。暴露文件路径?太粗暴。ContentProvider 就是那个「中间人」,它帮你把数据封装好,只暴露你允许的操作。
为什么需要 ContentProvider?
我记得刚入行那会儿,有个需求是要让两个 App 共享用户登录状态。我当时想:简单,写个 SharedPreferences,让另一个 App 直接读不就行了?结果被 Leader 骂了一顿——跨进程读 SharedPreferences 根本不可靠,而且权限控制一塌糊涂。
ContentProvider 解决了三个核心问题:
- 统一的数据访问接口:不管底层是 SQLite、文件、还是网络,对外都暴露 CRUD 操作。
- 细粒度的权限控制:可以精确到读、写、甚至某张表的某几列。
- 跨进程安全:通过 Binder 机制,数据不会因为进程崩溃而丢失。
核心要点:ContentProvider 不是「数据库」,它是「数据访问层」。底层可以是任何存储方式。
自定义 ContentProvider 实战
我们来写一个最简单的自定义 ContentProvider。假设我们有一个 App,里面存了用户的笔记数据,想让其他 App 也能读取。
第一步,继承 ContentProvider 类:
public class NoteProvider extends ContentProvider {
private static final String AUTHORITY = "com.example.notes.provider";
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
private static final int NOTES = 1;
private static final int NOTE_ID = 2;
static {
URI_MATCHER.addURI(AUTHORITY, "notes", NOTES);
URI_MATCHER.addURI(AUTHORITY, "notes/#", NOTE_ID);
}
private SQLiteDatabase db;
@Override
public boolean onCreate() {
// 初始化数据库,这里用简化写法
NoteDbHelper helper = new NoteDbHelper(getContext());
db = helper.getWritableDatabase();
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
switch (URI_MATCHER.match(uri)) {
case NOTES:
return db.query("notes", projection, selection, selectionArgs, null, null, sortOrder);
case NOTE_ID:
String id = uri.getLastPathSegment();
return db.query("notes", projection, "_id=?", new String[]{id}, null, null, null);
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
// 省略 insert、update、delete 方法,结构类似
}
第二步,在 AndroidManifest.xml 中注册:
<provider
android:name=".NoteProvider"
android:authorities="com.example.notes.provider"
android:exported="true"
android:readPermission="com.example.notes.READ"
android:writePermission="com.example.notes.WRITE" />
小技巧:我习惯把 authority 定义成常量,放在一个单独的 Contract 类里。这样 Provider 和客户端都引用同一个常量,不容易写错。
访问系统通讯录
系统通讯录是 ContentProvider 最经典的应用场景。我记得有一次做社交 App,需要读取用户的联系人列表来推荐好友。当时踩了不少坑,这里分享给你。
读取通讯录的核心代码:
// 查询联系人
Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
null, null, null, null
);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(
cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)
);
String contactId = cursor.getString(
cursor.getColumnIndex(ContactsContract.Contacts._ID)
);
// 查询该联系人的电话号码
Cursor phoneCursor = getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
new String[]{contactId},
null
);
if (phoneCursor != null) {
while (phoneCursor.moveToNext()) {
String phone = phoneCursor.getString(
phoneCursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.NUMBER
)
);
// 处理数据...
}
phoneCursor.close();
}
}
cursor.close();
}
注意:Android 6.0 以上需要动态申请 READ_CONTACTS 权限。我曾经在线上遇到过用户反馈「读取不到联系人」,排查了半天发现是权限弹窗被用户拒绝了,但代码没有处理拒绝后的逻辑。
ContentProvider 的核心流程
为了让你更直观地理解 ContentProvider 的工作机制,我画了一张图:
ContentProvider 的权限模型
权限控制是 ContentProvider 里最容易出问题的地方。我见过不少 App 因为权限配置不当,导致数据泄露或者功能异常。
| 权限类型 | 配置方式 | 说明 |
|---|---|---|
| 读权限 | android:readPermission | 客户端需要声明该权限才能执行 query 操作 |
| 写权限 | android:writePermission | 客户端需要声明该权限才能执行 insert/update/delete |
| 临时权限 | android:grantUriPermissions | 通过 Intent 临时授权,适合一次性访问 |
| 路径权限 | <path-permission> | 对不同的 URI 路径设置不同的权限 |
经验之谈:如果你只是想让自己的 App 内部使用 ContentProvider,可以把 exported 设为 false。这样其他 App 就无法访问了,省去权限管理的麻烦。
ContentObserver:监听数据变化
ContentProvider 还有一个好搭档——ContentObserver。它可以监听数据的变化,一旦数据变了,立刻通知你。
举个例子,你的 App 显示通讯录列表,如果用户在系统设置里新增了一个联系人,你希望列表自动刷新。这时候 ContentObserver 就派上用场了:
getContentResolver().registerContentObserver(
ContactsContract.Contacts.CONTENT_URI,
true, // 是否监听子路径
new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
// 数据变了,刷新列表
loadContacts();
}
}
);
嗯,这里要注意一点:onChange 回调是在 Binder 线程池里执行的,不能直接更新 UI。记得 post 到主线程。
避坑指南
最后,分享几个我踩过的坑:
- Cursor 没有关闭:我曾经有一个功能,每次打开页面都查询通讯录,但忘了关闭 Cursor。结果用户反馈 App 越来越卡,最后 OOM 了。记住:Cursor 用完后一定要 close,或者用 try-with-resources。
- URI 匹配错误:UriMatcher 的匹配规则是从上到下匹配的。如果你把 NOTES 和 NOTE_ID 的顺序写反了,NOTE_ID 永远匹配不到。我因为这个 bug 排查了整整一个下午。
- 权限声明遗漏:在 AndroidManifest 里声明了权限,但忘了在代码里动态申请。结果 6.0 以上的设备直接崩溃。现在我已经养成了习惯:所有危险权限都走动态申请流程。
ContentProvider 虽然看起来有点「重」,但它是 Android 跨进程数据共享的标准方案。掌握了它,你就能在多个 App 之间安全、高效地传递数据了。
公众号:蓝海资料掘金营,微信deep3321