android基础知识05:四大组件之content provider

文章摘自《解读Content Provider之一》及其续篇

本人制作了一个content provider的实例,有兴趣可以下载看看。android content provider示例程序(简单记账)

综述
Content providers是一个Android应用程序的主要部分,主要是为应用程序提供内容。它对数据进行封装然后通过单一的ContentResolver接口提供给应用程序。只有需要在多个应用程序间共享数据时,content provider才是必须的。例如:有多个应用程序(例如:打电话程序和发短信程序等等)都需要使用联系人的数据,因此我们必须将这些数据存放在content provider。如果不需要在多个应用程序中共享数据,那么可以直接在应用程序中使用SQLiteDatabase。
当程序通过一个ContentResolver接口请求Content provider时,系统就会检查传递进来的URI的权限,并且将请求传递给拥有这些权限的content provider。content provider可以以任意的方式解析URI。UriMatcher类对于解析URIs是非常有帮助的。
对于content provider主要要实现的方法有如下几个:
onCreate():主要是初始化provider;
query(Uri, String[], String, String[], String):讲数据返回给它的调用者
insert(Uri, COntentValues):插入新的数据到content provider
update(Uri, COntentValues, String, String[]):更新content provider中的数据
delete(Uri, String, String[])删除content provider中的数据
getType(Uri):返回content provider中数据的MIME类型
数据存取方法(例如:insert(Uri, ContentValues)和update(Uri, ContentValues, String, String[]))可能会在同一时间被多个线程调用,并且是线程安全的。其他的方法(例如:onCreate())仅仅被程序的主线程调用,而且必须要避免在其中进行耗时的操作。具体的请看方法的描述。
对于ContentResolver将会自动的传给合适的ContentProvider实例,因此子类不需要担心夸进程调用的细节。
开发者指南
对于如何使用content provider的细节,请阅读Content Providers开发者指南。
下面是Content Providers开发者指南:
Content Providers存取数据,并且使这些数据对于所有的程序都是可用的。Android系统中并没有对任何程序都可使用的共同存储区域,因此,这是在多个应用程序间共享数据的唯一方法。
Android系统包含许多的对常见类型数据(音频、视频、图片以及个人联系信息等等)的content providers。你可以在android.provider包中看到一部分的content provider。你可以查询这个content provider中包含的数据(尽管对于其中的一部分数据,你必须要拥有合适的权限)。
如果你想让自己的数据对外部公开,你有两个选择:
1.你可以创建你自己的content provider(一个Content Provider子类)
2.你可以将自己的数据添加到已经存在的content provide,如果存在包含着和你想公开数据的相同类型数据的content provider,并且你拥有权限,你就可以将这些数据写入到这个content provider中。
这篇文档是介绍如何使用content provider。在简洁的讨论了一下基本后,这些讨论覆盖了如何查询一个content provider中的内容,如何修改一个content provider中包含的数据,如何为自己创建一个content provider。

Content Provider 基础知识
content provider如何存储数据是依赖于它的设计的。但是所有的content provider都实现了一个共同的接口用于查询provider并且返回结果--以及:添加,修改和删除数据等。
客户端不会直接使用这个接口,几乎都是通过ContentResolver对象。你可以通过实现在Activity中或其它应用程序组件中的getContentResolver()方法来获得ContentResolver.
ContentResolver cr = getContentResolver();
你可以通过ContentResolver的方法和感兴趣的content providers进行交互。
当一个查询操作初始化后,Android系统识别查询要进行的操作对象,并且确保它处于运行状态。系统实例化了所有的Contentprovider对象:你根本不需要进行这样的操作。实际上,你从来就不会直接和ContentProvider打交道。实际上,对于每一种ContentProvider,系统仅仅有一个实例。但是这一个ContentProvider实例却可以和处于多个程序、多个进程中的ContentResolver对象进行通信。多个进程间的交互是通过ContentResolver类和ContentProvider类进行的。
数据模型
Content providers是像数据库中的表的模型一样暴露自己所包含的数据,每一行都是一条记录,每一列都是一个特定的类型并且有特定的意义。例如:联系人的信息以及他们的电话号码就可能像如下一样存储:

