android Content Provider详解

内容提供者-Content Provider

Content providers管理对结构化数据集的使用.它们封装数据,并提供了数据安全的机制.Content providers是从一个进程连接另一个进程中的数据的标准接口. 

当你想使用一个content provider中的数据,你需在你的应用的Context 中使用ContentResolver对象作为客户端与provider 进行通讯.ContentResolver对象与provider对象通讯,provider是实现ContentProvider的类.Provider对象接收客户端发来的请求,执行请求的动作,返回结果. 

如果你不想把你的数据共享给其它应用,你不需开发你自己的provider.然而,你需要自己的provider来在你的应用中提供自定义搜索建议.如果你需要从你的应用中考贝复杂的数据或文件粘贴到其它应用中,你也需要提供自己的provider 

Android自己包含了管理音频,视频,图像,个人通讯录等数据的content providers.你可以从android.provider 包的参考文档中爪到它们.这些providers 可以被所有的android应用使用,但可能带有一些限制. 

Content Provider 基础

一个content provider 管理对中央数据仓库的使用.一个provider是一个Android应用的一部分,应用一般提供它自己的UI来操作数据.然而,content providers主要是为了给其它应用使用,其它的应用使用provider客户端对象来操作providerproviders provider客户端一起提供了一致的,标准的接口来操作用于进程间通讯的数据并保处数据的安全性. 

本节讲解以下基础知识:

· content providers如何工作.

· content provider取得数据的API.

· content provider插入,更新以及删除数据的API.

· 其它有助于使用providersAPI.

概述

一个content provider代表了面向外部应用的数据,这些数据看起来就像关系型数据库中的一个或多个table.一行代表某种数据类型的一个实例,一列代表这个实例的一个属性或字段.

举个例子,Android平台中的一个内建的provider是用户词典,它存储了用户想保存的非标准词的拼写.表演示了数据在provider的表中可能看起来的样子:

Table 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

在上表中,每行代表了一个不能在标准字典中找到的词.每一列代表了这个词了一个属性.列头是存储在provider中的列的名字.要引用一行的locale属性,需引用locale 列.对于这个provider_ID列作为"主键"列,provider会自动管理它.

注:一个provider不是必须具备主键的,并且也不是必须使用_ID 作为主键的列名来引用一行.然而,如果你把一个provider绑定到一个ListView就必须有一个列名叫做_ID.此需求将在显示查询结果一节中有详细的解释.

操作一个provider

应用使用ContentResolver客户端对象来操作content provider中的数据.此对象具有一些与provider 对象中同名的方法,provider对象指的是某个ContentProvider具体派生类的实例.ContentResolver 的方法们提供了对存储数据的基本的"CRUD" (增删改查)功能.

ContentResolver 对象处于客户端应用的线程中,ContentProvider 对象位于另外的进程并且自动处理进程间通讯. ContentProvider 也代表了数据层与可视层之间的一个抽象层.

注:要使用一个provider,你的应用通常需要在manifest请求一些权限这将在Content Provider 一节中进行更详细的讲解.

举个例子,要从用户词典Provider中获取取单词和它们的locale列表,你需调用ContentResolver.query()query() 方法会调用用户词典中的ContentProvider.query() 方法.下面的代码演示了ContentResolver.query() 调用:

// 查询用户词典并返回结果 mCursor = getContentResolver().query(     UserDictionary.Words.CONTENT_URI,   // 单词表的content URI     mProjection,                        // 每行要返回的列们     mSelectionClause                    // Selection的条件     mSelectionArgs,                     // Selection的条件     mSortOrder);                        // 返回各行要如何排序

2展示了query(Uri,projection,selection,selectionArgs,sortOrder) 的参数们如何与一个SQL SELECT语句匹配:

Table 2: Query() SQL 查询的对比

query() argument

SELECT keyword/parameter

Notes

Uri

FROM table_name

Uri 对应provider 中叫做table_name的表.

projection

col,col,col,...

projection 是每行要包含的列们

selection

WHERE col = value

selection 指定了一些选择条件

selectionArgs

