当前位置: 代码迷 >> Android >> 解读Android之ContentProvider(二)创建自己的Provider
  详细解决方案

解读Android之ContentProvider(二)创建自己的Provider

热度:105   发布时间:2016-04-27 23:48:15.0
解读Android之ContentProvider(2)创建自己的Provider

本文翻译自android官方文档,结合自己测试,整理如下。

content provider管理数据的访问,我们可以在自己的应用程序中实现一个或多个自定义的provider(通过继承抽象类ContentProvider),当然这些provider需要在manifest文件中注册。尽管content provider是用来为其它程序来访问数据的,但是在自己程序中的activities显然可以对这些数据进行处理。

创建provider之前注意事项

确定是否需要提供content provider。若有以下一种或多种需求的话需要创建content provider:

  • 想提供给其他程序复杂的数据或文件;
  • 想允许用户将复杂的数据从我们的程序复制到另外的程序中;
  • 想使用查询框架提供自定义查询建议。

若在程序内部使用SQLite数据库,则不需要provider。

接下来,通过下列步骤创建provider(先简单的总结,后续详细介绍):

  1. 为数据设计存储方式,content provider提供两种方式:

    • 文件数据
      数据保存在文件中,例如图片,视频,音频等。这些文件存储在私有的空间,provider可以提供外部程序访问。
    • 结构化的数据
      这样的数据通常保存在数据库,数组,或者相似的结构中,当然可以把这些数据以兼容的方式保存在表中。表中的一行表示一个实体(记录),一列表示实体相关属性的取值。通常这些数据保存在SQLite数据库中,当然也可以永久地保存。
  2. 需要继承抽象类ContentProvider,并且覆盖必要的方法。这个类是我们的数据和其他程序交互的接口。

  3. 定义provider的权限(authority),内容URIs和列名。若程序想要处理intents,则还必须定义intent的action,extra data和flags。同样需要定义其它程序想要访问该provider必须请求的许可(permission)。通常可以考虑把这些值定义为常量并定义在另一个类中。
  4. 添加额外的信息。

设计数据存储

在提供provider之前,我们必须要确定我们的数据该如何存储,当然存储方式我们可以任意指定,然后再针对该存储方式设计provider。

有下列几种存储方式:

  • 存储在SQLite数据库中。这种方式我们不是必须要使用一个数据库,provider对外表现为一组表,因此不需要内部实现相关的数据库。
  • 存储在文件中。
  • 存储在网络中。

注意事项

  • 表数据通常需要主键,provider为每行赋值一个唯一的数值。尽管主键这一列可以任意名称,但是推荐使用BaseColumns._ID,这样的话,在ListView就能很方便的检索。
  • 若提供位图或者更大的文件数据的话,这些数据会保存在文件中,以间接地方式提供。若是使用这些数据的话,我们应该通知客户端它们应该使用ContentResovler类中的文件方法来获取这些数据。
  • 存储BLOB数据类型的话大小或结构会发生变化。

设计内容URIs

内容URI是能够识别provider中数据的URI,包括权限(authority)和路径(path)。权限找到provider,路径找到表或文件。还可以有一个id,能够表示某一行。

设计权限authority

权限用于区分不同程序,一般为了避免冲突,都会采用包名的形式命名。例如包名为:com.example.sywyg,则该权限就可以为:com.example.sywyg.provider

设计路径path

URI是权限加路径的方式来查找指定的表。路径是区分同一程序中表或者其它形式(例如文件)的,可以直接添加在权限后面。例如table1和table2,则形成的URI分别为:com.example.sywyg.provider/table1com.example.sywyg.provider/table2

最后内容URI需要在权限和路径前加上content://表示内容URI。例如,一个标准的内容URI写法如下:content://com.example.sywyg.provider/table1

处理URI的ID

将ID追加到URI后面的话就可以检索到表中的指定的行,ID对应的列名为_ID。

URI模式匹配

UriMatcher类映射内容URI模式到一个integer类型数,我们可以使用该值进行模式匹配。

URI模式通过通配符匹配:

  • *:匹配任意长度和有效的字符串;
  • #:匹配任意长度的数字字符;

假设权限为:com.example.app.provider,识别下面的URI对应的表:

content://com.example.app.provider/table1: 表为 table1.content://com.example.app.provider/table2/dataset1: 表为 dataset1.content://com.example.app.provider/table2/dataset2: 表为 dataset2.content://com.example.app.provider/table3: 表为 table3.

若带有ID同样可以识别:
content://com.example.app.provider/table3/1 表table3中的第1行

下面的URI模式:
content://com.example.app.provider/*
匹配provider中的任意URI

content://com.example.app.provider/table2/*将会匹配表dataset1和dataset2,但是不会匹配table1或table3。

content://com.example.app.provider/table3/#将会匹配table3中的任意行。
content://com.example.app.provider/table3/6将会匹配table3中的第6行。

总结起来,URI标准就是:content:///或content:////,前者针对表,后者针对指定行。

