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客户端对象来操作provider.providers 和provider客户端一起提供了一致的,标准的接口来操作用于进程间通讯的数据并保处数据的安全性.
本节讲解以下基础知识:
· content providers如何工作.
· 从content provider取得数据的API.
· 向content provider插入,更新以及删除数据的API.
· 其它有助于使用providers的API.
概述
一个content provider代表了面向外部应用的数据,这些数据看起来就像关系型数据库中的一个或多个table.一行代表某种数据类型的一个实例,一列代表这个实例的一个属性或字段.
举个例子,Android平台中的一个内建的provider是用户词典,它存储了用户想保存的非标准词的拼写.表1 演示了数据在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,须依如下步骤:
1 请求provider的读权限.
2 定义发送请求到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会禁止同步适配器操作某些列,所以它不会把它们返回给一个activity或service.
如果没有符合选择条件的行,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支持ListView,cursor必须包含一个叫做_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 { // 如果cursor为null或前面抛出了异常,在处插入代码报告错误. }
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 客户端与provider的ContentProvider以交互方式修改数据.你调用个ContentResolver 的方法,其参数是要传给 ContentProvider对应方法的.Provider与provider客户端自动处理安全问题和进程间通信问题.
插入数据
要向一个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详解三

浙公网安备 33010602011771号