(不能对应.替换selection参数各类似于? 的占位符)

sortOrder

ORDER BY col,col,...

sortOrder 指定排序方式

Content URIs

content URI 是一个标志provider中的数据的URI.Content URI中包含了整个provider的以符号表示的名字(它的authority) 和指向一个表的名字(一个路径).当你调用一个客户端的方法来操作一个provider中的一个表,指向表的content URI是参数之一. 

常量CONTENT_URI 中包含了用户词典table的content URI.ContentResolver 对象分析出URI的authority,并使用它与一个已知provider组成的系统表中的authority进行对比来"解决"provider.ContentResolver之后就会派送查询参数给正确的 provider. 

ContentProvider使要长content URI的路径辨别分来选择要操作的表.通常一个provider中要暴露的每个表都具有一个路径. 

在上面的例子的代码中,"词典"表的全URI是: 

content://user_dictionary/words

user_dictionary部分是provider的 authority,words部分是表的路径.字符串 content:// (the scheme) 总是要存在,它表示引用一个content URI.

很多provider允许你通过在URI的末尾增加一个ID来操作表中一个单独的行.例如,要从用户词典中获取_ID是4的一行,你可要行使用这样的content URI: 

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

当你要获取多行然后更新或删除其中一时,你经常要使用的是id值. 

注:Uri和Uri.Builder类包含由字符串构建格式正确的Uri对象的简便的方法们.ContentUris 包含向一个URI添加id值的简便方法们.上面的小代码片段就是使用了withAppendedId() 来向UserDictionary content URI添加id

 

从Provider取得data

 

本节讲述了如何从provider取得数据,使用用户词典作为例子.

 

为了清析易懂,本节中调用ContentResolver.query()的代码片断置于"UI 线程"中.但是,在实际代码中,你应该在另一个线程执行查询动作,这样做的一种方法是使用CursorLoader 类.而,那几行示例代码仅是片断,它们不能展示一个完整的应用.

 

要从provider取得data,须依如下步骤:

 

请求provider的读权限.

 

定义发送请求到provider的代码.

 

 

 

请求读权限

 

要从一个provider中获取数据,你的应用需要对目标provider具有"读权限".你不能在运行时请求此权限,而只能在manifest文件中使用 <uses-permission> 元素指定你的权限需求.当你在manifest中指定此元素时,你实际上就是在为你的应用请求这个权限.当用户安装你的应用时,就表示同意了这个权限请求.

 

要找到你使用的provider读权限的所对应的准确名字,以及其它用于provider的权限的名字,请浏览provider的文档.

 

关于操作provider的权限的角色的更多信息,请见Content Provider权限一节.

 

用户词典Provider在它的manifest 中定义了android.permission.READ_USER_DICTIONARY 权限,所以一个想读取它内容的应用必须请求此权限.

 

 

 

构建请求

 

获取数据的下一步是构建一个请求(query).这里的第一个代码片段定义了一些用于操作用户词典Provider的变量:

 

// "projection" 定义了要返回的各列们 String[] mProjection = {     UserDictionary.Words._ID,    // Contract class constant for the _ID column name     UserDictionary.Words.WORD,   // Contract class constant for the word column name     UserDictionary.Words.LOCALE  // Contract class constant for the locale column name }; // 定义一个包含"select"条款的字符串 String mSelectionClause = null; // 初始化一个包含"select"参数的字符串 String[] mSelectionArgs = {""};

 

下一个代码片段演示了如何使用ContentResolver.query(),将用户词典Provider作为一个例子.一个provider客户端查询极像一个SQL查询,它包含了要返回的一坨column们,一堆筛选条件,和一个排序方式.

 

查询返回的column集合被称作projection (变量 mProjection)

 

指定返回的列的语句被分解为选择条款选择参数两部分.选择条款是逻辑和布尔表达式,列名以及值的组合体(变量mSelection).如果你在其中指定了使用 ? 来代表一个值,查询方法就会从选择参数部分取得这个值(变量mSelectionArgs)

 

