Android 数据共享标准:ContentProvider 简介

本文参考文献:《疯狂Android讲义 : 第2版

ContentProvider 是不同应用程序之间进行数据交换的标准 API,ContentProvider 以某种 Uri 的形式对外提供数据,允许其他应用访问或修改数据;其他应用程序使用 ContentResolver 根据 Uri 去访问操作指定数据。

完整地开发一个 ContentProvider 的步骤:

  1. 定义自己的 ContentProvider 类,该类需要继承 Android 提供的 ContentProvider 基类;
  2. 在 AndroidManifest.xml 文件中注册这个 ContentProvider,在注册时绑定一个 Uri。

Uri 简介

Uri 可以分为如下三个部分:

  1. content:// —— 这个部分是 Android 的 ContentProvider 规定的,就像上网的协议默认是 http:// 一样。暴露 ContentProvider、访问 ContentProvider 的协议默认是 content://;
  2. testContentProvider.toby.person —— 这个部分就是 content:// 的 authority。系统就是由这个部分来找到操作哪个 ContentProvider。只要访问哪个指定的 ContentProvider,这个部分总是固定的;
  3. words —— 资源(数据)部分,当访问者需要访问不同资源时,这个部分是动态改变的。
content://testContentProvider.toby.person/word/2

此时它要访问的资源为 word/2,这意味着访问 word 数据中 ID 为 2 的数据。

content://testContentProvider.toby.person/word/2/word

此时它要访问的资源为 word/2,这意味着访问 word 数据中 ID 为 2 的数据的word字段。

ContentProvider、ContentResolver、Uri 的关系

图片来源 http-_www.jianshu.com_p_c6c52c3ba66e

从图中可以看出,ContentResolver 可以实现”间接调用“ ContentProvider 的 CRUD 方法:

  1. 当 A 应用调用 ContentResolver 的 insert() 方法时,实际上相当于调用了该 Uri 对应的 ContentProvider 的 insert() 方法;
  2. 当 A 应用调用 ContentResolver 的 update() 方法时,实际上相当于调用了该 Uri 对应的 ContentProvider 的 update() 方法;
  3. 当 A 应用调用 ContentResolver 的 delete() 方法时,实际上相当于调用了该 Uri 对应的 ContentProvider 的 delete() 方法;
  4. 当A应用调用 ContentResolver 的 query() 方法时,实际上相当于调用了该 Uri 对应的 ContentProvider 的 query() 方法。

开发 ContentProvider 子类

开发 ContentProvider 只要如下两步:

  1. 开发一个 ContentProvider 子类,该子类需要实现 query()、insert()、update() 和 delete() 等方法;
  2. 在AndroidManifest.xml 文件中注册该 ContentProvider,指定 android:authorities 属性。

配置 ContentProvider

只要为 <applicaton.../> 元素添加了 <provider.../> 子元素即可配置 ContentProvider。例如如下的配置片段:

<provider
        android:name=".FirstProvider"
        android:authorities="com.toby.personal.testlistview.FirstProvider"
        android:exported="true"/>

下面是一个简单的使用示例,示例01:
示例01,主布局文件的内容:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/container"
    android:background="@color/colorGray"
    android:orientation="vertical"
    >

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/query"
        android:onClick="query"
        />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/insert"
        android:onClick="insert"
        />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/update"
        android:onClick="update"
        />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/delete"
        android:onClick="delete"
        />

</LinearLayout>

示例01 的 FirstProvider.java 文件的内容:

package com.toby.personal.testlistview;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

/**
 * Created by toby on 2017/4/23.
 */

public class FirstProvider extends ContentProvider {

    final private static String TAG = "Toby_Provider";

    @Override
    public boolean onCreate() {
        Log.d(TAG, "========= onCreate is called =========");
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        Log.d(TAG, "========= query is called selection =========> " + selection);
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        Log.d(TAG, "========= insert is called values =========> " + values);
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        Log.d(TAG, "========= delete is called selection =========> " + selection);
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        Log.d(TAG, "========= update is called selection =========> " + selection);
        return 0;
    }
}

