25、ContentProvider:跨进程数据共享,读取系统联系人、自定义ContentProvider
ContentProvider,说白了就是Android给不同App之间准备的“数据管道”。
我刚开始做Android开发时,总觉得这玩意儿有点多余。自己App的数据自己用不就行了?直到有一次我需要读取系统通讯录,才发现没有ContentProvider,你根本拿不到别的App的数据。嗯,这就是它的核心价值——跨进程数据共享。
为什么需要ContentProvider?
Android的每个App都运行在自己的进程里,有自己的沙箱。正常情况下,App A访问不了App B的数据库文件。那系统通讯录、短信、相册这些数据,怎么让第三方App读取呢?
ContentProvider就是官方提供的标准方案。它把数据暴露出来,其他App通过URI去访问。你想想看,如果没有这个机制,每个App都自己去读别的App的数据库文件,那安全性和稳定性就全乱套了。
核心要点:ContentProvider是Android四大组件之一,用于跨进程数据共享。它提供了一套统一的接口,让数据访问变得标准化、安全化。
ContentProvider的工作原理
我画了一张图,帮你理解ContentProvider在整个系统中的位置:
流程其实很简单:App A通过ContentResolver发起请求,系统根据URI找到对应的ContentProvider,然后调用App B中对应的方法。整个过程对开发者来说,就像在调用本地方法一样。
读取系统联系人
读取系统通讯录,是ContentProvider最经典的应用场景。我在项目中做过好几次联系人同步功能,这里给你一个完整的示例:
// 读取系统联系人
public List<String> readContacts(Context context) {
List<String> contacts = new ArrayList<>();
ContentResolver resolver = context.getContentResolver();
// 系统联系人的URI
Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
// 要查询的字段
String[] projection = new String[]{
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
};
// 查询,返回Cursor
Cursor cursor = resolver.query(uri, projection, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String number = cursor.getString(cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.NUMBER));
contacts.add(name + " : " + number);
}
cursor.close(); // 别忘了关闭Cursor
}
return contacts;
}
注意:读取联系人需要声明权限。Android 6.0以上需要动态申请READ_CONTACTS权限。我曾经遇到过用户拒绝权限后App直接崩溃的情况,所以一定要做权限检查。
权限声明在AndroidManifest.xml中:
<uses-permission android:name="android.permission.READ_CONTACTS" />
动态权限申请代码:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_CODE);
}
自定义ContentProvider
有时候我们需要把自己的数据暴露给其他App。比如我做过一个笔记App,想让其他App能读取笔记内容。这时候就需要自定义ContentProvider。
实现一个ContentProvider,需要继承ContentProvider类,并实现六个方法:
| 方法 | 说明 | 调用时机 |
|---|---|---|
| onCreate() | 初始化Provider | App启动时 |
| query() | 查询数据 | 外部调用query时 |
| insert() | 插入数据 | 外部调用insert时 |
| update() | 更新数据 | 外部调用update时 |
| delete() | 删除数据 | 外部调用delete时 |
| getType() | 返回MIME类型 | 外部调用getType时 |
下面是一个简单的自定义ContentProvider示例:
public class MyContentProvider extends ContentProvider {
private static final String AUTHORITY = "com.example.myapp.provider";
private static final String TABLE_NAME = "notes";
// 定义URI匹配器
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
private static final int NOTES_ALL = 1;
private static final int NOTES_ID = 2;
static {
URI_MATCHER.addURI(AUTHORITY, TABLE_NAME, NOTES_ALL);
URI_MATCHER.addURI(AUTHORITY, TABLE_NAME + "/#", NOTES_ID);
}
private SQLiteDatabase db;
@Override
public boolean onCreate() {
// 初始化数据库
MyDatabaseHelper helper = new MyDatabaseHelper(getContext());
db = helper.getWritableDatabase();
return db != null;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Cursor cursor;
switch (URI_MATCHER.match(uri)) {
case NOTES_ALL:
cursor = db.query(TABLE_NAME, projection, selection,
selectionArgs, null, null, sortOrder);
break;
case NOTES_ID:
String id = uri.getLastPathSegment();
cursor = db.query(TABLE_NAME, projection, "_id=?",
new String[]{id}, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
// 设置通知URI,让Cursor能感知数据变化
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
long rowId = db.insert(TABLE_NAME, null, values);
if (rowId > 0) {
Uri newUri = ContentUris.withAppendedId(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 = db.update(TABLE_NAME, values, selection, selectionArgs);
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = db.delete(TABLE_NAME, selection, selectionArgs);
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
@Override
public String getType(Uri uri) {
switch (URI_MATCHER.match(uri)) {
case NOTES_ALL:
return "vnd.android.cursor.dir/vnd.com.example.notes";
case NOTES_ID:
return "vnd.android.cursor.item/vnd.com.example.notes";
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
}
别忘了在AndroidManifest.xml中注册:
<provider
android:name=".MyContentProvider"
android:authorities="com.example.myapp.provider"
android:exported="true" />
个人经验:我建议把exported属性设为true时一定要谨慎。如果只是给自己App的另一个进程用,可以设为false。暴露给外部App时,最好加上权限控制,防止数据被随意访问。
ContentProvider的避坑指南
我在项目中踩过不少ContentProvider的坑,这里分享几个:
- Cursor没关闭导致内存泄漏——我曾经在循环中忘了close Cursor,结果内存越占越多,最后App直接OOM。一定要用try-finally或者try-with-resources确保Cursor被关闭。
- URI匹配错误——UriMatcher的匹配规则很容易写错。我建议把所有的URI常量集中定义,不要散落在代码里。
- 跨进程数据量大时ANR——ContentProvider的方法默认运行在调用方的Binder线程池中。如果查询很耗时,比如一次查几千条数据,很容易触发ANR。我建议大数据量查询时用分页,或者用ContentProvider的异步查询接口。
- 权限声明遗漏——自定义Provider如果忘了声明权限,外部App调用时会直接抛SecurityException。记得在provider标签里加上readPermission和writePermission。
ContentProvider vs 其他方案
你可能会问,跨进程通信不是还有AIDL、BroadcastReceiver、Messenger吗?为什么非要用ContentProvider?
我的看法是:
- 数据共享场景——比如通讯录、媒体库,用ContentProvider最合适。它天然支持CRUD操作,还有数据变化通知机制。
- 方法调用场景——比如让另一个App执行某个计算任务,用AIDL更直接。
- 事件通知场景——比如某个状态变化了,用BroadcastReceiver更轻量。
说白了,ContentProvider是专门为“数据”设计的。如果你的核心需求是共享结构化数据,那就选它准没错。
总结一下:ContentProvider是Android跨进程数据共享的标准方案。读取系统联系人时,用ContentResolver配合ContactsContract即可。自定义ContentProvider时,注意URI匹配、权限控制和Cursor管理。记住,数据变化后要调用notifyChange,这样监听者才能及时收到更新。
公众号:蓝海资料掘金营,微信deep3321