在下一个代码片段中,如果用户没有输入单词,选择条款就被设为null,并且查询会反回所provider中所有的单词.如果用户输入了单词,选择条款就被设置为UserDictionary.Words.Word + " = ?" 并且选择参数(数组)的第一项被设置为用户输入的单词.

 

/*  * 定义一个一维的字符串数组来容纳选择参数们  */ String[] mSelectionArgs = {""}; // 从界面中获取一个单词 mSearchString = mSearchWord.getText().toString(); // 记住要在此插插入代码检查不合法的或恶意的输入. // 如果单词是空的,则获取所有数据 if (TextUtils.isEmpty(mSearchString)) {     // 设置选择条款为null就会返回所有单词     mSelectionClause = null;     mSelectionArgs[0] = ""; } else {     // 构造一个匹配用户输入的单词的选择条款     mSelectionClause = " = ?";     // 将用户输入的单词置于选择参数中     mSelectionArgs[0] = mSearchString; } // 执行查询并返回游标对象 mCursor = getContentResolver().query(     UserDictionary.Words.CONTENT_URI,  // The content URI of the words table     mProjection,                       // The columns to return for each row     mSelectionClause                   // Either null, or the word the user entered     mSelectionArgs,                    // Either empty, or the string the user entered     mSortOrder);                       // The sort order for the returned rows // 有些provider在出错时返回null,有抛出异常 if (null == mCursor) {     /*      * 在此插入代码处理错误.记住不要使用游标你可能要调用      * android.util.Log.e()把错误记录的日志      *      */ // 如果游标是空的,找不到匹配的provider } else if (mCursor.getCount() < 1) {     /*      * 在此插入代码来通知用户,查找不成功.这也不能完全算是个错误.你可能想为用户提供插入一个新行或重新输入查询单词的选项      */ } else {     // 在此插入代码,利用返回的结果做想做的事 }

 

查询与下面的SQL语句等价:

 

SELECT _ID, word, frequency, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

 

在此SQL 语句中,以实际的列名代替了内置的类别常量.

 

 

 

防止恶意输入

 

如果被content provider管理的数据是一个SQL 数据库,在原始的SQL语句中包含不可信的数据会导致SQL注入

 

思考以下选择条款:

 

// 通过连接用户输入到列名来构造一个选择条款 String mSelectionClause =  "var = " + mUserInput;

 

如果你这样做,你就允许用户连接恶意的SQL语句到你的SQL语句中.例如,用户可以输入"nothing; DROP TABLE *;" ,这将在选择条款中变为 var = nothing; DROP TABLE *;..既然选择条款被作为SQL语句,这就可能导致provider删除SQLite数据库中的所有的表(除非provider被设置成捕获SQL injection 阴谋).

 

要避免此问题,应使用一个运用?作为可替换参数的选择条款和一个作为选择参数的数组.当你这样做时,用户输入被直接绑定到查询而不是被解释为SQL语句的一部分.因为它不被认为是SQL,于是用户输入就不能注入恶意SQL.使用以下选择条款来代替连接用户输入的那个:

 

// 构造一个带有占位符的选择条款 String mSelectionClause =  "var = ?";

 

像这样建立起选择参数数组:

 

// 定义一个数组来容纳选择参数 String[] selectionArgs = {""};

 

像这样把一个值置入选择参数数组中:

 

// Sets the selection argument to the user's input selectionArgs[0] = mUserInput;

 

一个使用?作为占位符的选择条款+一个选择参数数组是指定一个选择器的最佳方式,即使provider不是基于SQL数据库的.

 

显示查询结果

 

客户端方法ContentResolver.query() 总是返回一个包含所查询的列们的Cursor .一个Cursor 对象提供了随机的读取它所包含的行和列的能力.使用Cursor 的方法们,你可以迭代结果中的行,决定每列的数据类型,从列获得数据,以及检测结果的其它属性.一些Cursor 的实现会在provider的数据改变时自动更新,或在Cursor 改变时触发监听者的方法,或者两者都支持.

 

注: 一个provider可能跟据构建查询的对象的性质限制对某些列的操作.例如,联系人Provider会禁止同步适配器操作某些列,所以它不会把它们返回给一个activityservice

 

