[Android]【安卓】Content Provider详解

[Android]【安卓】Content Provider详解

本篇博客已收录到我的安卓开发小结中——点击【安卓开发小结】

参考资料:
《第一行代码》


一、内容提供器简介

内容提供器(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前,使用内容提供器是 Android 实现跨程序共享数据的标准方式。

不同于文件存储和 SharedPreferences 存储中的两种全局可读写操作模式,内容提供器可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。

内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。那么接下来我们就一个一个开始学习吧,首先从使用现有的内容提供器开始。

二、 访问其他程序中的数据

当一个应用程序通过内容提供器对其数据提供了外部访问接口,任何其他的应用程序就都可以对这部分数据进行访问。 Android 系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以充分地利用这部分数据来实现更好的功能。下面我们就来看一看,内容提供器到底是如何使用的。

1 、ContentResolver 的基本用法

对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助ContentResolve 类,可以通过Context 中的 getContentResolver()方法获取到该类的实例。ContentResolver 中提供了一系列的方法用于对数据进行 CRUD 操作,其中 insert()方法用于添加数据, update()方法用于更新数据, delete()方法用于删除数据, query()方法用于查询数据。有没有似曾相识的感觉?没错, SQLiteDatabase 中也是使用的这几个方法来进行 CRUD 操作的,只不过它们在方法参数上稍微有一些区别。

不同于 SQLiteDatabase, ContentResolver 中的增删改查方法都是不接收表名参数的,而是使用一个 Uri 参数代替,这个参数被称为内容 URI。 内容 URI 给内容提供器中的数据建立了唯一标识符,它主要由两部分组成,权限(authority)和路径(path)。权限是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是com.example.app,那么该程序对应的权限就可以命名为com.example.app. provider。路径则是用于对同一应用程序中不同的表做区分的,通常都会添加到权限的后面。比如某个程序的数据库里存在两张表, table1 和 table2,这时就可以将路径分别命名为/table1 和/table2,然后把权限和路径进行组合,内容 URI 就变成了 com.example.app.provider/table1 和 com.example.app.provider/table2。不过,目前还很难辨认出这两个字符串就是两个内容 URI,我们还需要在字符串的头部加上协议声明。因此,内容 URI 最标准的格式写法如下:

content://com.example.app.provider/table1
content://com.example.app.provider/table2

有没有发现,内容 URI 可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据。 也正是因此, ContentResolver 中的增删改查方法才都接收 Uri 对象作为参数,因为使用表名的话系统将无法得知我们期望访问的是哪个应用程序里的表。

在得到了内容 URI 字符串之后,我们还需要将它解析成 Uri 对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:

Uri uri = Uri.parse("content://com.example.app.provider/table1")

只需要调用 Uri.parse()方法,就可以将内容 URI 字符串解析成 Uri 对象了。
现在我们就可以使用这个 Uri 对象来查询 table1 表中的数据了,代码如下所示:

Cursor cursor = getContentResolver().query(
uri,
projection,
selection,
selectionArgs,
sortOrder);

这些参数和 SQLiteDatabase 中 query()方法里的参数很像,但总体来说要简单一些,毕竟这是在访问其他程序中的数据,没必要构建过于复杂的查询语句。下表对使用到的这部分参数进行了详细的解释。
这里写图片描述
查询完成后返回的仍然是一个 Cursor 对象,这时我们就可以将数据从 Cursor 对象中逐个读取出来了。读取的思路仍然是通过移动游标的位置来遍历 Cursor 的所有行,然后再取出每一行中相应列的数据,代码如下所示:

扫描二维码关注公众号,回复: 2807263 查看本文章
if (cursor != null) {
        while (cursor.moveToNext()) {
            String column1 = cursor.getString(cursor.getColumnIndex("column1"));
            int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
        } c
        ursor.close();
    }

掌握了最难的查询操作,剩下的增加、修改、删除操作就更不在话下了。我们先来看看如何向 table1 表中添加一条数据,代码如下所示:

ContentValues values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
getContentResolver().insert(uri, values);

可 以 看 到 , 仍 然 是 将 待 添 加的 数 据 组 装 到 ContentValues 中 , 然 后 调 用ContentResolver 的 insert()方法,将 Uri 和 ContentValues 作为参数传入即可。

现在如果我们想要更新这条新添加的数据,把 column1 的值清空,可以借助ContentResolver 的 update()方法实现,代码如下所示:

ContentValues values = new ContentValues();
values.put("column1", "");
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[]
{"text", "1"});

注意上述代码使用了 selection 和 selectionArgs 参数来对想要更新的数据进行约束,以防止所有的行都会受影响。

最后,可以调用 ContentResolver 的 delete()方法将这条数据删除掉,代码如下所示:

getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });

到这里为止,我们就把 ContentResolver 中的增删改查方法全部学完了。是不是感觉非常简单?因为这些知识早在上一章中学习 SQLiteDatabase 的时候你就已经掌握了,所需特别注意的就只有 uri 这个参数而已。那么接下来,我们就利用目前所学的知识,看一看如何读取系统电话簿中的联系人信息。