我们可以借助UriMatcher类快速实现内容URI的匹配。常用代码如下:

继承ContentProvider类

ContentProvider类能够管理我们provider中的数据,外界通过ContentResovler可以调用对应的ContentProvider方法实现操作数据。因此,我们必须要提供相应的方法来操作数据。

覆盖方法

我们需要实现以下方法,才能方便ContentResovler访问数据。

  1. query()
    检索数据,返回Cursor对象。
  2. insert()
    插入一行数据,返回新插入行的URI。
  3. update()
    更新存在的某行,返回更新的行号。
  4. delete()
    删除存在的某行,返回删除的行号。
  5. getType()
    返回对应URI的MIME类型。
  6. onCreate()
    初始化provider。当创建provider对象时,就会立即调用。注意只有ContentResovler对象试图访问数据时才会创建provider对象。

可以看到对于上述几个操作数据的方法,在ContentResovler有同样名称的方法。

在覆盖方法时需注意一下几点:

  • 除了onCreate()之外的方法都要注意多线程安全问题。
  • 避免在onCreate()进行长时间的操作。直到需要时再初始化对应的内容。
  • 尽管我们需要实现这些方法,但是除了返回值外,我们可以不用做任何事。例如我们不希望外界删除数据,因此我们只需要返回对应的行号,而不在方法中写任何代码。

实现query()方法

该方法会返回一个Cursor对象,或者失败的话抛出异常。若没有查到对应的行则应该返回一个getCount()方法为0的Cursor对象。只有在内部出现错误是才返回null。若使用SQLite数据库保存数据的话,可以直接调用SQLiteDatabase类的query()方法返回Cursor对象。若不使用的话,就要使用Cursor类的具体子类。

在查询时可能会抛出下列异常:

  • IllegalArgumentException(当接收到无效URI时)
  • NullPointerException

若访问的是SQLite数据库,则query()简单实现代码如下:

public class ExampleProvider extends ContentProvider {    private static final UriMatcher sUriMatcher;    sUriMatcher.addURI("com.example.app.provider", "table3", 1);    sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);    // 参数为ContentResovler调用query()方法传递过来的    public Cursor query(        Uri uri,        String[] projection,        String selection,        String[] selectionArgs,        String sortOrder) {        switch (sUriMatcher.match(uri)) {            // 对应table3            case 1:                if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";                break;            // 对应带有id的table3            case 2:                selection = selection + "_ID = " uri.getLastPathSegment();                break;            default:                throw new IllegalArgumentException("Unknown URI " + uri);        }        // 实际查询SQLite数据库语句    }

实现insert()方法

插入方法将新的一行添加到指定的表中,数据来自于参数ContentValues 传递的数据,若某一列没有指定,则提供默认值(该默认值取决于provider或数据库本身)。

该方法会返回新行的URI。我们可以通过ContentUris的withAppendId()方法在URI后添加主键ID(通常是_ID)来构建该URI。直接通过parse()方法也行。

实现update()方法

和插入类似,不再介绍。

实现delete()方法

删除指定的行。该方法不需要真的删除某行,若我们使用同步适配器的话,可以考虑先把要删除的数据进行标记删除。该同步适配器可以在从provider中真的删除数据之前能够检查出删除的行,并把这些排除,实现假删除。

实现getType()方法

将在下面部分详细讲解。

实现onCreate()方法

当创建provider时,系统调用onCreate()方法。我们应该保证在这里初始化的内容是必须的且能够快速执行,对于不是必须的且耗时的可以在需要时再初始化。例如数据库创建以及数据加载可以在真正请求操作数据时再执行。若太耗时的话,provider启动就会耗时,显然这会影响回应请求该provider的程序。

ContentProvider的MIME类型

ContentProvider有两个方法能返回类型:

  • getType()
    这个需要在子类中实现
  • getStreamTypes()
    若provider提供文件访问的话,就需要实现这个。

表的MIME类型

getType()方法返回一个MIME格式的字符串,用来描述参数URI对应的 数据类型。参数Uri可以匹配一个模式而不是指定的URI,这样我们应该返回和匹配模式的URIs相关的数据类型。

对于常见的类型:text,HTML,JPEG,getType()方法应该返回标准的MIME类型。

对于指定一行或多行的URIs,getType()方法应该返回android指定的MIME格式:

  • 类型部分:vnd
  • 子类型部分:
    • URI模式只有一行:android.cursor.item/
    • URI模式有多行:android.cursor.dir/
  • Provider指定部分:vnd..
    name应该是全局唯一的,type应该是对应URI模式唯一的。通常,name应该是包名,type应该是和URI相关的表名。
    例如,provider权限为com.example.app.provider,表名为:table1,则table1多行的MIME类型为:
    vnd.android.cursor.dir/vnd.com.example.provider.table1
    单行的MIME类型为:
    vnd.android.cursor.item/vnd.com.example.provider.table1

文件的MIME类型

