概述
Content Provider 以数据表的形式向外部应用程序提供数据,这与关系型数据库中的表很类似。 其中,行(row)表示由多个不同类型数据构成的单个实体,每行数据中的列(column)代表实体中的一个数据项。
例如,用户词典就是 Android 系统内置的 Provider 之一,里面记录着用户需要留存的自定义拼写规则的单词。 表1例举了此 Provider 数据表中可以查询的字段信息:
表1: 用户词典表举例
word |
app id |
frequency |
locale |
_ID |
mapreduce |
user1 |
100 |
en_US |
1 |
precompiler |
user14 |
200 |
fr_FR |
2 |
applet |
user2 |
225 |
fr_CA |
3 |
const |
user1 |
255 |
pt_BR |
4 |
int |
user5 |
100 |
en_UK |
5 |
在表1中,每行代表一个可能无法在标准词典中查到的单词。 每列代表与单词相关的数据,比如首次使用时的地区(语言)。 每列的标题即为存储时的列名称。 引用 locale
列就可以得到每一行数据的地区信息。 这里的 _ID
列被用作“主键”(primary key),并且是由 Provider 自动维护的。
注意: Provider 本身不需要用到主键,主键的名称也不一定要是 _ID
。 但是,如果要把 Provider 作为数据源与 ListView
绑定,则必须有一个列的名称是 _ID
。 详细要求将在 显示查询结果中描述。
访问 Provider
应用程序是通过客户端对象 ContentResolver
访问 Content Provider 的。 此对象中包含一些方法,这些方法将会调用 Provider 对象中的同名方法。而 Provider 对象是 ContentProvider
某个具体子类的实例。 ContentResolver
中的方法内置了基本的“CRUD”(创建、查询、更新、删除(create、retrieve、update 和 delete))功能。
ContentResolver
对象运行于客户端应用的进程中,而 ContentProvider
运行于提供 Provider 应用的进程中,两者会自动完成进程间的通讯。 ContentProvider
还发挥着数据抽象层的作用,负责将内部数据以数据库表的形式提供出来。
注意: 为了访问 Provider,应用程序通常必须在 Manifest 文件中请求相应的权限
例如,要从 User Dictionary Provider 中读取单词及地区列表,就要用到 ContentResolver.query()
。query()
方法会去调用 User Dictionary Provider 中对应的 ContentResolver.query()
方法。以下代码演示了 ContentResolver.query()
的调用过程:
1
2 mCursor = getContentResolver().query(
3 UserDictionary.Words.CONTENT_URI,
4 mProjection,
5 mSelectionClause,
6 mSelectionArgs,
7 mSortOrder);
表2给出了 query(Uri,projection,selection,selectionArgs,sortOrder)
的参数与 SQL SELECT 语句的对应关系:
表2: Query() 与 SQL 查询的对比
query() 参数 |
SELECT 关键字/参数 |
说明 |
Uri |
FROM *table_name* |
Uri 对应于 table_name 指定的 Provider 数据表名。 |
projection |
*col,col,col,...* |
projection 是包含返回列名称的数组。 |
selection |
WHERE *col* = *value* |
selection 指定查询条件。 |
selectionArgs |
(没有固定值,该查询参数将会替换查询语句中的占位符“?”。) |
|
sortOrder |
ORDER BY *col,col,...* |
sortOrder 指定了返回 Cursor 中各行的显示顺序。 |
Content URI
Content URI 是一种用于标识 Provider 数据的 URI。 Content URI 包括了整个 Provider 的符号名称(authority)和表名(path)。 调用客户端的方法访问 Provider 数据表时,表的 Content URI 是参数之一。
在前面的代码中,常量 CONTENT_URI
包含了指向用户词典中 “word” 表的 Content URI。 ContentResolver
对象将分离出 URI 中的 authority ,并用它“解析” 出 Provider,这是通过将 authority 与系统记录的已有 Provider 清单进行比较来实现的。 然后 ContentResolver
就可以将查询参数发送给相应的 Provider 了。
ContentProvider
用 Content URI 的 path 部分选择要访问的数据表。 通常, Provider 公开的所有数据表都会带有自己的 path 。
在上述代码中,“word”表的完整 URI 为:
content://user_dictionary/words
这里的字符串 user_dictionary
是 Provider 的 authority 部分, 字符串 words
是数据表的 path 部分。 字符串 content://
(scheme)是必须指定的,以表明这是一个 Content URI。
很多 Provider 提供了对单条记录的访问能力,只要在 URI 后面跟一个 ID 值即可。 例如,要读取用户词典中 _ID
为 4
的数据行,可以使用以下 Content URI:
Uri singleUri =ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
如果已经读取了一些数据,然后需要修改或删除其中的某一条,这时就经常会用到 ID 值了。
注意: Uri
和Uri.Builder
类中已内置了一些工具性的方法,可以由字符串搭建合乎规则的 Uri 对象。 ContentUris
中有一些在 URI 后面追加 ID 值的常用方法。 上述代码就用了 withAppendedId()
把 ID 追加到 UserDictionary 的 Content URI 之后。
从 Provider 读取数据
本节将介绍从 Provider 读取数据的过程,还是以 User Dictionary Provider 为例。
为了清晰起见,本节中的代码将会调用“UI 线程”中的 ContentResolver.query()
。但是在实际的代码中,应该在单独的线程中实现异步查询。 一种方案是利用 CursorLoader
类,而且,以下只给出了部分代码,而非一个完整的应用程序。
从 Provider 中读取数据的基本步骤如下所示:
- 申请读取 Provider 的权限。
- 编写向 Provider 发送查询请求的代码。
申请读取权限
要从 Provider 读取数据,应用程序需要拥有对 Provider 的“读权限”。 在运行时是无法申请该权限的,只能在 Manifest 文件中通过 <uses-permission>
指定。在 Manifest 文件中的定义,实际上是表明此应用程序需要“申请”该权限。 这样用户在安装此应用程序时,就可以明确授权。
在 Provider 的参考文档中,给出了其用到的全部权限的准确名称。
User Dictionary Provider 在其 Manifest 文件中定义了 android.permission.READ_USER_DICTIONARY
权限, 因此要读它的应用程序就必须请求该权限。
构建查询
接下来是构建查询请求。 以下代码定义了一些变量,在访问 User Dictionary Provider 时将会用到:
1
2 String[] mProjection =
3 {
4 UserDictionary.Words._ID, &n
5 UserDictionary.an class="typ">Words.WORD,
6 UserDictionary.an class="typ">Words.LOCALE &nbLOCALE
7 };
8
9
10 String mSelectionClause =an class="pln"> null;<s;
11
12
13 String[]an class="pln"> mSelectionArgs ={""};
接下来的代码演示了 ContentResolver.query()
的使用方法,这里以 User Dictionary Provider 为例。 Provider 客户端查询与 SQL 查询很类似,也包含了需返回的列名、查询条件和排序要求。
查询返回的列名集合对象被称为”投影“( Projection )(即变量 mProjection
)。
查询数据的表达式被拆分为查询条件和查询参数。 查询条件是由逻辑/布尔表达式、列名、数值组成(即变量 mSelectionClause
)。 如果用参数 ?
代替了具体数值,则查询方法将会从查询参数数组(变量 mSelectionArgs
)中读取实际的值。
在以下代码中,如果用户没有输入单词,则查询语句将被置为 null
,这样查询将会返回 Provider 中的所有单词。 如果用户输入了单词,那么查询语句将会是 UserDictionary.Words.WORD + " = ?"
,且查询参数数组中的第一个成员被设为用户输入的单词。
1
4 String[] mSelectionArgs ={""};
5
6
7 mSearchString = mSearchWord.getText().toString();
8
9
10
11
12 if(TextUtils.isEmpty(mSearchString)){
13
14 mSelectionClause =null;
15 mSelectionArgs[0]="";
16
17 }else{
18
19 mSelectionClause =UserDictionary.Words.WORD +" = ?";
20
21
22 mSelectionArgs[0]= mSearchString;
23
24 }
25
26
27 mCursor = getContentResolver().query(
28 UserDictionary.Words.CONTENT_URI,
29 mProjection,
30 mSelectionClause
31 mSelectionArgs,
32 mSortOrder);
33
34
35 if(null== mCursor){
36
41
42 }elseif(mCursor.getCount()<1){
43
44
48
49 }else{
50
51
52 }
查询的语句与以下 SQL 语句类似:
SELECT _ID, word, locale FROM words WHERE word =<userinput> ORDER BY word ASC;
这条 SQL 语句中使用的是真实的列名,而不是 Contract 类常量。
防止非法输入
如果 Content Provider 管理的数据存放于 SQL 数据库中,那么在 SQL 语句中插入某些非法信息可能会引发 SQL 注入问题。
请看下面这条查询语句:
String mSelectionClause = "var = "+ mUserInput;
这时,用户就可以将恶意 SQL 拼接到查询语句中。 比如,用户可以将 mUserInput
输入为“nothing; DROP TABLE *;”,这样查询语句就会成为“var = nothing; DROP TABLE *;
”. 因为查询语句将用作 SQL 语句,所以会导致 Provider 删除底层 SQLite 数据库中的所有数据表(除非 Provider 设置为捕获 SQL 注入异常)。
为了避免这类问题,可以在查询语句中使用 ?
作为可替代参数,并用另一个数组作为实际的参数值。 这样,用户的输入就与查询直接关联,而不会被解释为 SQL 语句的一部分。 因为不再用作 SQL 语句,用户输入就无法注入恶意 SQL 了。 用户的输入内容不直接用于拼接 SQL 语句,查询语句如下:
String mSelectionClause = "var = ?";
查询参数数组定义如下:
String[] selectionArgs ={""};
在数组中放入一个查询参数值:
selectionArgs[0]= mUserInput;
在构造查询时,推荐使用这种将 ?
作为形参、数组提供实参的查询语句,即使不是基于 SQL 数据库的 Provider 也可以使用。
显示查询结果
客户端方法 ContentResolver.query()
将返回一个 Cursor
,其中的数据列由对应查询条件的 Projection 指定。 Cursor
对象支持对数据行和数据列的随机读取。通过 Cursor
的内部方法,可以遍历结果数据行、获取每一列的数据类型、读取某一字段的数据并检查其他属性。 某些 Cursor
对象可以在 Provider 的数据发生变化时进行自动更新,或是在 Cursor
数据变动时触发其他监听对象的方法。
注意: 根据建立查询的对象性质, Provider 可以限制对数据列的访问。 比如,联系人 Provider 就不允许 Sync Adapter 访问某些数据列,也就不会在 Activity 和服务中返回这些列。
如果没有找到符合条件的数据, Provider 就会返回一个 Cursor.getCount()
为 0 的 Cursor
对象(即空游标)。
如果发生了内部错误,查询返回的结果将视 Provider 的不同而定。 可能是返回 null
,也可能抛出一个 Exception
。
因为 Cursor
是一个数据行的“列表”,所以一种较好的显示方式就是通过 SimpleCursorAdapter
把它与 ListView
关联起来。
以下代码将延续上面的代码。 创建了一个含有 Cursor
的 SimpleCursorAdapter
对象,并将其设置为一个 ListView
的数据源适配器(Adapter):
1
2 String[] mWordListColumns =
3 {
4 UserDictionary.Words.WORD,
5 UserDictionary.Words.LOCALE
6 };
7
8
9 int[] mWordListItems ={ R.id.dictWord, R.id.locale};
10
11
12 mCursorAdapter =newSimpleCursorAdapter(
13 getApplicationContext(),
14 R.layout.wordlistrow,
15 mCursor,
16 mWordListColumns,
17 mWordListItems,
18 0);
19
20
21 mWordList.setAdapter(mCursorAdapter);
注意: 要将 Cursor
用作 ListView
的后台数据源,游标必须包含一个名为 _ID
的数据列。 因此,上述查询从“word”表中读取了 _ID
列,当然 ListView
并不会显示这个字段。 这也是大部分 Provider 中的数据表都带有 _ID
列的原因所在。
从查询结果中读取数据
查询结果不只是简单地用于显示,还可以用来完成其他操作。 比如,可以从用户词典中读取单词并在其他 Provider 中进行检索。 这时就需要遍历 Cursor
中的每行数据:
1
2 int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);
3
4
8
9 if(mCursor !=null){
10
14 while(mCursor.moveToNext()){
15
16
17 newWord = mCursor.getString(index);
18
19
20
21 ...
22
23
24 }
25 }else{
26
27
28 }
Cursor
中有很多用于读取不同类型数据的“get”方法。 例如,上述代码中用到了 getString()
。还有一个 getType()
方法用于返回字段的类型。
本文源代码获取方式:私信 发送 “底层源码” 即可 免费获取