引言
什么是ContentProvider
ContentProvider是 Android 提供的专门用于应用间访问数据的机制。
它是 Android 四大组件之一,专门设计用来提供结构化数据的共享,并支持复杂的数据类型和查询操作。通过 ContentProvider,开发者可以定义一组统一的接口来访问应用程序内部的数据,同时也能够安全地控制哪些数据可以被其他应用访问。
例如,微信、QQ 等社交 App 经常需要访问系统相册中的图片,或者读取联系人信息。这些功能的背后,正是 ContentProvider 在发挥作用。
主要功能
- 数据共享:允许不同的应用之间安全地交换数据。例如,一个图像编辑应用可能需要访问存储在另一个应用中的图片资源。
- 数据抽象:为数据源提供了一层抽象,使得数据的存储方式对使用者透明。这意味着你可以改变底层数据存储的方式,而不需要修改使用这些数据的应用代码。
- 权限控制:提供了细粒度的权限管理,可以精确地控制哪些应用或哪些操作可以访问特定的数据集,例如,控制应用可访问的图片数量。
- 数据操作:支持基本的 CRUD 操作,Android 在这项功能上提供了十足的抽象,让我们可以想操作数据库一样操作其他应用的数据,并且可以通过自定义 URI 来执行更复杂的查询。
与广播的区别
| 功能 | ContentProvider | BroadcastReceiver |
|---|---|---|
| 主要用途 | 数据共享与访问 | 消息通知与广播 |
| 数据操作 | 支持增删改查 | 仅用于发送/接收事件 |
| 访问方式 | 通过 URI | 通过 Intent |
| 安全性 | 支持权限控制 | 权限控制较弱 |
- 如果需要让别的应用使用你的数据,用 ContentProvider;
- 如果需要让别的应用感知某事的发生,用 BroadcastReceiver。
使用
无论是获取其他 App 数据还是向其他 App 提供数据,都需要使用Uri。Android 系统在这里的Uri格式为content://authority/path。其中path为具体的资源名,authority主要用于区分不同应用,作用类似于URL里的域名。这里通常取packageName.provider。
一个Uri的例子:content://com.example.app.provider/table。
获取其他APP数据
当需要使用其他APP的数据时,需要使用 ContentResolver 对象来访问其他应用提供的 ContentProvider。当然,应用需要提前获知所需的 URI 以及权限。
可以认为Resolver就是访问器,只需通过方法指定对应的uri就可以执行对应的操作
查询
private void getContacts() {
mCancellationSignal = new CancellationSignal();
// 注册取消监听器
mCancellationSignal.setOnCancelListener(() -> {
Log.d(TAG, "查询已被取消");
});
// 获取 ContentResolver
ContentResolver resolver = getContentResolver();
Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
try {
Cursor cursor = resolver.query(uri, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
// 获取联系人姓名和电话号码
String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
Log.d("Contact", "Name: " + name + ", Number: " + number);
}
cursor.close();
}
} catch (Exception e) {
if (e instanceof android.os.OperationCanceledException) {
Log.d(TAG, "查询被用户取消");
} else {
Log.e(TAG, "查询出错: " + e.getMessage());
}
}
}
public void cancelQuery() {
if (mCancellationSignal != null && !mCancellationSignal.isCanceled()) {
mCancellationSignal.cancel();
}
}
其中,query的Api为:
/**
* 基于URI,返回对应结果。
* 参数:
* uri – 被查找的目标,用于检索内容。from table
* projection – 要返回那些列。 select A, B, C.
* selection – 声明查询条件,格式为 WHERE 子句。传递 null 表示不对数据进行筛选。 wehere name = ?
* selectionArgs – 对应于 SQL 的预处理,用于对 Selection 中的 ? 进行替换 ? -> "john"
* sortOrder – 对行进行排序,格式为 ORDER BY 子句。传递 null 代表不排序。
* cancellationSignal - 用于中途取消操作,防止资源浪费。用于异步操作。
* 返回:
* Cursor,位于第一个item。如果内容提供程序返回 null,或者呈现崩溃,则可能会返回null
*/
public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder,
@Nullable CancellationSignal cancellationSignal)
public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder)
查询在结束后,会返回一个Cursor。如果熟悉JDBC编程的小伙伴,应该对Cursor非常熟练。它们的操作是类似的。
添加
添加类似于查询,同时也类似于数据库操作。
public void insertData(ContentResolver contentResolver, Uri uri) {
// 创建ContentValues对象并添加数据
ContentValues values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
// 执行插入操作
contentResolver.insert(uri, values);
}
上面的代码类似于INSERT INTO table_name (column1, column2) VALUES ('text', 1);。
不过对联系人的添加就比较复杂了,安卓对联系人的数据分为三级
- Contact(逻辑联系人):表示一个完整的联系人,可能合并了多个数据源的信息。
- RawContact(原始联系人):来自单个数据源的联系人数据(如本地存储、Google 账户等)。
- Data(数据项):具体的数据记录,如姓名、电话、邮箱等。
在添加联系人时,必须先创建RawContact,然后基于返回的id来填充数据。
private void addContact() {
ContentResolver resolver = getContentResolver();
ContentValues values = new ContentValues();
// 1. 创建 RawContact(必须先创建)
values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, (String) null);
values.put(ContactsContract.RawContacts.ACCOUNT_NAME, (String) null);
Uri rawContactUri = resolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
assert rawContactUri != null;
long rawContactId = ContentUris.parseId(rawContactUri);
// 2. 添加姓名数据
values.clear();
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "New Contact");
resolver.insert(ContactsContract.Data.CONTENT_URI, values);
// 3. 添加电话号码数据
values.clear();
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, "1234567890");
values.put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
resolver.insert(ContactsContract.Data.CONTENT_URI, values);
}
修改
public void updateData(ContentResolver contentResolver, Uri uri) {
// 创建 ContentValues 对象并添加数据
ContentValues values = new ContentValues();
values.put("column1", "");
// 设置 WHERE 子句条件和参数
String selection = "column1 = ? and column2 = ?";
String[] selectionArgs = new String[]{"text", "1"};
// 执行更新操作
contentResolver.update(uri, values, selection, selectionArgs);
}
上面的代码类似于UPDATE table_name SET column1 = '', column3 = 'new' WHERE column1 = 'text' AND column2 = '1';。
删除
public void deleteData(ContentResolver contentResolver, Uri uri) {
// 设置 WHERE 子句条件和参数
String selection = "column2 = ?";
String[] selectionArgs = new String[]{"1"};
// 执行删除操作
contentResolver.delete(uri, selection, selectionArgs);
}
上面的代码类似于DELETE FROM uri WHERE column2 = '1';
APP向外提供数据
如果希望自己的应用数据可以被其他应用访问,就需要自定义一个ContentProvider。
创建类
创建一个继承自 ContentProvider 基类,然后重写其中的关键方法。并设置对应的匹配器
public class MyBookProvider extends ContentProvider {
// 数据库帮助类(如使用 SQLiteOpenHelper)
private MyDatabaseHelper dbHelper;
@Override
public boolean onCreate() {
// 初始化数据库等操作
dbHelper = new MyDatabaseHelper(getContext());
return true;
}
// 必须实现的方法:query, insert, update, delete, getType
}
实现Uri匹配器
根据前面的内容可以指定,uri的内容结构比较灵活。可以使用*,#来作为通配符。为了方便我们传入的uri。Android 提供了 UriMather来简化开发
private static final int BOOKS = 100;
private static final int BOOK_ID = 101;
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
// 定义 authority 和 path 匹配规则;
// authority+path为key -> 右侧int为value
sUriMatcher.addURI("com.example.provider", "books", BOOKS);
sUriMatcher.addURI("com.example.provider", "books/#", BOOK_ID);
}
实现关键方法
| 方法 | 作用 |
|---|---|
| onCreate() | 初始化组件,通常用于打开或创建数据库 |
| query() | 查询数据 |
| insert() | 插入数据 |
| update() | 更新数据 |
| delete() | 删除数据 |
| getType() | 返回指定 URI 对应的数据类型(MIME 类型) |
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor;
switch (sUriMatcher.match(uri)) {
case BOOKS:
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case BOOK_ID:
String id = uri.getLastPathSegment();
cursor = db.query("Book", projection, "_id=?", new String[]{id}, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
// 注册监听器以通知 resolver 数据变化
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
if (sUriMatcher.match(uri) != BOOKS) {
throw new IllegalArgumentException("Insertion is not supported for this URI: " + uri);
}
SQLiteDatabase db = dbHelper.getWritableDatabase();
long id = db.insert("Book", null, values);
if (id != -1) {
// 数据发生变化,通知观察者
getContext().getContentResolver().notifyChange(uri, null);
return Uri.withAppendedPath(uri, String.valueOf(id));
}
return null;
}
// ... ...
注册 Provider
在 AndroidManifest.xml 中注册 Provider;以便 Android 进行相关处理
<provider
android:name=".MyBookProvider"
android:authorities="com.example.provider"
android:exported="true"
android:readPermission="com.example.permission.READ_BOOK"
android:writePermission="com.example.permission.WRITE_BOOK" />
android:authorities:唯一标识符,其他应用通过这个 authority 来访问你的 Provider。android:exported:是否允许其他应用访问。android:readPermission/writePermission:限制外部访问权限(可选)。
添加权限声明
如果希望限制其他应用对数据的访问权限,可以在 AndroidManifest.xml 中定义权限:
<permission
android:name="com.example.permission.READ_BOOK"
android:protectionLevel="signature" />
<permission
android:name="com.example.permission.WRITE_BOOK"
android:protectionLevel="signature" />
如果权限是自定义权限,则必须与上面的android:readPermission与android:writePermission同时设置。如果使用系统预定义权限,只需后者。
其他应用如果想访问你的 ContentProvider,需要在它们的 AndroidManifest.xml 中添加权限请求:
<uses-permission android:name="com.example.permission.READ_BOOK" />
<uses-permission android:name="com.example.permission.WRITE_BOOK" />
getType
该方法要求返回 URI 所对应的MIME字符串。其主要由3部分组成,Android 对这3个部分做了如下格式规定:
- 必须以
vnd开头。 - 如果内容URI以路径结尾,则后接
android.cursor.dir/;如果内容URI以id结尾,则后接android.cursor.item/。 - 最后接上
vnd.\<authority>.\<path>。
例如,vnd.android.cursor.dir/vnd.com.example.app.provider.table1与vnd.android.cursor.item/vnd.com.example.app.provider.table1