本文主要简单介绍一下关于ContentProvider的一些基本概念,以及运用ContentProvider的方法和其中包含的一些概念等。内容主要基于Andorid官方文档。
内容提供者概述
内容提供程序有助于应用管理其自身和其他应用所存储数据的访问,并提供与其他应用共享数据的方法。它们会封装数据,并提供用于定义数据安全性的机制。内容提供程序是一种标准接口,可将一个进程中的数据与另一个进程中运行的代码进行联通。实现内容提供程序大有好处。最重要的是,通过配置内容提供程序,可以使其他应用安全地访问和修改自己的应用数据。
其本质在于:可以安全的跨进程访问其他程序数据,或给其他应用程序提供数据。
内容提供者的主要工作流程
一、找到目标程序的特定内容提供者
想要通过ContentProvider实现进程间数据共享,第一步肯定是要找到目标ContentProvider。那么如何才能找到目标程序的特定ContentProvider呢?答案就是通过URI(统一资源标识符)。
我们知道,URI是有其通用格式的:
[协议名]://[用户名]:[密码]@[主机名]:[端口]/[路径]?[查询参数]#[片段ID]
如:
http://example.org/absolute/URI/with/absolute/path/to/resource.txt
ftp://example.org/resource.txt
file:///home/username/RomeoAndJuliet.pdf
...
可以看出,协议名是URI中的开头部分,同样也是最为重要的一部分。在Android系统中,也针对ContentProvider特别指定了协议名:content。
通过这个特定的协议名,Android系统就可以识别出URI的目标是某个程序的ContentProvider。那么具体是哪一个程序ContentProvider呢?这就要由紧跟协议名之后的“主机名”来指定了。之所将主机名加上引号,是因为在Android系统中其实是将这一部分称作为:授权信息。
那么如果一个URI的形式为:content://com.example.mycontentprovider,系统就可以得知此URI标识的是ContentProvider,且是一个授权信息为com.example.mycontentprovider的ContentProvider。
二、目标程序的内容提供者找到目标数据源
有了协议名和授权信息,系统就能够找到目标ContentProvider。但我们的目的,是希望通过其操作我们想操作的具体数据。而在ContentProvider我们不仅可以提供数据数据,还可以提供内存数据或文件数据等几个数据源。那么这就需要在URI中能够标识出目标数据源,那么该如何标识呢?答案就是紧跟授权信息之后的路径。
这个路径不是随意的,而是由数据提供方(也就是目标ContentProvider)早已既定好的一些路径。每个路径都对应着不同的目标数据。比如我们用database1指代数据库表1,database2指代数据库表2。
那么URI:content://com.example.mycontentprovider/database1,就代表要操作的目标数据源就是数据库表1。
三、操作数据得到结果
有了特定的URI,找到了指定的ContentProvider和目标数据,接下来就是操作数据了。作为数据提供方,在得到数据获取方的请求后,首先要做的就是判断对方想要对哪个数据源进行操作。这个信息在上一步中,我们已经封装到了URI中,那么该如何从URI中获取这个信息呢?这就是接下来要出场的UriMatcher。
从其名字就可以得知作用:URI匹配器。
在上一步中我们假定了database1和database2,分别代表数据库表1和数据库表2。那么如何通过这个UriMatcher,从URI中识别出数据源呢?
首先要做的就是将数据源添加到适配器中:
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
int URI_CODE_DATABASE1 = 1;
int URI_CODE_DATABASE2 = 2;
matcher.addURI("com.example.mycontentprovider", "database1", URI_CODE_DATABASE1);
matcher.addURI("com.example.mycontentprovider", "database2", URI_CODE_DATABASE2);
其次通过匹配器从URI中匹配出具体的数据源类型:
switch(matcher.match(uri)){
case URI_CODE_DATABASE1:
获知目标数据源是数据库表1...
break;
case URI_CODE_DATABASE2:
获知目标数据源是数据库表2...
break;
}
至此,我们已经得到了具体的ContentProvider,也找到了具体的数据源,接下来就可以对数据进行操作了。对于数据的操作,无外乎四种方式:增(inset)、删(delete)、改(update)、查(query)。那么增加的数据长啥样?删除的数据是哪条?修改的条目是哪个?查询的特征又是啥?这些又该怎么制定呢?就是接下来要用到的ContentValues。
ContentValues的使用极其简单,创建然后往里面塞数据:
ContentValues values = new ContentValues();
values.put("_id",1);
values.put("name","values1");
insert(values)
or delete(values)
or update(values)
or query(values)
经过上面一系列分析,我们已经知道如何定位ContentProvider、数据源、操作方式、操作参数,那么如何通过系统,完成这一系列的行为呢?Android系统提供了ContentResolver。
在ContentResolver中,定义了与ContentProvider相同名字相同作用的四个方法:
// 外部进程向 ContentProvider 中添加数据
public Uri insert(Uri uri, ContentValues values) 
// 外部进程 删除 ContentProvider 中的数据
public int delete(Uri uri, String selection, String[] selectionArgs)
// 外部进程更新 ContentProvider 中的数据
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) 
// 外部应用 获取 ContentProvider 中的数据
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
也就是说通过ContentResolver不光可以找到目标ContentProvider,而且它还可以作为中间人,去调用ContentProvider中操作数据的方法。那么我们使用ContentProvider的逻辑到此也就很清晰了:
// 1.首先定义URI 指定是哪个ContentProvider,哪个数据源
URI uri = URI.parse("content://com.example.mycontentprovider/database1");
// 2.获取ContentResolver,借由系统这个中间人完成我们想干的事
ContentResolver resolver = context.getContentResolver();
// 3.定义操作参数
ContentValues values = new ContentValues();
values.put("_id",1);
values.put("name","values1");
// 4.一气呵成,串联整个行为(寻找contentprovider,完成insert操作,且参数为values)
resolver.insert(uri,values);
ContentProvider的创建
通过上面环节的分析,我们已经在知道了ContentProvider的大体工作流程。接下来就进深入细节,去看一下应该如何规范和高效的使用它。
首先来看的就是如何创建:
一、继承ContentProvider类
public class MyProvider extends ContentProvider {
public static final String AUTOHORITY = "com.example.mycontentprovider";
public static final int URI_CODE_DATABASE1 = 1;
public static final int URI_CODE_DATABASE2 = 2;
// UriMatcher类使用:在ContentProvider 中注册URI
private static final UriMatcher mMatcher;
static{
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 初始化
mMatcher.addURI(AUTOHORITY,"database1", URI_CODE_DATABASE1);
mMatcher.addURI(AUTOHORITY,"database2", URI_CODE_DATABASE2);
}
@Override
public boolean onCreate() {}
@Override
public Uri insert(Uri uri, ContentValues values) {
//针对指定数据源操作
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
//针对指定数据源操作
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
//针对指定数据源操作
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
//针对指定数据源操作
}
@Override
public String getType(Uri uri) {
//校验uri指定数据源
mMatcher.match(uri);
return null;
}
}
ContentProvider类的主要方法:
// 外部进程向 ContentProvider 中添加数据
public Uri insert(Uri uri, ContentValues values)
// 外部进程 删除 ContentProvider 中的数据
public int delete(Uri uri, String selection, String[] selectionArgs)
// 外部进程更新 ContentProvider 中的数据
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
// 外部应用 获取 ContentProvider 中的数据
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 
<-- 2个其他方法 -->
// ContentProvider创建后或打开系统后其它进程第一次访问该ContentProvider时,由系统进行调用
public boolean onCreate()
// 得到数据类型,即返回当前 Url 所代表数据的MIME类型
public String getType(Uri uri){
//在此匹配URI
URIMatcher.match(uri);
}
需要注意的是:
- 这4个方法由外部进程回调,并运行在ContentProvider进程的Binder线程池中(不是主线程)
- 存在多线程并发访问,需要实现线程同步: 1. 若ContentProvider的数据存储方式是使用SQLite & 一个,则不需要,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无法进行线程同步 2. 若ContentProvider的数据存储方式是内存,则需要自己实现线程同步
- 运行在ContentProvider进程的主线程,故不能做耗时操作
二、在AndroidManifest中注册
通过对ContentProvider工作流程的分析,我们知道系统查询特定ContentProvider的时候关键在于授权信息。在注册创建的ContentProvider时就需要将其填写到AndroidManifest中:
<provider
android:name="MyProvider"
android:authorities="com.example.mycontentprovider"
/>
通过上面两个步骤,就完成了ContentProvider的创建和注册。其实很明显能够看出,在创建ContentProvider时,无非就是完成这么几个方面的事情:
- 告诉系统我是谁 —— 通过授权信息告诉系统我是某某ContentProvider,通过这个授权信息标识自己
- 记录手里有哪些数据源 —— 通过实例化UriMatcher,并添加每个数据源对应的path和编码,来记录手里的资源
- 调用方通过URI找我,是为了哪个数据源的数据 —— 通过getType、create、delete、update、query方法匹配参数URI,找到目标数据源进行后续操作
- 调用方找我干啥 —— 增(create)、删(delete)、改(update)、查(query)
关于数据
ContentProvider提供数据的方式
我们知道,ContentProvider工作的核心都是围绕的数据进行的,那么它是以哪几种方式提供数据的呢?答案是两种:
文件数据 通常存储在文件中的数据,如照片、音频或视频。将文件存储在应用的私有空间内。提供程序可以应其他应用发出的文件请求提供文件句柄。
“结构化”数据 通常存储在数据库、数组或类似结构中的数据。以兼容行列表的形式存储数据。行表示实体,如人员或库存商品。列表示实体的某项数据,如人员姓名或商品价格。
调用方直接访问“结构化”数据特定项
ContentProvider可以而且主要用于SQLite数据库中存储的数据,正如之前例子中的database1和database2,对应的都是整个数据库表。实际情况是,每个数据库表中有可能存放着非常多的数据,有时我们访问数据库是针对某个特征数据(如id=1),那么该怎么办呢?
这就又要将视线挪回到URI,通过
content://com.example.mycontentprovier/database1
我们定位到了ContentProvider和database1,通过该URI访问数据,那么会返回database1的所有数据项。但是我们的目标是id=1的那条,通过URI的id标识,继续追加id,可以直接进行指定:
content://com.example.mycontentprovier/database1/1
[协议]/[授权信息]/[数据源]/[ID]
通过这样定义的URI,就可以定义要访问的数据,是database1表中id为1的数据项。从而实现精准定位,而不是再返回database1中所有的数据项。
ContentProvider在处理调用方请求时,通过URI解析出具体的id数值,执行正确的操作。
ContentUris 为了方便在原始URI进行ID的追加和通过URI读取ID,SDK中提供了此类,方便进行相关的操作:
- withAppendedId()作用:向URI追加一个id
Uri uri = Uri.parse("content://com.example.mycontentprovier/database1") ;
// 生成Uri为:content://com.example.mycontentprovier/database1/1
Uri resultUri = ContentUris.withAppendedId(uri, 1);
- parseId()作用:从URI中获取ID
Uri uri = Uri.parse("content://com.example.mycontentprovier/database1/1") ;
long personid = ContentUris.parseId(uri);
调用方如何获知数据的具体类型
既然ContentProvider提供数据的方式有两种,而且文件数据还包括很多类型,那么调用方如何获取真实的数据类型(MIME)呢?ContentProvider就此对外提供了两个方法:
getType():任何提供程序都须实现的一种必需方法,返回MIME格式(类型/子类型)的 String
如果返回的数据是文本、HTML 或 JPEG 等常见数据类型,那么返回值就要是标准的MIME类型。
对于指向一行或多行表数据的内容 URI,以 Android 特有的 MIME 格式返回 MIME 类型:
a.vnd
b.子类型部分:
如果 URI 模式用于单个行(指定id):android.cursor.item/ 如果 URI 模式用于多个行: android.cursor.dir/c.提供程序特有部分:
vnd.<name>.<type>如之前例子的授权信息是 com.example.app.provider,那么对于公开的名为database1的表。
database1 中多个行的 MIME 类型为:
vnd.android.cursor.dir/com.example.mycontentprovier.database1
对于 database1 的单个行,MIME 类型为:
vnd.android.cursor.item/com.example.mycontentprovier.database1
getStreamTypes():当提供程序提供文件时,系统要求实现的方法。
权限
ContentProvider在使用过程中,可以配置权限项,用于加强安全属性。
全权限和读写权限
通过在<provider>标签内部,使用不同的标签,可以达到对不同层次权限的约束:
<android:permission>:同时控制对整个ContentProvider进行读取和写入访问的权限<android:readPermission>:设置读权限,优先于<android:permission>-
<android:writePermission>:设置写权限,优先于<android:permission>
<android:pathPermission>:路径级权限,可以为指定的每个内容 URI 指定读取/写入权限、读取权限或写入权限,或同时指定这三种权限。
语法:
< path-permission
android:path="string"
android:pathPrefix="string"
android:pathPattern="string"
android:permission="string"
android:readPermission="string"
android:writePermission="string" />
属性:
android:path
内容提供程序数据子集的完整 URI 路径。只能授予对由此路径标识的特定数据的相应权限。用于提供搜索建议内容时,必须附加有“/search_suggest_query”。
android:pathPrefix
内容提供程序数据子集的 URI 路径的初始部分。可以授予对路径共有此初始部分的所有数据子集的相应权限。
android:pathPattern
内容提供程序数据子集的完整 URI 路径,但可以使用以下通配符: 星号("*")。此通配符匹配出现零次到多次的紧邻前面的字符的一个序列。 句点后跟星号(". *")。此通配符匹配零个或多个字符的任意序列。
android:permission
客户端要读取或写入内容提供程序的数据而必须具备的权限的名称。可以使用此属性来方便地设置适用于读取和写入的单项权限。不过,readPermission 和 writePermission 属性优先于此属性。
android:readPermission
客户端要查询内容提供程序而必须具备的权限。
android:writePermission
客户端要对由内容提供程序控制的数据进行更改而必须具备的权限。
定义:一种权限级别,即使应用没有通常需要的权限,该权限级别也能授予对应用的临时访问权限。临时访问功能可减少应用需在其清单文件中请求的权限数量。启用临时权限时,只有持续访问所有数据的应用才需要提供程序的“永久”访问权限。
如要启用临时权限,请设置
<provider>元素的 android:grantUriPermissions 属性,或者向<provider>元素添加一个或多个<grant-uri-permission>子元素。如果使用临时权限,则每当从提供程序中为某个已关联临时权限的内容 URI 移除支持时,都须调用 Context.revokeUriPermission()。
<grant-uri-permission>指定父内容提供程序有权访问的应用数据的子集。数据子集由 content: URI 的路径部分指示。 语法:< grant-uri-permission
android:path="string"
android:pathPattern="string"
android:pathPrefix="string" />
grantUriPermissions属性的值决定了可访问的提供程序范围。如果将该属性设置为 true,则系统会向整个提供程序授予临时权限,进而替换提供程序级或路径级权限所需的任何其他权限。
如果将此标志设置为 false,则必须向 元素添加 子元素。每个子元素都会指定被授予临时权限的一个或多个内容 URI。
如要向应用授予临时访问权限,Intent 必须包含 FLAG_GRANT_READ_URI_PERMISSION 和/或 FLAG_GRANT_WRITE_URI_PERMISSION 标志。需使用 setFlags() 方法对其进行设置。
如果不存在 android:grantUriPermissions 属性,则假设其为 false。
其他
ContentObserver
用于监听特定URI标识的数据,当发生更改时可以获得回调。
getContentResolver().registerContentObserver (Uri uri, boolean notifyForDescendants, ContentObserver observer)
notifyForDescendants:如果为false,则当uri指定的确切URI或路径层次结构中URI的祖先之一发生更改时,将通知观察者。如果为true,则每当路径层次结构中URI的后代发生更改时,也会通知观察者。
需要在发生数据变更的地方调用方法:getContentResolver().notifyChange(uri, null),通知变更
ContentProvider其他方法:
- ContentProviderResult[] applyBatch(String authority, ArrayList operations)
重写此方法以处理执行一批操作的请求,否则默认实现将迭代这些操作并在每个操作上调用ContentProviderOperation.apply。ContentProviderOperation描述了具体的操作行为,ContentProviderResult是针对每个操作行为得到的返回结果。
- int bulkInsert(Uri uri, ContentValues[] values)
传入ContentValues数组,遍历调用insert
- Bundle call(String method, String arg, Bundle extras)
调用者可以通过call方法直接或间接的调用指定方法,通过method可以指定方法名,arg指定定义的String参数,附加的Bunle参数。有利于扩展,使ContentProvider不限制于增删改查。
- void shutDown()
实施此操作以关闭ContentProvider实例。
至此,Android四大组件之一的ContentProvider,就已经在基础层面分析完成。后续会在系统层面探索其中的奥秘。