2 、读取系统联系人

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <ListView
      android:id="@+id/contacts_view"
      android:layout_width="match_parent"
      android:layout_height="match_parent" >
    </ListView>
</LinearLayout>
public class MainActivity extends Activity {
    ListView contactsView;
    ArrayAdapter<String> adapter;
    List<String> contactsList = new ArrayList<String>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        contactsView = (ListView) findViewById(R.id.contacts_view);
        adapter = new ArrayAdapter<String>(this, android.R.layout. simple_list_item_1,
                contactsList);
        contactsView.setAdapter(adapter);
        readContacts();
    }
    private void readContacts() {
        Cursor cursor = null;
        try {
            // 查询联系人数据
            cursor = getContentResolver().query(
                    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    null, null, null, null);
            while (cursor.moveToNext()) {
                // 获取联系人姓名
                String displayName = cursor.getString(cursor.getColumnIndex(
                        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                // 获取联系人手机号
                String number = cursor.getString(cursor.getColumnIndex(
                        ContactsContract.CommonDataKinds.Phone.NUMBER));
                contactsList.add(displayName + "\n" + number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.contactstest"
android:versionCode="1"
android:versionName="1.0" >
……
<uses-permission android:name="android.permission.READ_CONTACTS" />
……
</manifest>

这里写图片描述

三、 创建自己的内容提供器

前面已经提到过,如果想要实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以通过新建一个类去继承 ContentProvider 的方式来创建一个自己的内容提供器。 ContentProvider 类中有六个抽象方法,我们在使用子类继承它的时候,需要将这六个方法全部重写。新建 MyProvider 继承自 ContentProvider,代码如下所示:

public class MyProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[]
            selectionArgs, String sortOrder) {
        return null;
    }
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[]
            selectionArgs) {
        return 0;
    }
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }
    @Override
    public String getType(Uri uri) {
        return null;
    }
}

在这六个方法中,相信大多数你都已经非常熟悉了,我再来简单介绍一下吧。
1. onCreate()
初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回 true 表示内容提供器初始化成功,返回 false 则表示失败。注意,只有当存在ContentResolver 尝试访问我们程序中的数据时,内容提供器才会被初始化。
2. query()
从内容提供器中查询数据。使用 uri 参数来确定查询哪张表, projection 参数用于确定查询哪些列, selection 和 selectionArgs 参数用于约束查询哪些行, sortOrder参数用于对结果进行排序, 查询的结果存放在 Cursor 对象中返回。
3. insert()
向内容提供器中添加一条数据。使用 uri 参数来确定要添加到的表,待添加的数据保存在 values 参数中。添加完成后,返回一个用于表示这条新记录的 URI。
4. update()
更新内容提供器中已有的数据。使用 uri 参数来确定更新哪一张表中的数据,新数据保存在 values 参数中, selection 和 selectionArgs 参数用于约束更新哪些行, 受影响的行数将作为返回值返回。
5. delete()
从内容提供器中删除数据。使用 uri 参数来确定删除哪一张表中的数据, selection和 selectionArgs 参数用于约束删除哪些行, 被删除的行数将作为返回值返回。
6. getType()
根据传入的内容 URI 来返回相应的 MIME 类型。

可 以 看 到 , 几 乎 每 一 个 方 法 都 会 带 有 Uri 这 个 参 数 , 这 个 参 数 也 正 是 调 用ContentResolver 的增删改查方法时传递过来的。而现在,我们需要对传入的 Uri 参数进行解析,从中分析出调用方期望访问的表和数据。

回顾一下,一个标准的内容 URI 写法是这样的:

content://com.example.app.provider/table1

这就表示调用方期望访问的是 com.example.app 这个应用的 table1 表中的数据。除此之外,我们还可以在这个内容 URI 的后面加上一个 id,如下所示:

content://com.example.app.provider/table1/1

这就表示调用方期望访问的是 com.example.app 这个应用的 table1 表中 id 为 1 的数据。

内容 URI 的格式主要就只有以上两种,以路径结尾就表示期望访问该表中所有的数据,以 id 结尾就表示期望访问该表中拥有相应 id 的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容 URI,规则如下。

  1. *:表示匹配任意长度的任意字符
  2. #:表示匹配任意长度的数字

所以,一个能够匹配任意表的内容 URI 格式就可以写成:

content://com.example.app.provider/*

而一个能够匹配 table1 表中任意一行数据的内容 URI 格式就可以写成:

content://com.example.app.provider/table1/#

接着,我们再借助 UriMatcher 这个类就可以轻松地实现匹配内容 URI 的功能。UriMatcher 中提供了一个 addURI()方法,这个方法接收三个参数,可以分别把权限路径和一个自定义代码传进去。这样,当调用 UriMatcher 的 match()方法时,就可以将一个Uri 对象传入,返回值是某个能够匹配这个 Uri 对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了。修改 MyProvider 中的代码,如下所示:

public class MyProvider extends ContentProvider {
    public static final int TABLE1_DIR = 0;
    public static final int TABLE1_ITEM = 1;
    public static final int TABLE2_DIR = 2;
    public static final int TABLE2_ITEM = 3;
    private static UriMatcher uriMatcher;
    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);
        uriMatcher.addURI("com.example.app.provider ", "table1/#", TABLE1_ITEM);
        uriMatcher.addURI("com.example.app.provider ", "table2", TABLE2_ITEM);
        uriMatcher.addURI("com.example.app.provider ", "table2/#", TABLE2_ITEM);
    }
    ……
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[]
            selectionArgs, String sortOrder) {
        switch (uriMatcher.match(uri)) {
            case TABLE1_DIR:
                // 查询table1表中的所有数据
                break;
            case TABLE1_ITEM:
                // 查询table1表中的单条数据
                break;
            case TABLE2_DIR:
                // 查询table2表中的所有数据
                break;
            case TABLE2_ITEM:
                // 查询table2表中的单条数据
                break;
            default:
                break;
        }
        ……
    }
    ……
}

