Android四大组件之——内容提供者

509 阅读14分钟

本文主要简单介绍一下关于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

  1. 如果返回的数据是文本、HTML 或 JPEG 等常见数据类型,那么返回值就要是标准的MIME类型

  2. 对于指向一行或多行表数据的内容 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,就已经在基础层面分析完成。后续会在系统层面探索其中的奥秘。