解读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数据库中。
  • 存储在文件中。
  • 存储在网络中。

这一部分可以参考我翻译的文章:Android数据存储方案

注意事项

  • 表数据通常需要主键,provider为每行赋值一个唯一的数值。尽管主键这一列可以任意名称,但是推荐使用BaseColumns._ID,这样的话,在ListView就能很方便的检索。
  • 若提供位图或者更大的文件数据的话,这些数据会保存在文件中,以间接地方式提供。若是使用这些数据的话,我们应该通知外部应用程序它们应该使用ContentResovler类中的文件方法来获取这些数据,例如openInputStream(Uri uri)openOutputStream(Uri uri)等方法。
  • 存储BLOB数据类型的话大小或结构会发生变化。我们可以使用BLOB实现一个模式独立的表,该类型的表,我们需要定义一个主键,一个MIME类型列,一个或多个BLOB类型的列。在BLOB列中数据的含义是通过MIME类型列中的值表示,这种方式允许在相同的表中存储不同的行类型。

设计内容URIs

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

设计权限authority

权限用于区分不同程序的provider,一般为了避免冲突,都会采用包名的形式命名。例如包名为:com.example.sywyg,则该权限就可以为:com.example.sywyg.provider。可以在AndroidManifest配置文件中的<provider>标签中设置<android:authority>标签。

设计路径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的匹配。常用代码如下:


public static UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(authority,path,customMatch);

addURI()能够将字符串authority和字符串path解析为:content://<authority>/<path>(这里的path中可以带有id),然后通过match(URI)方法进行匹配字符串customMatch,match(URI)返回的就是对应URI的customMatch(addURI()中的第三个参数),利用这个参数就可以响应调用者期望访问的数据。

其它的类,如ContentUris,Uri和Uri.Builder。ContentUris可以方便的在uri后面添加id,Uri和Uri.builder能够方便的解析Uri对象以及生成新的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相关的MIME类型。

对于常见的类型: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。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Android Studio ContentProvider 是一种在 Android 应用程序中提供数据访问的机制。ContentProvider 通过实现标准接口,允许应用程序将数据共享给其他应用程序。例如,一个应用程序可以使用 ContentProvider 来提供它的数据给其他应用程序使用,而不需要直接暴露数据库或文件。ContentProviderAndroid 应用程序中非常有用的一个组件,它可以帮助应用程序更好地管理和共享数据。 ### 回答2: Android Studio ContentProviderAndroid 框架中的一个关键组件,它提供了一种标准化的方式访问应用程序数据存储系统。ContentProvider 允许一个应用程序共享数据给其他应用程序,同时也提供了一种集中管理数据访问的方式。 ContentProviders 通过封装数据的方式,抽象出数据的访问接口,为应用程序提供了一种标准的、统一的数据访问方式。应用程序可以使用 ContentResolver 通过 Uri 访问提供程序中的数据。ContentSupplier 将数据存储在持久性存储介质(例如 SQLite 数据库、内存、文件、网络、 content provider service 等)中,提供客户端应用程序通过 ContentResolver 访问的统一接口,无论数据如何存储,使用方式都是一致的。 在 Android Studio 中,创建 ContentProvider 可以使用 Android Studio 提供的模板,可以在创建应用程序时选择“空 Activity”,然后在下一个页签中选择“Content Provider”模板。通过模板创建ContentProvider 包含了两个部分:Contract 和 Provider 实现。 Contract 定义了该 ContentProvider 中的数据类型,包括数据表、列名等信息。Provider 实现包括了 ContentProvider 中的基本功能,比如实现 insert()、delete()、update()、query() 等方法,实现 UriMatcher 解析,处理来自外部应用程序的数据请求。 ContentProvider 可以实现在不暴露底层数据存储方式的前提下,提供独立的、可扩展的数据提供服务,这为多个应用程序之间共享数据提供了一种很好的机制。在应用开发过程中,需要注意数据访问权限、数据安全、Uri 注册等问题,以确保 ContentProvider 提供的数据能够被合法访问和使用。 ### 回答3: Android Studio ContentProviderAndroid平台提供的一种数据共享方式,它允许多个应用程序访问同一份数据而不会相互干扰。每个ContentProvider对应一个特定的数据源,开发者可以在ContentProvider中定义URI(Uniform Resource Identifier),用于标识不同的数据源。 在Android Studio中创建ContentProvider非常简单,只需通过New->Other->Content Provider创建即可。创建ContentProvider完成后,需要定义provider的各种属性,如权限、支持的数据类型等。定义完成后,就可以在应用程序中访问数据了。开发者可以使用ContentResolver类来访问ContentProvider中定义的数据。ContentResolver是Android中提供的类,用于管理应用程序和ContentProvider之间的数据交互。 ContentProviderAndroid中非常重要的一个组件,它可以实现数据共享、数据提供和数据访问等功能。开发者需要谨慎地设计ContentProvider,特别是在涉及敏感数据时,需要仔细考虑安全性和权限控制。此外,ContentProvider还需要考虑数据格式的兼容性和数据的有效性等问题。 总之,Android Studio ContentProviderAndroid平台提供的一种强大的数据共享方式,可以实现多个应用程序之间的数据交换和共享。开发者需要根据具体需求仔细设计ContentProvider,确保其安全性和数据的有效性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值