【我的安卓第一课】四大组件之一 ContentProvider

134 阅读7分钟

引言

什么是ContentProvider

ContentProvider是 Android 提供的专门用于应用间访问数据的机制

它是 Android 四大组件之一,专门设计用来提供结构化数据的共享,并支持复杂的数据类型和查询操作。通过 ContentProvider,开发者可以定义一组统一的接口来访问应用程序内部的数据,同时也能够安全地控制哪些数据可以被其他应用访问。

例如,微信、QQ 等社交 App 经常需要访问系统相册中的图片,或者读取联系人信息。这些功能的背后,正是 ContentProvider 在发挥作用。

主要功能

  1. 数据共享:允许不同的应用之间安全地交换数据。例如,一个图像编辑应用可能需要访问存储在另一个应用中的图片资源。
  2. 数据抽象:为数据源提供了一层抽象,使得数据的存储方式对使用者透明。这意味着你可以改变底层数据存储的方式,而不需要修改使用这些数据的应用代码。
  3. 权限控制:提供了细粒度的权限管理,可以精确地控制哪些应用或哪些操作可以访问特定的数据集,例如,控制应用可访问的图片数量。
  4. 数据操作:支持基本的 CRUD 操作,Android 在这项功能上提供了十足的抽象,让我们可以想操作数据库一样操作其他应用的数据,并且可以通过自定义 URI 来执行更复杂的查询。

与广播的区别

功能ContentProviderBroadcastReceiver
主要用途数据共享与访问消息通知与广播
数据操作支持增删改查仅用于发送/接收事件
访问方式通过 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)

image.png

查询在结束后,会返回一个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);

不过对联系人的添加就比较复杂了,安卓对联系人的数据分为三级

  1. Contact(逻辑联系人):表示一个完整的联系人,可能合并了多个数据源的信息。
  2. RawContact(原始联系人):来自单个数据源的联系人数据(如本地存储、Google 账户等)。
  3. 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:readPermissionandroid: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.table1vnd.android.cursor.item/vnd.com.example.app.provider.table1