Android 开发 Content Provider 应用程序的使用方法

概述

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,   // 单词表的 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 值即可。 例如,要读取用户词典中 _ID4 的数据行,可以使用以下 Content URI:

Uri singleUri =ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);

如果已经读取了一些数据,然后需要修改或删除其中的某一条,这时就经常会用到 ID 值了。

注意: UriUri.Builder 类中已内置了一些工具性的方法,可以由字符串搭建合乎规则的 Uri 对象。 ContentUris 中有一些在 URI 后面追加 ID 值的常用方法。 上述代码就用了 withAppendedId() 把 ID 追加到 UserDictionary 的 Content URI 之后。

从 Provider 读取数据


本节将介绍从 Provider 读取数据的过程,还是以 User Dictionary Provider 为例。

为了清晰起见,本节中的代码将会调用“UI 线程”中的 ContentResolver.query() 。但是在实际的代码中,应该在单独的线程中实现异步查询。 一种方案是利用 CursorLoader 类,而且,以下只给出了部分代码,而非一个完整的应用程序。

从 Provider 中读取数据的基本步骤如下所示:

  1. 申请读取 Provider 的权限。
  2. 编写向 Provider 发送查询请求的代码。

申请读取权限

要从 Provider 读取数据,应用程序需要拥有对 Provider 的“读权限”。 在运行时是无法申请该权限的,只能在 Manifest 文件中通过 指定。在 Manifest 文件中的定义,实际上是表明此应用程序需要“申请”该权限。 这样用户在安装此应用程序时,就可以明确授权。

在 Provider 的参考文档中,给出了其用到的全部权限的准确名称。

User Dictionary Provider 在其 Manifest 文件中定义了 android.permission.READ_USER_DICTIONARY 权限, 因此要读它的应用程序就必须请求该权限。

构建查询

接下来是构建查询请求。 以下代码定义了一些变量,在访问 User Dictionary Provider 时将会用到:

 1 // "projection" 定义了要返回的数据列
 2 String[] mProjection =
 3 {
 4     UserDictionary.Words._ID,   &n    // 对应列名为 _ID 的 Contract Class 常量
 5     UserDictionary.an class="typ">Words.WORD,   // 对应列名为 word 的 Contract Class 常量
 6     UserDictionary.an class="typ">Words.LOCALE &nbLOCALE  // 对应列名为 local 的 Contract Class 常量
 7 };
 8 
 9 // 定义存放查询条件的字符串
10 String mSelectionClause =an class="pln"> null; mSelectionArgs ={""};

接下来的代码演示了 ContentResolver.query() 的使用方法,这里以 User Dictionary Provider 为例。 Provider 客户端查询与 SQL 查询很类似,也包含了需返回的列名、查询条件和排序要求。

查询返回的列名集合对象被称为”投影“( Projection )(即变量 mProjection)。

查询数据的表达式被拆分为查询条件和查询参数。 查询条件是由逻辑/布尔表达式、列名、数值组成(即变量 mSelectionClause )。 如果用参数 ? 代替了具体数值,则查询方法将会从查询参数数组(变量 mSelectionArgs)中读取实际的值。

在以下代码中,如果用户没有输入单词,则查询语句将被置为 null,这样查询将会返回 Provider 中的所有单词。 如果用户输入了单词,那么查询语句将会是 UserDictionary.Words.WORD + " = ?" ,且查询参数数组中的第一个成员被设为用户输入的单词。

 1 /*
 2  * 定义只有一个成员的字符串数组,用于存放查询参数。
 3  */
 4 String[] mSelectionArgs ={""};
 5 
 6 // 从用户界面读取一个单词
 7 mSearchString = mSearchWord.getText().toString();
 8 
 9 // 别忘了在这里添加检查输入内容是否非法或恶意的代码
