概述 ContentProvider以数据表的形式向外部应用程序提供数据,这与关系型数据库中的表很类似。其中,行(row)表示由多个不同类型数据构成的单个实体,每行数据中的列(column)代表实体中的一个数据项。 例如,用户词典就是Android系统内置的Provider之一,里面记录着用户需要留存的自定义拼写规则的单词。表1例举了此Provider数据表中可以查询的字段信息: 表1:用户词典表举例 word appid frequency locale ID mapreduce user1 100hrenUS 1hrprecompiler user14 200hrfrFR 2hrapplet user2 225hrfrCA 3hrconst user1 255hrptBR 4hrint user5 100hrenUK 5hr在表1中,每行代表一个可能无法在标准词典中查到的单词。每列代表与单词相关的数据,比如首次使用时的地区(语言)。每列的标题即为存储时的列名称。引用locale列就可以得到每一行数据的地区信息。这里的ID列被用作主键(primarykey),并且是由Provider自动维护的。 注意:Provider本身不需要用到主键,主键的名称也不一定要是ID。但是,如果要把Provider作为数据源与ListView绑定,则必须有一个列的名称是ID。详细要求将在显示查询结果中描述。访问Provider 应用程序是通过客户端对象ContentResolver访问ContentProvider的。此对象中包含一些方法,这些方法将会调用Provider对象中的同名方法。而Provider对象是ContentProvider某个具体子类的实例。ContentResolver中的方法内置了基本的CRUD(创建、查询、更新、删除(create、retrieve、update和delete))功能。 ContentResolver对象运行于客户端应用的进程中,而ContentProvider运行于提供Provider应用的进程中,两者会自动完成进程间的通讯。ContentProvider还发挥着数据抽象层的作用,负责将内部数据以数据库表的形式提供出来。 注意:为了访问Provider,应用程序通常必须在Manifest文件中请求相应的权限 例如,要从UserDictionaryProvider中读取单词及地区列表,就要用到ContentResolver。query()。query()方法会去调用UserDictionaryProvider中对应的ContentResolver。query()方法。以下代码演示了ContentResolver。query()的调用过程:1查询用户词典并返回结果2mCursorgetContentResolver()。query(3UserDictionary。Words。CONTENTURI,单词表的ContentURI4mProjection,需要返回的列5mSelectionClause,查询条件6mSelectionArgs,查询条件的参数7mSortOrder);返回结果的排序要求 表2给出了query(Uri,projection,selection,selectionArgs,sortOrder)的参数与SQLSELECT语句的对应关系: 表2:Query()与SQL查询的对比 query()参数 SELECT关键字参数 说明 Uri FROMtablename Uri对应于tablename指定的Provider数据表名。 projection col,col,col,。。。 projection是包含返回列名称的数组。 selection WHEREcolvalue selection指定查询条件。 selectionArgs (没有固定值,该查询参数将会替换查询语句中的占位符?。) sortOrder ORDERBYcol,col,。。。 sortOrder指定了返回Cursor中各行的显示顺序。ContentURI ContentURI是一种用于标识Provider数据的URI。ContentURI包括了整个Provider的符号名称(authority)和表名(path)。调用客户端的方法访问Provider数据表时,表的ContentURI是参数之一。 在前面的代码中,常量CONTENTURI包含了指向用户词典中word表的ContentURI。ContentResolver对象将分离出URI中的authority,并用它解析出Provider,这是通过将authority与系统记录的已有Provider清单进行比较来实现的。然后ContentResolver就可以将查询参数发送给相应的Provider了。 ContentProvider用ContentURI的path部分选择要访问的数据表。通常,Provider公开的所有数据表都会带有自己的path。 在上述代码中,word表的完整URI为:content:userdictionarywords 这里的字符串userdictionary是Provider的authority部分,字符串words是数据表的path部分。字符串content:(scheme)是必须指定的,以表明这是一个ContentURI。 很多Provider提供了对单条记录的访问能力,只要在URI后面跟一个ID值即可。例如,要读取用户词典中ID为4的数据行,可以使用以下ContentURI:UrisingleUriContentUris。withAppendedId(UserDictionary。Words。CONTENTURI,4); 如果已经读取了一些数据,然后需要修改或删除其中的某一条,这时就经常会用到ID值了。 注意:Uri和Uri。Builder类中已内置了一些工具性的方法,可以由字符串搭建合乎规则的Uri对象。ContentUris中有一些在URI后面追加ID值的常用方法。上述代码就用了withAppendedId()把ID追加到UserDictionary的ContentURI之后。从Provider读取数据 本节将介绍从Provider读取数据的过程,还是以UserDictionaryProvider为例。 为了清晰起见,本节中的代码将会调用UI线程中的ContentResolver。query()。但是在实际的代码中,应该在单独的线程中实现异步查询。一种方案是利用CursorLoader类,而且,以下只给出了部分代码,而非一个完整的应用程序。 从Provider中读取数据的基本步骤如下所示:申请读取Provider的权限。编写向Provider发送查询请求的代码。申请读取权限 要从Provider读取数据,应用程序需要拥有对Provider的读权限。在运行时是无法申请该权限的,只能在Manifest文件中通过指定。在Manifest文件中的定义,实际上是表明此应用程序需要申请该权限。这样用户在安装此应用程序时,就可以明确授权。 在Provider的参考文档中,给出了其用到的全部权限的准确名称。 UserDictionaryProvider在其Manifest文件中定义了android。permission。READUSERDICTIONARY权限,因此要读它的应用程序就必须请求该权限。构建查询 接下来是构建查询请求。以下代码定义了一些变量,在访问UserDictionaryProvider时将会用到:1projection定义了要返回的数据列2String〔〕mProjection3{4UserDictionary。Words。ID,n对应列名为ID的ContractClass常量5UserDictionary。anclasstypWords。WORD,对应列名为word的ContractClass常量6UserDictionary。anclasstypWords。LOCALEnbLOCALE对应列名为local的ContractClass常量7};89定义存放查询条件的字符串10StringmSelectionClauseanclassplnnull;s;1112初始化存放查询参数的数组13String〔〕anclassplnmSelectionArgs{}; 接下来的代码演示了ContentResolver。query()的使用方法,这里以UserDictionaryProvider为例。Provider客户端查询与SQL查询很类似,也包含了需返回的列名、查询条件和排序要求。 查询返回的列名集合对象被称为投影(Projection)(即变量mProjection)。 查询数据的表达式被拆分为查询条件和查询参数。查询条件是由逻辑布尔表达式、列名、数值组成(即变量mSelectionClause)。如果用参数?代替了具体数值,则查询方法将会从查询参数数组(变量mSelectionArgs)中读取实际的值。 在以下代码中,如果用户没有输入单词,则查询语句将被置为null,这样查询将会返回Provider中的所有单词。如果用户输入了单词,那么查询语句将会是UserDictionary。Words。WORD?,且查询参数数组中的第一个成员被设为用户输入的单词。12定义只有一个成员的字符串数组,用于存放查询参数。34String〔〕mSelectionArgs{};56从用户界面读取一个单词7mSearchStringmSearchWord。getText()。toString();89别忘了在这里添加检查输入内容是否非法或恶意的代码1011如果单词为空字符串,则读取所有数据12if(TextUtils。isEmpty(mSearchString)){13将查询语句设为null将返回所有数据14mSelectionClausenull;15mSelectionArgs〔0〕;1617}else{18由用户录入单词构建查询语句19mSelectionClauseUserDictionary。Words。WORD?;2021将用户录入的字符串置入查询参数数组中22mSelectionArgs〔0〕mSearchString;2324}2526查询数据并返回游标(Cursor)对象27mCursorgetContentResolver()。query(28UserDictionary。Words。CONTENTURI,单词表的ContentURI29mProjection,需返回的列30mSelectionClause为null或是用户录入的单词31mSelectionArgs,为空或是用户录入的字符串32mSortOrder);定义返回数据的排序规则3334在出错时,某些Provider返回null,另一些会抛出异常35if(nullmCursor){3637在这里插入处理错误的代码。38请勿在这里使用游标!39可能需要调用4041如果游标中没有内容,表示Provider没找到匹配的记录。42}elseif(mCursor。getCount()1){434445在这里插入通知用户查询失败的代码。46这不一定是出错了,可以让用户录入新记录,也可以重新输入查询条件。474849}else{50在这里插入处理查询结果的代码。5152} 查询的语句与以下SQL语句类似:SELECTID,word,localeFROMwordsWHEREworduserinputORDERBYwordASC; 这条SQL语句中使用的是真实的列名,而不是Contract类常量。防止非法输入 如果ContentProvider管理的数据存放于SQL数据库中,那么在SQL语句中插入某些非法信息可能会引发SQL注入问题。 请看下面这条查询语句:将用户输入内容拼接在列名之后,构造一条查询语句。StringmSelectionClausevarmUserInput; 这时,用户就可以将恶意SQL拼接到查询语句中。比如,用户可以将mUserInput输入为nothing;DROPTABLE;,这样查询语句就会成为varnothing;DROPTABLE;。因为查询语句将用作SQL语句,所以会导致Provider删除底层SQLite数据库中的所有数据表(除非Provider设置为捕获SQL注入异常)。 为了避免这类问题,可以在查询语句中使用?作为可替代参数,并用另一个数组作为实际的参数值。这样,用户的输入就与查询直接关联,而不会被解释为SQL语句的一部分。因为不再用作SQL语句,用户输入就无法注入恶意SQL了。用户的输入内容不直接用于拼接SQL语句,查询语句如下:用可替代参数构造查询语句StringmSelectionClausevar?; 查询参数数组定义如下:定义存放查询参数值的数组String〔〕selectionArgs{}; 在数组中放入一个查询参数值:将查询参数赋为用户的输入值selectionArgs〔0〕mUserInput; 在构造查询时,推荐使用这种将?作为形参、数组提供实参的查询语句,即使不是基于SQL数据库的Provider也可以使用。显示查询结果 客户端方法ContentResolver。query()将返回一个Cursor,其中的数据列由对应查询条件的Projection指定。Cursor对象支持对数据行和数据列的随机读取。通过Cursor的内部方法,可以遍历结果数据行、获取每一列的数据类型、读取某一字段的数据并检查其他属性。某些Cursor对象可以在Provider的数据发生变化时进行自动更新,或是在Cursor数据变动时触发其他监听对象的方法。 注意:根据建立查询的对象性质,Provider可以限制对数据列的访问。比如,联系人Provider就不允许SyncAdapter访问某些数据列,也就不会在Activity和服务中返回这些列。 如果没有找到符合条件的数据,Provider就会返回一个Cursor。getCount()为0的Cursor对象(即空游标)。 如果发生了内部错误,查询返回的结果将视Provider的不同而定。可能是返回null,也可能抛出一个Exception。 因为Cursor是一个数据行的列表,所以一种较好的显示方式就是通过SimpleCursorAdapter把它与ListView关联起来。 以下代码将延续上面的代码。创建了一个含有Cursor的SimpleCursorAdapter对象,并将其设置为一个ListView的数据源适配器(Adapter):1定义需要从Cursor读取并显示出来的数据列2String〔〕mWordListColumns3{4UserDictionary。Words。WORD,对应word列的Contract类常量5UserDictionary。Words。LOCALE对应locale列的Contract类常量6};78定义ViewID列表,用于保存Cursor返回的一行数据。9int〔〕mWordListItems{R。id。dictWord,R。id。locale};1011新建一个SimpleCursorAdapter对象12mCursorAdapternewSimpleCursorAdapter(13getApplicationContext(),应用程序的Context对象14R。layout。wordlistrow,XML格式的Layout,用于ListView中每一行的布局15mCursor,查询结果16mWordListColumns,字符串数组,存放游标中的列名17mWordListItems,整形数组,存放行布局中的ViewID180);标志位(一般用不上)1920设置ListView的Adapter21mWordList。setAdapter(mCursorAdapter); 注意:要将Cursor用作ListView的后台数据源,游标必须包含一个名为ID的数据列。因此,上述查询从word表中读取了ID列,当然ListView并不会显示这个字段。这也是大部分Provider中的数据表都带有ID列的原因所在。从查询结果中读取数据 查询结果不只是简单地用于显示,还可以用来完成其他操作。比如,可以从用户词典中读取单词并在其他Provider中进行检索。这时就需要遍历Cursor中的每行数据:1找到列名为word的字段编号2intindexmCursor。getColumnIndex(UserDictionary。Words。WORD);345仅当游标可用时才会执行。6如果发生内部错误,UserDictionaryProvider将会返回null。而其他Provider可能会抛出异常。789if(mCursor!null){1011前进至下一行。12在第一次移动之前,记录指针为1,如果这时读取数据,将会触发异常。1314while(mCursor。moveToNext()){1516读取值17newWordmCursor。getString(index);1819在这里插入处理返回单词的代码2021。。。2223while循环结束24}25}else{2627如果游标为空或Provider抛出异常,在这里插入显示错误的代码。28} Cursor中有很多用于读取不同类型数据的get方法。例如,上述代码中用到了getString()。还有一个getType()方法用于返回字段的类型。 本文源代码获取方式:私信发送底层源码即可免费获取