如果没有符合选择条件的行,provider返回一个Cursor 对象,其Cursor.getCount() 0 (一个空cursor)

 

如果一个内部错误发生,查询结果会因provider的不同而不同.它可能返回null,也可能抛出一个Exception

 

既然一个Cursor 是行组成的"列表",那么一个和显示Cursor 内容的好方法就是把它链接到一个ListView 上,通过SimpleCursorAdapter

 

下面的代码片段是衔接前面的代码来的.它创建一个SimpleCursorAdapter 对象,包含有查询返回的Cursor ,然后设置这个对象为ListView的适配器.

 

// 定义要从Cursor取出的并要加载到view中的列们 String[] mWordListColumns = {     UserDictionary.Words.WORD,   // Contract class constant containing the word column name     UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name }; // 定义一个View ID组成的列表,它们将接收每行的Cursor列的值 int[] mWordListItems = { R.id.dictWord, R.id.locale}; // Creates a new SimpleCursorAdapter mCursorAdapter = new SimpleCursorAdapter(     getApplicationContext(),               // The application's Context object     R.layout.wordlistrow,                  // ListView的一行的layout     mCursor,                               // The result from the query     mWordListColumns,                      // A string array of column names in the cursor     mWordListItems,                        // An integer array of view IDs in the row layout     0);                                    // Flags (usually none are needed) // 将适配器设置给ListView mWordList.setAdapter(mCursorAdapter);

 

注:要使Cursor支持ListViewcursor必须包含一个叫做_ID的列,因此,上面所示的查询从"单词"表中取出了_ID ,当ListView 可以不显示它.这条限制同时也解释了为毛大多数provider在它们的表中都具有一个_ID 列.

 

从查询结果中获取数据

 

 

 

你可以使用查询结果做更多是事情,而不是仅简单地显示它们.比如,你可以从用户词典中获取拼法然后在其它provider中查找它们.要这样做,你需在Cursor中迭代所有的行.

 

// 获取叫做"word"的列的序号 int index = mCursor.getColumnIndex(UserDictionary.Words.WORD); /*  * 仅在cursor有效时执行下面语句.如果发生内部错误,用户词典Provider返回null  * 其它provider可能抛出一个异常而不是返回null.  */ if (mCursor != null) {     /*      * 移到cursor中的下一行.在第一次移动之前,      * "行指针-1,并且,如果你想获取那个位置的数据,你将得到一个异常      */     while (mCursor.moveToNext()) {         // 从列中获取值.         newWord = mCursor.getString(index);         // 在此插入代码处理获取到的单词         ...         // 循环结束     } } else {     // 如果cursornull或前面抛出了异常,在处插入代码报告错误. }

 

Cursor 的实现包含了多个"get" 方法,用于从对象中获取不同类型的数据.例如,前面的代码片段使用getString().它们也具有一个getType() 方法,用它可以返回的值代表了数据的类型.

 

Content Provider 的权限

 

一个具有provider的应用可以指定其它要操作自己的数据所应具有的权限.这些权限保证了用户能了解一个应用将要操作那个数据.其它应用需基于provider的需求请求相应的权限.用户在安装应用时会看到它们所请求的权限.

 

如果一个provider的应用没有指定任务权限,那么其它应用就不能操作provider的数据.然而,provider所在的应用的组件们却具有完整的读写权限,而不管是否指定了权限.

 

如上面所提到的,用户词典Provider需要android.permission.READ_USER_DICTIONARY 权限来从它取得数据.Provider具有另一个android.permission.WRITE_USER_DICTIONARY权限,代表了插入,更新或删除的权限.

 

要获取操作一个provider的权限,应用需在自己的manifest文件中使用<uses-permission> 元素.当Android包管理器安装这个应用时,用必须批准所有的权限请求.如果用户批准了所有的权限请求,包管理器会继续安装这个应用;如果没有,包管理器就会取消安装过程.

 

下面的<uses-permission> 元素请求对用户词典的读权限:

 

    <uses-permission android:name="android.permission.READ_USER_DICTIONARY">

 