可以看到, MyProvider 中新增了四个整型常量,其中TABLE1_DIR表示访问 table1表中的所有数据,TABLE1_ITEM 表示访问 table1 表中的单条数据,TABLE2_DIR表示访问 table2表中的所有数据,TABLE2_ITEM表示访问 table2 表中的单条数据。接着在静态代码块里我们创建了 UriMatcher 的实例,并调用 addURI()方法,将期望匹配的内容URI格式传递进去,注意这里传入的路径参数是可以使用通配符的。然后当 query()方法被调用的时候,就会通过 UriMatcher 的 match()方法对传入的 Uri 对象进行匹配,如果发现UriMatcher 中某个内容 URI 格式成功匹配了该 Uri 对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的到底是什么数据了。

上述代码只是以 query()方法为例做了个示范,其实 insert()、 update()、 delete()这几个方法的实现也是差不多的,它们都会携带 Uri 这个参数,然后同样利用 UriMatcher 的match()方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了。

除此之外,还有一个方法你会比较陌生,即 getType()方法。它是所有的内容提供器都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由三部分组分, Android 对这三个部分做了如下格式规定。
1. 必须以 vnd 开头。
2. 如果内容 URI 以路径结尾,则后接 android.cursor.dir/,如果内容 URI 以 id 结尾,则后接 android.cursor.item/。
3. 最后接上 vnd.< authority>.< path>。

所以,对于 content://com.example.app.provider/table1 这个内容 URI,它所对应的 MIME 类型就可以写成:

vnd.android.cursor.dir/vnd.com.example.app.provider.table1

对于 content://com.example.app.provider/table1/1 这个内容 URI,它所对应的
MIME 类型就可以写成:

vnd.android.cursor.item/vnd. com.example.app.provider.table1

现在我们可以继续完善 MyProvider 中的内容了,这次来实现 getType()方法中的逻辑,代码如下所示:

public class MyProvider extends ContentProvider {
    ……
    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)) {
            case TABLE1_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider. table1";
            case TABLE1_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider. table1";
            case TABLE2_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider. table2";
            case TABLE2_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider. table2";
            default:
                break;
        }
        return null;
    }
}

到这里,一个完整的内容提供器就创建完成了,现在任何一个应用程序都可以使用ContentResolver 来访问我们程序中的数据。那么前面所提到的,如何才能保证隐私数据不会泄漏出去呢?其实多亏了内容提供器的良好机制,这个问题在不知不觉中已经被解决了。因为所有的 CRUD 操作都一定要匹配到相应的内容 URI 格式才能进行的,而我们当然不可能向 UriMatcher 中添加隐私数据的 URI,所以这部分数据根本无法被外部程序访问
到,安全问题也就不存在了。

好了,创建内容提供器的步骤你也已经清楚了,下面就来实战一下,真正体验一回跨程
序数据共享的功能。

四、注册

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.databasetest"
    android:versionCode="1"
    android:versionName="1.0" >
    ……
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    ……
    <provider
        android:name="com.example.databasetest.DatabaseProvider"
        android:authorities="com.example.databasetest.provider" >
    </provider>
    </application>
</manifest>

可以看到,这里我们使用了标签来对 DatabaseProvider 这个内容提供器进行注册,在 android:name 属性中指定了该类的全名,又在 android:authorities 属性中指定了该内容提供器的权限。

这里简单介绍一下ContentProvider规定数据形式的URI组成:
主要包括三个部分
一:scheme
ContentProvider(内容提供者)的scheme已经由Android所规定,
scheme为:content://
二:主机名
主机名(或叫Authority)用于唯一标识这个ContentProvider,比如这里com.cj.mycontentprovider,外部调用者可以根据这个标识来找到它。
三:path
路径(path)可以用来表示我们要操作的数据,路径的构建应根据业务而定,如上面程序:
要操作contact表/contact
上面三个部分是主要的,还可以有更详细的,比如还包含数据id /contact/id。

猜你喜欢

转载自blog.csdn.net/Hystudio_lzu/article/details/80696946