我们需要在 AndroidManifest.xml 中的 application 节点下加入如下代码:

<provider
            android:authorities="com.toby.personal.testlistview.FirstProvider"
            android:name=".FirstProvider"
            android:exported="true"
            />

最后是主程序文件的代码:

package com.toby.personal.testlistview;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    final private static String TAG = "Toby_Test";

    ContentResolver contentResolver;
    Uri uri = Uri.parse("content://com.toby.personal.testlistview.FirstProvider/");

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        contentResolver = getContentResolver();
    }

    public void query(View source) {
        Cursor c = contentResolver.query(uri, null, "query_where", null, null);
        Toast.makeText(this, "远端 ContentProvider 返回的 Cursor 为:" + c,
                Toast.LENGTH_LONG).show();
    }

    public void insert(View source) {
        ContentValues values = new ContentValues();
        values.put("name", "jsdhfkjsjh");
        Uri newUri = contentResolver.insert(uri, values);
        Toast.makeText(this, "远端 ContentProvider 新插入记录的 Uri 为:" + newUri,
                Toast.LENGTH_LONG).show();
    }

    public void update(View source) {
        ContentValues values = new ContentValues();
        values.put("name", "jsdhfkjsjh");
        int count = contentResolver.update(uri, values, "update_where", null);
        Toast.makeText(this, "远端 ContentProvider 更新记录数为:" + count,
                Toast.LENGTH_LONG).show();
    }

    public void delete(View source) {
        int count = contentResolver.delete(uri, "update_where", null);
        Toast.makeText(this, "远端 ContentProvider 删除记录数为:" + count,
                Toast.LENGTH_LONG).show();
    }

}

该示例运行之后,控制台的 log 监控效果:

显示效果

配置ContentProvider时通常指定如下属性:

  1. name:指定该ContentProvider的实现类的类名。
  2. authorities:指定该ContentProvider对应的Uri(相当于为该ContentProvider分配一个域名。)
  3. android:exported:指定该ContentProvider是否允许其他应用调用。如果将该属性设为false,那么该ContentProvider将不允许其他应用调用。

为了确定ContentProvider实际能处理的Uri,以及确定每个方法中Uri参数所操作的数据,Android系统提供了UriMatcher工具类,主要提供了如下两个方法:

  1. void addURI(String authority ,String path ,int code):该方法用于向UriMatcher对象注册Uri。其中authority和path组合成一个Uri,而code则代表该Uri对应的标识码。
  2. int match(Uri uri):根据前面注册的Uri来判断指定Uri对应的标识码。如果找不到匹配的标识码,就会返回-1。

例如:

UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.toby.personal.testlistview.FirstProvider", "words", 1);
uriMatcher.addURI("com.toby.personal.testlistview.FirstProvider", "word/#", 2);

其中 # 为通配符,这意味着如下匹配结果:

uriMatcher.match(Uri.parse("content://com.toby.personal.testlistview.FirstProvider/words"));
// 返回标识码 1
uriMatcher.match(Uri.parse("content://com.toby.personal.testlistview.FirstProvider/word/2"));
// 返回标识码 2
uriMatcher.match(Uri.parse("content://com.toby.personal.testlistview.FirstProvider/word/10"));
// 返回标识码 2

至于到底需要为 UriMatcher 注册多少个 Uri,取决于系统的业务需求。
对于 content://com.toby.personal.testlistview.FirstProvider/words 这个 Uri,它的资源部分为 words,这种资源通常代表了访问所有数据项;对于 content://com.toby.personal.testlistview.FirstProvider/word/2 这个 Uri,它的资源部分通常代表访问指定数据项,其中最后一个数值往往代表了该数据的 ID。

除此之外,Android 还提供了一个 ContentUris 工具类,它是一个操作 Uri 字符串的工具类,提供了如下两个工具方法:

  1. withAppendedId(uri , id):用于为路径加上ID部分,例如:
Uri uri = Uri.parse("content://com.toby.personal.testlistview.FirstProvider/word");
Uri resultUri = ContentUris.withAppendedId(uri, 2);
// 生成后的 Uri 为 "content://com.toby.personal.testlistview.FirstProvider/word/2"
  1. parseId(uri):用于从指定Uri中解析出所包含的ID值,例如:
Uri uri = Uri.parse("content://com.toby.personal.testlistview.FirstProvider/word/2");
long wordId = ContentUris.parseId(uri); // 获取的结果为:2

操作系统的 ContentProvider

Android系统本身提供了大量的ContentProvider,使用ContentResolver操作系统的ContentProvider数据的步骤也是两步:

  1. 调用Context的getContentResolver()获取ContentResolver对象;
  2. 根据需要调用ContentResolver的insert()、delete()、update()和query()方法操作数据。

Android系统用于管理联系人的ContentProvider的几个Uri如下:

  1. ContactsContract.Contacts.CONTENT_URI:管理联系人的Uri;
  2. ContactsContract.CommonDataKinds.Phone.CONTENT_URI:管理联系人的电话的Uri;
  3. ContactsContract.CommonDataKinds.Email.CONTENT_URI:管理联系人的E-mail的Uri。

Android为多媒体提供的ContentProvider的Uri如下所示:

  1. MediaStore.Audio.Media.EXTERNAL_CONTENT_URI:存储在外部存储其上的音频文件内容的ContentProvider的Uri;
  2. MediaStore.Audio.Media.INTERNAL_CONTENT_URI:存储在手机内部存储器上的音频文件内容的ContentProvider的Uri;
  3. MediaStore.Images.Media.EXTERNAL_CONTENT_URI:存储在外部存储器上的图片文件内容的ContentProvider的Uri;
  4. MediaStore.Images.Audio.Media.INTERNAL_CONTENT_URI:存储在手机内部存储器上的图片文件内容的ContentProvider的Uri;
  5. MediaStore.Video.Media.EXTERNAL_CONTENT_URI:存储在外部存储器上的视频文件内容的ContentProvider的Uri;
  6. MediaStore.Video.Audio.Media.INTERNAL_CONTENT_URI:存储在手机内部存储器上的视频文件内容的ContentProvider的Uri。

监听 ContentProvider 的数据改变

在之前的介绍中,只要导致了 ContentProvider 数据发生了改变,程序中就调用如下代码:

getContext().getContentResolver(),notifyChange(uri ,null);

为了在应用程序中监听ContentProvider数据的改变,需要利用Android提供的ContentObserver基类。监听ContentProvider数据改变的监听器需要继承ContentObserver类,并重写该基类所定义的onChange(boolean selfChange)方法--当所监听的ContentProvider数据发生改变时,该onChange()方法将会被触发。

为了监听指定ContentProvider的数据变化,需要通过ContentResolver向指定Uri注册ContentObserver监听器。ContentResolver提供了如下方法来注册监听器:

registerContentObserver(Uri uri , boolean notifyForDescendents , ContentObserver observer)

这个方法的三个参数分别表示:

  1. uri —— 该监听器所监听的ContentProvider的Uri。
  2. notifyForDescendents —— 如果该参数设为true,假如注册监听的Uri为content://abc,nameUri为contetn://abc/xyzcontent://abc/xyz/foo的数据改变时也会触发该监听器;如果设为false,那么只有content://abc的数据发生改变时才会触发该监听器。
  3. observer —— 监听器实例。

提供程序访问的替代形式

提供程序访问的三种替代形式在应用开发的过程中十分重要:

  1. 批量访问:可以通过 ContentProviderOperation 类中的方法创建一批访问调用,然后通过 ContentResolver.applyBatch() 执行它们。
  2. 异步查询:应该在单独线程中执行查询。
  3. 通过 Intent 访问数据:尽管无法直接向提供程序发送 Intent,但是可以向提供程序的应用发送 Intent,后者通常具有修改提供程序数据的最佳配置。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容