10 
11 // 如果单词为空字符串,则读取所有数据
12 if(TextUtils.isEmpty(mSearchString)){
13     // 将查询语句设为 null 将返回所有数据
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 // 查询数据并返回游标(Cursor)对象
27 mCursor = getContentResolver().query(
28     UserDictionary.Words.CONTENT_URI,  // 单词表的 Content URI
29     mProjection,                       // 需返回的列
30     mSelectionClause                   // 为 null 或是用户录入的单词
31     mSelectionArgs,                    // 为空或是用户录入的字符串
32     mSortOrder);                       // 定义返回数据的排序规则
33 
34 // 在出错时,某些 Provider 返回 null,另一些会抛出异常
35 if(null== mCursor){
36     /*
37      * 在这里插入处理错误的代码。
38      * 请勿在这里使用游标!
39      * 可能需要调用 
40      */
41 // 如果游标中没有内容,表示 Provider 没找到匹配的记录。
42 }elseif(mCursor.getCount()<1){
43 
44     /*
45      * 在这里插入通知用户查询失败的代码。
46      * 这不一定是出错了,可以让用户录入新记录,也可以重新输入查询条件。
47      */
48 
49 }else{
50     // 在这里插入处理查询结果的代码。
51 
52 }

查询的语句与以下 SQL 语句类似:

 SELECT _ID, word, locale FROM words WHERE word = 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 关联起来。

以下代码将延续上面的代码。 创建了一个含有 CursorSimpleCursorAdapter 对象,并将其设置为一个 ListView 的数据源适配器(Adapter):

 1 // 定义需要从 Cursor 读取并显示出来的数据列
 2 String[] mWordListColumns =
 3 {
 4     UserDictionary.Words.WORD,   // 对应 word 列的 Contract 类常量
 5     UserDictionary.Words.LOCALE  // 对应 locale 列的 Contract 类常量
 6 };
 7 
 8 // 定义 View ID 列表,用于保存 Cursor 返回的一行数据。
 9 int[] mWordListItems ={ R.id.dictWord, R.id.locale};
10 
11 // 新建一个 SimpleCursorAdapter 对象
12 mCursorAdapter =newSimpleCursorAdapter(
13     getApplicationContext(),               // 应用程序的 Context 对象
14     R.layout.wordlistrow,                  // XML 格式的 Layout,用于 ListView 中每一行的布局
15     mCursor,                               // 查询结果
16     mWordListColumns,                      // 字符串数组,存放游标中的列名
17     mWordListItems,                        // 整形数组,存放行布局中的 View ID
18     0);                                    // 标志位(一般用不上)
19 
20 // 设置 ListView 的 Adapter
21 mWordList.setAdapter(mCursorAdapter);

注意: 要将 Cursor 用作 ListView 的后台数据源,游标必须包含一个名为 _ID 的数据列。 因此,上述查询从“word”表中读取了 _ID 列,当然 ListView 并不会显示这个字段。 这也是大部分 Provider 中的数据表都带有 _ID 列的原因所在。

从查询结果中读取数据

查询结果不只是简单地用于显示,还可以用来完成其他操作。 比如,可以从用户词典中读取单词并在其他 Provider 中进行检索。 这时就需要遍历 Cursor 中的每行数据:

 1 // 找到列名为“word”的字段编号
 2 int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);
 3 
 4 /*
 5  * 仅当游标可用时才会执行。
 6  * 如果发生内部错误,User Dictionary Provider 将会返回 null。而其他 Provider 可能会抛出异常。
 7  */
 8 
 9 if(mCursor !=null){
10     /*
11      * 前进至下一行。 
12      * 在第一次移动之前,“记录指针”为 -1,如果这时读取数据,将会触发异常。
13      */
14     while(mCursor.moveToNext()){
15 
16         // 读取值
17         newWord = mCursor.getString(index);
18 
19         // 在这里插入处理返回单词的代码
20 
21         ...
22 
23         // while 循环结束
24     }
25 }else{
26 
27     // 如果游标为空或 Provider 抛出异常,在这里插入显示错误的代码。
28 }

Cursor 中有很多用于读取不同类型数据的“get”方法。 例如,上述代码中用到了 getString() 。还有一个 getType() 方法用于返回字段的类型。

本文源代码获取方式:私信 发送 “底层源码” 即可 免费获取

展开阅读全文

页面更新:2024-03-24

标签:游标   字符串   使用方法   语句   应用程序   单词   对象   参数   代码   方法   数据   用户

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top