android基础知识05:四大组件之content provider_第1张图片

在表中的每一条记录都含有是个数值型的_ID域,每一条记录的该域都是唯一的,用以标示这条记录。IDs可以用来匹配关联表中的记录,例如:在一个表中找到一个联系人的电话号码,在另一个表中找到该联系人的图片。
查询操作可以返回一个Cursor对象,通过这个Cursor对象你可以从一条记录移动到另一条记录,从一个列移动到另一个列来读取每个列值。
对于每一种类型的数据,他都有特定的方法读取。因此,要读取一个域的值,你必须要知道这个域的值的数据类型(关于这方面的详细内容会在查询操作以及Cursor对象中介绍)。
URIs
每一个content provider都暴露了一个公共的URI(包装成一个Uri对象)用来唯一标示它包含的数据集。对于那些包含多数据集(多个表)的content provider它会为每一个数据集提供一个URI。所有由provider暴露的URIs都是以字符串"content://"开头的。"content:"表明这个数据集是由一个content provider提供的。
如果你定义了一个content provider,那么为它的URI定义一个常量是一个比较好的做法,这样就可以简化客户端的编程并且使得将来的更新更加的彻底。Android随着平台一起为所有的providers定义了CONTENT_URI这个常量,例如:对于联系人电话号码的表的URI和存储联系人图片的表的URI(两者都是被Contacts content provider控制)分别如下:
android:provider.Contacts.Phone.CONTENT_URI
android:provider.Contacts.Photos.CONTENT_URI
URI常量在所有与content provider的交互操作中都会被使用到。每一个ContentResolver的方法的第一个参数都是URI。它代表着ContentResolver将会与哪一个provider通信,竟会操作provider中的哪一个表。
查询一个Content Provider
你需要三方面的信息来查询一个xontent provider:
1.标示provider的URI;
2.你希望接收的数据域的名称;
3.你希望操作的数据域的数据类型。
如果你是查询特定的记录,那么你还需要唯一标示这条记录的ID。
执行查询操作
要查询一个content provider,你将会使用到ContentResolver.query(0方法或者是Activity.managedQuery()方法。这两个方法使用同样的参数,都返回一个Cursor对象。然而,managedQuery()方法会使得由activity来管理返回的Cursor对象的生命周期。一个被管理的Cursor对象必须处理所有的细节,例如:在activity处于暂停状态时就要卸载自己,当activity处于重启时,又要开始执行查询操作。 你可以调用Activity.startManagingCursor()方法来管理一个还未被管理的Cursor对象。
query()和managedQuery()方法的第一参数都是provider的URI——CONTENT_URI常量标示了一个特定的ContentProvider以及其数据集。
要严格的查询到一条记录,你必须追加记录的_ID到URI上——也就是,将代表ID的字符串放置在URI的最后字段。例如:如果ID是23,那么URI就如下所示:
content://. . . ./23
这里有一些帮助的方法,特别是ContentUris.withAppendedId()和Uri.withAppendedPath()方法,他们使得追加一个ID到URI上变得更加容易。这两个静态方法都返回追加了ID的Uri对象。因此,如果你想在联系人的数据库中查找到标示为23的这条记录,你可以像下面这样构建一个查询器:

对于query()方法和managedQuery()方法的其他参数的详细介绍如下:
1.要返回的列的名称。如果指定为null,那么就会返回所有的列。否则,只有那些别列出的名字的列才会被返回。所有的content provider都有平台定义的列常量。例如:android.provider.Contacts.Phones类定义了phone表中的列的名字,就像之前介绍的_ID,NUMBER,NUMBER_KEY,NAME等等。
2.对于要返回的行的过滤器,如同SQL语句中的WHERE语句(除掉WHERE自身)。若指定为N,则返回所有的行(除非指定了只查询特定的单个记录)。
3.进行选择的参数
4.一个代表返回的所有的行排序的参数,如同SQL语句中的QRDER BY语句(除了ORDER BY自身)。若指定为null,那么就按照记录在表中的记录返回,有可能是无序的。
让我们看看一个查询并获取联系人名字和他们电话号码的例子:

这个查询操作从Contacts content provider的People表中获得数据。它获得了联系人姓名和他们的主要电话号码以及每一条记录的ID。他也将查询到的总的记录数通过_COUNT域返回。
对于每个列的名字的常量是定义在不同的接口中的——_ID和_COUNT在BaseColumns。NAME是在PeopleColumns,NUMBER是在PhoneColumns。Contacts.People类实现了所有的这些接口,这也就是为什么上面的示例中我们可以仅仅通过类名就引用这些方法。
一个查询操作的返回值
一个查询操作会返回0条或者是更多的数据库记录。列的名称,它们默认的顺序,以及他们对于每个contect provider的数据类型。每一个provider都有一个数值型的_ID列,也就是每条记录的唯一标示ID。每一个provider也可以通过_COUNT列来返回这次查询操作总共返回了多少条记录,对于返回的每条记录,_COUNT域的值都是一样的。
下面是通过查询操作返回的上面例子的结果的示例:

android基础知识05:四大组件之content provider_第2张图片

获取的数据是通过一个Cursor对象来呈现的,通过这个Cursor对象,我们可以通过这个结果集来重复的向前向后操作。你仅仅可以通过这个Cursor对象来读取数据。要想添加、修改或者是删除数据,你必须使用ContentReslover对象。
读取检索出来的数据
我们可以通过查询providers返回的Cursor对象来操作返回的数据集。如果在查询的时候指定了ID,那么这个结果集中仅仅包含一条记录。否则他就会包含多条记录(如果没有匹配的数据,那么返回的也有可能为空)。你可以读取记录的特定域,但是你必须知道你要读取的域存放的数据的数据类型,因为Cursor对象对于每一种类型的数据都有不同的方法——例如:getString(),getInt()和getFloat()。(然而,对于大多数的类型,如果你调用读取字符串的方法,那么Cursor对象就会将代表数据的字符串形式返回给你)。Cursor对象允许你从列的索引中回去列名,或者是从列名中获得索引号。
下面的例子演示了从前面的查询操作中读取联系人的名字和电话号码:

如果一个查询操作可以返回二进制数据,例如图片或者是声音,那么这些数据可能会直接记录在表中,或者是对于这类型数据可能会特殊指定一个字符串content:URI指定表的入口来允许我们对这类型的数据进行存取。一般地,少量的数据(从20K到50K或者更少的)几乎都是直接记录在表中的并且可以通过Cursor.getBlob()方法读取,他返回一个byte类型的数组。
如果表的入口是一个content:URI,你不应该直接打开或者读取(因为一件事,权限问题可能导致这个操作失败)。此时,你应该使用ContentRecesolver.openInputStream()方法获得一个InputStream对象,再通过这个InputStream对象来读去数据。

修改数据
可以通过如下方法修改被content provider保存的数据:
1.添加新的记录;
2.为已经存在的数据添加新值;
3.批量更新已经存在的记录;
4.删除记录
所有的数据修改操作都可以通过ContentResolver的方法来完成。一些content provider在修改数据的时候要求拥有比读取数据拥有更多的权限。如果没有修改content provider数据的权限,那么ContentResolver的方法就会失效。
添加记录
为了添加新的记录到content provider中,首先在ContentValues对象中设置键-值对,设置的键和content provider中的列名对应,并且值的内容就是我们想为新记录设置值。然后,通过调用ContentResolver.insert()方法,并且将要处理的content provider和设置好的ContentValues键-值对作为参数传递给这个方法。这个方法将会返回新纪录的完整的URI——也就是,要操作的provider的URI加上为新记录追加的ID号。然后,你可以用这个完整的URI来进行查询操作,获得一个指向这个新记录的Cursor,并且利用这个得到的Cursor进行后续的修改记录的操作。如下例:

添加新的值
如果一条记录已经存在,你可以添加新的信息到其中或者是修改已经存在的信息。例如:上面例子的下一步就是添加新的联系信息——例如一个电话号码或者是一个IM帐号再或者是邮箱的地址——到新的条目中。
向Contacts数据库中添加新纪录的最好的方法就是追加表名到存储新纪录的URI后,然后用修改过的URI来添加新的数据值。每一个Contacts表都会暴露出一个名字作为CONTENT_DIRECTORY常量。接下来的代码是接着上面的例子,要为新创建的记录添加一个电话号码和一个邮箱地址。

可以通过调用ContentValues.put()方法通过一个byte类型的数组将少量的二进制数据加入到表中。例如:这种方法对小的图标类型的图片和短小的音频文件是十分有效的。然而,如果你有比较大的二进制数据要添加,例如一个图片文件和一首完整的歌曲,为这个要添加的数据这是一个content:URI,然后调用ContentResolver.openOutputStream()方法,并且这个文件的URI作为这个方法的参数。(这将使得content provider使用文件来存储这些数据,并且将文件文件的路径记录在这条记录的一个隐含的域中)
在这方面,MediaStore content provider独立于图片、音频和视频文件的主要的provider,这就提供了一个便利:我们可以通过query()和managedQuery()使用一样的URI来获取二进制数据(例如:捕获到的图片)的信息,我们可以通过openInputStream()方法和刚刚获得的二进制数据的信息来获取这些二进制数据,下面的代码就显示了这个便利之处:

批量更新数据
要批量更新一组记录(例如:要将所有域的“New York”都改变为“NY”)。调用ContentResolver.update()方法,将要修改的列和数值作为参数。
删除一条记录
要删除单个记录,使用特殊行的URI作为参数,调用ContentResolver.delete()进行删除
要删除多个行(多条记录),使用要删除的记录的数据类型的URI和一条定义哪个行要被删除的SQL WHERE语句作为参数调用ContentResolver.delete()方法(例如:android.provider.Contacts.People.CONTENT_URI)。(注意:如果你要删除一般的类型,那就要确保包含了一条正确的WHERE语句,否则你就会删除比自己设想的要删除的记录更多的记录)。
创建一个Content Provider
要创建一个content provider,你必须:
1.设置一个存储数据的系统。大多数的content providers都会使用Android系统的文件存储方法或者是SQLite数据库来存储这些数据,但是你可以以任何方式存储你自己的数据。Android提供了SQLiteOpenHelper类来帮助你创建数据库和SQLiteDatabase来管理数据库。
2.扩展ContentProvider类提供存储数据的方法
3.在你的应用程序的ANndroidManifest.xml文件中声明自己的content provider。
扩展ContentProvider类
你可以定义一个ContentProvider子类将自己的数据暴露给那些通过ContentResolver和Cursor对象使用这些数据的对象。原则上,这也意味着要实现6个定义在ContentProvider类中的抽象方法:
query()
insert()
update()
delete()
getType()
onCreate()
query()方法必须返回一个Cursor对象,通过它可以操作请求的数据。Cursor是一个接口,但是Android提供了一些定义好的Cursor对象,可以供直接使用。例如:SQLiteCursor能够操作那些存储在SQLite数据库中数据。你可以通过SQLiteDatabase类的query()方法获得Cursor对象。这里有一些其他的Cursor实现——例如:MatrixCursor——对于那些不存储再数据库中的数据。
因为这些ContentProvider方法可以被处于多个进程和多个线程中的ContentResolver对象调用,因此他必须以一种线程安全的方式实现。
出于礼节,你可能希望在数据被修改的时候通过调用ContentResolver.notifyChange()来通知监听者或者用户。
除了定义一个子类外,你仍然做如下几步来简化客户端的编程,并且使得这个类更加有效:
1.定义一个命名为CONTENT_URI的public static final Uri。这就是代表你的content provider要处理的数据的完整的content:URI字符串。
你必须为这个字符串常量定义一个唯一的值。最好的方式就是使用content provider完成的类名(小写).因此,对于一个Transpotation类,我们可以如下面一样定义: public static final Uri CONTENT_URI = Uri.parse("content://com.example.codelab.transportationprovider");
public static final Uri CONTENT_URI = Uri.parse("content://com.example.codelab.transportationprovider"); 如果这个provider包含子表,那么同样需要为每一个子表定义CONTENT_URI常量,这些子表拥有相同的权限(也就是content provider的标示符),并且这些子表仅仅由他们的路径来区分,如下面示例:
content://com.example.codelab.transportationprovider/train
content://com.example.codelab.transportationprovider/air/domestic
content://com.example.codelab.transportationprovider/air/international
content://com.example.codelab.transportationprovider/train
content://com.example.codelab.transportationprovider/air/domestic
content://com.example.codelab.transportationprovider/air/international 对于content:URI的整体介绍,详看本文最后的Content URI总结部分。
2.定义content provider要返回给客户端的列名。
3.小心的记录每一列数据的数据类型,客户端需要这些信息来读取数据。
4.如果你要处理一个新的数据类型,你必须定义一个新的MIME类型作为你的COntentProvider.getType()方法的返回值。
5.如果你想添加到表中的数据太大了——一个代表大位图的文件——那么暴露数据给客户端的域必须包含一个content:URI字符串。
定义一个content provider。
要让系统知道你自己定义的content provider,那么你应该在AndroidManifest文件中使用<provider>标签进行定义。未在这里定义的content provider对于系统来说都是不可见的。
<provider>标签的name属性就是Conprovider子类的全名。authotities属性就是标示provider的content:URI的权限部分。例如,如果ContentProvider子类的名称是AutoInfoProvider,那么<provider>元素可能如下面这样定义:

注意authorities属性忽略了content:URI的路径。例如:如果AutoINfoProvider对不同类型的autos或者不同的制造商拥有子表, content://com.example.autos.autoinfoprovider/honda
content://com.example.autos.autoinfoprovider/gm/compact
content://com.example.autos.autoinfoprovider/gm/suv
content://com.example.autos.autoinfoprovider/honda
content://com.example.autos.autoinfoprovider/gm/compact
content://com.example.autos.autoinfoprovider/gm/suv 这些路径不应该在AndroidManifest文件中声明。因为权限是对provider而言的,而不是对于路径;你的provider应该可以以自己选择的方式来解释URI的路径信息部分。
<provider>的其他一些属性可以设置读取和改写数据的权限,提供一个展示给用户的图标和一条信息,使能和禁止provider等等。如果content provider中的数据不需要在多个进程中进行同步,那么就设置multiprocess属性为“true”。这就允许每一个客户端进程都实例化这个provider,建立必要的IPC。
Content URI总结
下面是对于一个content URI的重要部分的概要介绍:


A.标准的前缀,表明这些数据是被content provider持有的。这个永远不需要修改。
B.URI所属的部分,它是用来鉴别一个content provider的。对于第三方的应用程序,这必须是类的全名(必须是小写)来确保它是唯一的。所属是定义再<provider>元素的authorities属性的: <provider android:name=".TransportationProvider"
android:authorities="com.example.transportationprovider"
. . . >
<provider android:name=".TransportationProvider"
android:authorities="com.example.transportationprovider"
. . . > C.这部分是content provider用来决定是哪些类型的数据被请求。这个可以为空,也可以有几个段那么长。如果一个content provider仅仅暴露一种类型的数据(例如:仅仅是trains类型),那么他就可以为空。如果一个content provider暴露几种类型的数据,包括子类型,那么它就有可能有几段那么长——例如:“land/bus”、“labd/train”、"sea/ship"和“sea/submarine”这四种可能的类型。
D.被查询的特殊记录的ID。如果有,那么这个就是被请求记录的_ID值。如果请求不是对单个记录,那么这个段和斜杠都会被忽略:
content://com.example.transportationprovider/trains
content://com.example.transportationprovider/trains




你可能感兴趣的:(Provider,content)