若provider提供文件的话,需要实现getStreamTypes()方法。该方法返回一个MIME类型的字符串数组,根据给定的URI。我们应该根据MIME类型过滤参数过滤MIME类型,以便返回客户端想处理的MIME类型。

例如,provider提供图片文件:.jpg,.png和.gif格式。若一个程序调用了getStreamTypes()使用过滤参数image/*,那么该方法就会返回:
{ "image/jpeg", "image/png", "image/gif"}
若程序只需要.jpg的话,可以使用过滤参数*\/jpeg,则方法返回:
{"image/jpeg"}
若provider不提供类型的话,方法返回null。

实现相关类

一般需要一个public final类型的相关类来定义常量:URIs,列名,MIME或者其他和provider相关的信息。该类使provider和其它程序建立一种关系,能够确保provider被正确地获取。

对于其他程序来说,可以通过我们提供的.jar文件来操作这个相关类,进而实现操作provider。

实现Content Provider许可

需要注意一下几点:

  • 默认情况下,存储在设备内存(并不是运行内存)数据文件是我们程序和provider私有的。
  • SQLite数据库是我们程序和provider私有的。
  • 默认情况,保存在存储卡上的数据是公有的都可以访问。我们不能使用provider限制该数据的访问权限。
  • 打开或创建存储内存上的一个文件或SQLite数据库的方法调用潜在地授予了其它程序读写这些数据的权限。若是使用存储内存上的数据作为provider的数据集的话,其它程序都有读写的权限,而我们在manifest设置的将不起作用。默认的获取数据是私有的,不应该改变。

若我们想使用content provider权限控制数据的读取,我们需要把数据存储在内部文件,SQLite数据库或服务器中,并且确保这些文件和数据库是私有的。

实现许可

默认情况下,provider没有设置许可,所有的程序都能获取provider数据。我们可以在mainfest文件中<provider>标签的属性或子标签配置。许可可以针对整个provider或者特定的表或者特定的记录配置。

声明许可使用<permission>标签,例如:
<permission android:name="com.example.app.provider.permission.READ_PROVIDER">

下面描述了provider的详细许可设置:

  • 单个provider读写许可(Single read-write provider-level permission)
    该许可是控制整个provider的读写许可。在<provider>标签中的android:permission属性中设置。
  • provider级别分开的读或写权限(Separate read and write provider-level permission

    <provider>标签中的android:readPermission属性中设置读许可;在<provider>标签中的android:writePermission属性中设置写许可。这两个许可优于android:permission设置的许可。
  • 路径级别的许可(Path-level permission)
    读或写或读写指定URI的许可。在<provider>标签中的<path-permission>子标签中设置。该级别权限优于上面的两个许可。
  • 临时许可(Temporary permission)
    授予程序临时获取数据的许可。
    <provider>标签中的android:grantUriPermissions属性中设置,或者在<provider>标签中的<grant-uri-permission>子标签中添加一个或多个。
    若使用了临时许可,每当从provider移除对一个URI的支持时,必须调用Context.revokeUriPermission(),该URI和临时许可相关。若该属性设置了true,则系统支持授权临时许可,且会覆盖其它任何许可(provider级别或路径级别的)。
    若设置了false,就需要在<provider>标签中的<grant-uri-permission>子标签中添加一个或多个。每一个子标签指定一个或多个URIs有临时被访问的许可。

    为了在一个程序中委托临时访问许可,intent必须包含FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION中的一个或两个flags(通过setFlags()设置)。

    若没设置android:grantUriPermissions属性,则被认为是false。

<provider>标签

我们知道四大组件都需要在mainfest文件中配置,ContentProvider的实现类通过<provider>标签配置。该标签中还包括一些重要的属性和子标签:

  • android:authorities
    用于在这个系统中识别provider(先识别应用程序)。
  • android:name
    ContentProvider的实现类类名。
  • Permission
    上面已经详细描述,主要包括:

    - `android:grantUriPermssions`;- `android:permission`;- `android:readPermission`;- `android:writePermission`。
  • 启动和控制属性

    • android:enabled 是否允许实例化
    • android:exported 外部是否能使用
    • android:initOrder integer值,表示在同一个进程中被初始化的顺序,值越大越早被初始化
    • android:multiProcess 是否允许在多个进程中实例化
    • android:process 所在的进程
    • android:syncable
  • 信息属性

    • android:icon 图标
    • android:label 名称

总结

ContentResovler和ContentProvider之间的协作关系以查询SQLite数据库为例进行描述:

ContentResovler对象的query()方法中的参数URI,通过URI中的权限authority可以找到对应的ContentProvider实现类,对该类实例化并调用query()方法,在query()方法中通过UriMatcher.match()方法匹配Uri,匹配成功后交给SQLite数据库的查询方法,并返回Cursor,然后通过ContentProvider实例返回该Cursor给调用者。可以看到通过权限可以确定一个provider的,因此一个程序中可以包含多个providers。
遗留问题:

  • 多线程安全问题。
  相关解决方案