Provider操作权限的作用在指南安全和权限一节中有详细的描述.

 

 

 

插入更新删除数据

 

 

 

用从provider取得数据相同的方法,你也可以让provider 客户端与providerContentProvider以交互方式修改数据.你调用个ContentResolver 的方法,其参数是要传给 ContentProvider对应方法的.Providerprovider客户端自动处理安全问题和进程间通信问题.

 

插入数据

 

 

 

要向一个provider中插入数据,需调用ContentResolver.insert() 方法.此方法插入一个新行到provider中并且返回一个代表这一行的content URI.下面的代码片段演示了如何将一个新行插入到用户词典中:

 

// 定义一个新的Uri对象,用于接收插入后的返回值 Uri mNewUri; ... // 定义一个对象来包含要插入的值们 ContentValues mNewValues = new ContentValues(); /*  * 设置要插入行的每列的值."put"方法的参数是"column name""value"  */ mNewValues.put(UserDictionary.Words.APP_ID, "example.user"); mNewValues.put(UserDictionary.Words.LOCALE, "en_US"); mNewValues.put(UserDictionary.Words.WORD, "insert"); mNewValues.put(UserDictionary.Words.FREQUENCY, "100"); mNewUri = getContentResolver().insert(     UserDictionary.Word.CONTENT_URI,   // 用户词典的content URI     mNewValues                          // 要插入的值们 );

 

新行的数据被置入一个ContentValues 对象,就像构建一个单行cursor.对象中的列们不必都是相同的数据类型,并且如果你不想指定某列的值,你可以设置一个列为null ,使用ContentValues.putNull().

 

此代码片段中没有添加_ID 列,因为此列是被自动维护的.Provider会为每个添加的新行分配一个唯一的_ID 值.Provider总是把它用作表的主键.

 

返回的content URI newUri 代表了新添加的行,以下面的形式:

 

content://user_dictionary/words/<id_value>

 

<id_value> 是新行的 _ID 的值.大多数可以自动检测content URI 的格式然后执行对此行的请求的操作.

 

要从返回的Uri,获得_ID 的值,调用ContentUris.parseId()

 

更新数据

 

 

 

要更新一行,你可以使用一个ContentValues 对象,向它填充要更新的值,就像你插入时做的,并且选择条件跟查询时是一样的.你应使用的客户端方法是ContentResolver.update().你只需把要更新的列的值添加到ContentValues 对象.如果你想去清空一列的内容,设置其值为null.

 

下面的片段改变所有语言列中带有"en"的行,把其locale置为null.返回值表明了多少行被更新:

 

// 定义一个对象包含要更新的数据 ContentValues mUpdateValues = new ContentValues(); // 为要更新的行们定义选择条款 String mSelectionClause = UserDictionary.Words.LOCALE +  "LIKE ?"; String[] mSelectionArgs = {"en_%"}; // 定义一个变量存放更新的行的数量. int mRowsUpdated = 0; ... /*  * 设置更新的值并且更新选择的单词  */ mUpdateValues.putNull(UserDictionary.Words.LOCALE); mRowsUpdated = getContentResolver().update(     UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI     mUpdateValues                       // the columns to update     mSelectionClause                    // the column to select on     mSelectionArgs                      // the value to compare to );

 

删除数据

 

 

 

删除行与获取行的方式相似:你为想要更新的行指定选择条款,然后客户端方法就会返回被删除的行数.下面的代码片段删除那些appid等于"user"的行们.返回被删除的行数.

 

// 定义要删除的行们的选择条款 String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?"; String[] mSelectionArgs = {"user"}; // 定义一个存放删除的行数的变量 int mRowsDeleted = 0; ... // 删除那些符合选择条款的单词们 mRowsDeleted = getContentResolver().delete(     UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI     mSelectionClause                    // the column to select on     mSelectionArgs                      // the value to compare to );

 转自:android Content Provider详解一
         android Content Provider详解二
         android Content Provider详解三

posted @ 2014-09-05 19:57  zuiaishenlin  阅读(200)  评论(0)    收藏  举报