四大组件之ContentProvider

前言

ContentProvider作为Android的四大组件之一,是属于需要掌握的基础知识,可能在我们的应用中,对于Activity和Service这两个组件用的很常见,了解的也很多,但是对ContentProvider所知却甚少,所以有必要去整理归纳下其中的内容,讲讲为什么要用ContentProvider这个组件、ContentProvider是什么、ContentProvider用法如何,让大家对ContentProvider有个整体上的理解,方便以后在开发过程中如果忘记了可以及时回顾。

目录

  • 为什么要用ContentProvider
  • ContentProvider是什么
  • ContentProvider用法
  • 小结

为什么要用ContentProvider

我们都知道在Android中有数据持久化技术,常见的几种方式比如:括文件存储、SharedPreferences 存 储、以及数据库存储。但这几个存储方式有个共性,那就是只能在应用内访问存储的数据,如果有需要共享的数据呢,就不能对外提供了,虽然SharedPreferences 存储中提供了MODE_WORLD_READABLE 和MODE_WORLD_WRITEABLE这两种操作模式,但这两种模式在Android 4.2的版本已经被废弃了。

那该如何实现跨程序的数据共享呢?此时,就引出了ContentProvider内容提供器,或许你会问,我们为什么要实现跨程序的数据共享,很简单,如果我们想在基础系统上进行二次开发,想引用Android系统本身的数据,就需要一些程序的数据共享,比如,你想做基于通讯录的二次开发,基于短信系统的二次开发,就需要用Android系统提供的数据,因为这些基础数据本身被封装到系统内,你也不太可能自己去设置吧,如果这些数据都不允许第三方的程序进行 访问的话,恐怕很多应用的功能都要大打折扣了。

ContentProvider是什么

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

ContentProvider的用法一般有两种,一种是使用现有的ContentProdiver来读取和操作相应程序中的数据,另一种是创建自己的ContentProvider给我们程序的数据提供外部访问接口。

ContentProvider用法

  • 利用现有的ContentProvider来读取和操作
  • 自己创建ContentProvider提供数据

利用现有的ContentProvider来读取和操作

对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助 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最标准的格式写法:

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

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

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

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

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

那么我们可以看看getContentResolver()这个方法跟Sql部分的对应关系吧。

两者关系

查询完成后返回的仍然是一个 Cursor 对象,这时我们就可以将数据从 Cursor 对象中逐 个读取出来了。读取的思路仍然是通过移动游标的位置来遍历 Cursor的所有行,然后再取出每一行中相应列的数据,代码如下所示:

1
2
3
4
5
6
7
if (cursor != null) {  
while (cursor.moveToNext()) {
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}

剩下的增加、修改、删除操作就简单多了。

我们先来看看如何向 table1表中添加一条数据,代码如下所示:

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

可以看到,仍然是将待添加的数据组装到 ContentValues 中,然后调用 ContentResolver。

在table1中更新一条数据

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

在table1中删除一条数据

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

整体来说,我们可以利用现有的ContentProvider来获取数据,比如读取联系人的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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();
}
}
}
}

同时需要要注意的是在注册清单中加上读取联系人的权限,表示允许应用访问联系人信息。

1
<uses-permission android:name="android.permission.READ_CONTACTS" />

自己创建ContentProvider提供数据

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

比如新写一个MyProvider类,需要继承ContentProvider。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 String getType(Uri uri) {
return null;
}

@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}

@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}

在这六个方法中,相信大多数你都已经非常熟悉了。

  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写法是这样的:

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

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

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

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

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

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

content://com.example.app.provider/* 表示一个能够匹配任意表的内容 URI格式content://com.example.app.provider/table1/# 表示一个能够匹配 table1表中任意一行数据的内容 URI格式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
}
return null;
}

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

除此之外,还有一个方法你会比较陌生,即 getType()方法。它是所有的内容提供器都必 须提供的一个方法,用于获取 Uri对象所对应的 MIME类型。一个内容 URI所对应的 MIME 字符串主要由三部分组分,Android对这三个部分做了如下格式规定。

  1. 必须以 vnd开头。
  2. 如果内容 URI 以路径结尾,则后接 android.cursor.dir/,如果内容 URI 以 id 结尾, 则后接 android.cursor.item/。
  3. 最后接上 vnd..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
}
return null;
}

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

下面比如我们自己创建一个内容提供器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
public class DatabaseProvider extends ContentProvider {

public static final int BOOK_DIR = 0;

public static final int BOOK_ITEM = 1;

public static final int CATEGORY_DIR = 2;

public static final int CATEGORY_ITEM = 3;

public static final String AUTHORITY = "com.example.databasetest.provider";

private static UriMatcher uriMatcher;

private MyDatabaseHelper dbHelper;

static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
}

@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
return true;
}

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book", projection, "id = ?", new String[] { bookId }, null, null,sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category", projection, selection, selectionArgs, null, null,
sortOrder);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
cursor = db.query("Category", projection, "id = ?", new String[] { categoryId }, null,null, sortOrder);
break;
default:
break;
}
return cursor;
}

@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Category", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
break;
default:
break;
}
return uriReturn;
}

@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", values, "id = ?", new String[] { bookId });
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", values, selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updatedRows = db.update("Category", values, "id = ?", new String[] { categoryId });
break;
default:
break;
}
return updatedRows;
}

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deletedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deletedRows = db.delete("Book", selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deletedRows = db.delete("Book", "id = ?", new String[] { bookId });
break;
case CATEGORY_DIR:
deletedRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deletedRows = db.delete("Category", "id = ?", new String[] { categoryId });
break;
default:
break;
}
return deletedRows;
}

@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category";
}
return null;
}
}

这里结合了SQLiteDatabase数据库的操作,把想把共享的数据开放出去。

不过同样需要注意的是,需要在清单中注册。

1
2
3
4
<provider
android:name="com.example.databasetest.DatabaseProvider"
android:authorities="com.example.databasetest.provider" >
</provider>

小结

所以总的来说,ContentProvider是四大组件之一,这也是我们需要掌握的基础知识之一,从通过分析为何需要ContentProvider,以及ContentProvider是什么,并最后是如何使用,对这一个过程是不是清楚很多,当然这些都是基本用法,如果有兴趣的话,可以具体去看源码,了解其中实现的原理,很多时候我们要做到知其然而之所以然,这样使用起来了,以后有什么问题,可以迅速定位到其中原因。

,