英文原文: http://developer.android.com/guide/topics/providers/content-provider-creating.html
采集日期:2015-01-23
Content Provider 管理着数据库的访问工作。 依据 Manifest 中的定义, Provider 实现为 Android 应用中的一个或多个类。 其中一个类实现了 ContentProvider
的子类,作为连接 Provider 与其他应用程序的接口。 虽然 Content Provider 是用于向其他应用程序提供数据的,但在其所在的应用程序中,用户当然也可以通过 Activity 查询并修改 Provider 中的数据。
本文列出了建立一个 Content Provider 的基本步骤,并给出了涉及的 API。
在建立 Provider 之前,请完成:
如果只是在应用程序内部使用, Provider 不一定 要用到 SQLite 数据库。
接下来,按照以下步骤建立自己的 Provider:
ContentProvider
类,并实现必要的方法。 此类为访问数据的途径,更多信息请参阅实现 ContentProvider 类一节。 AbstractThreadedSyncAdapter
类。 Content Provider 是为数据提供结构化访问途径的接口。 在创建这个接口之前,首先必须确定数据的存储形式。 数据可以用任何方式存储,只要设计好必要的的读写接口即可。
以下列出了一些 Android 支持的数据存储形式:
SQLiteOpenHelper
是用于创建数据库的工具类, SQLiteDatabase
是访问数据库的基础类。
请记住,并不是一定要用数据库来存储数据。 Provider 向外部展现数据的形式是与关系型数据库类似的表格,但内部不一定非要这么去实现。
java.net
和 android.net
中的类。 还可以先将网络端数据与本地数据库进行同步,再以数据表或文件的形式提供出来。 Sync Adapter 示例 应用就给出了这种同步方式的例子。 以下列出了一些设计 Provider 的数据结构时需要考虑的因素:
BaseColumns._ID
,因为 ListView
与 Provider 关联时要求查询结果中必须包含名为 _ID
的列。 ContentResolver
的文件方法来访问数据。 还可以用 BLOB 来实现 库结构无关 (schema-independent)的表。 在这种表中,可以定义一个主键、一个 MIME 类型字段和多个 BLOB 之类的通用字段。 BLOB 字段中的数据含义由 MIME 类型字段来指明。 这样,就可以在同一张表中存放多种不同类型的数据行了。 Contracts Provider 的 “data”表 ContactsContract.Data
就是库结构无关的。
Content URI是用于标识 Provider 数据的 URI。 Content URI 包含了整个 Provider 的名称( authority )和某个表或文件的名称( path )。 可选的 id 部分标明了表中的数据行索引。 ContentProvider
中所有的数据访问方法都包含一个 Content URI 参数,指明了要访问的表、数据行或文件。
文章 Content Provider 基础 中介绍了 Content URI 的基础知识。
Provider 通常都需要指定一个 authority ,用作 Android 内部名称。 为了避免与其他 Provider 冲突,请使用 Internet 域名(反向)作为 Provider 的 authority 基础。 因为这也是 Android 包(package)的命名建议,所以可在 Provider 所在包名的基础上进行扩展来定义其 authority 。 比如,假设 Android 包名为 com.example.<appname>
, 则 Provider 的 authority 就可以定义为 com.example.<appname>.provider
。
开发人员创建 Content URi 的方式,通常是在 authority 后面添加指明了表名的 path 部分。 比如,假设已有两张表 table1 和 table2 , 则利用上述 authority 示例生成的 Content URI 即为: com.example.<appname>.provider/table1
和 com.example.<appname>.provider/table2
。 可以有多个 path 部分,每个 path 中并不一定包含表。
作为约定,通过在 Content URI 的末尾附带该行数据的 ID,Provider 提供了对单行数据的访问能力。 同样是系统约定, Provider 会把 ID 在表的 _ID
字段中进行检索,并在匹配的的数据行上执行所需的访问请求。
这样,应用程序可以按照统一的设计模式访问 Provider 中的数据。 在向 Provider 提交查询请求之后,应用程序就可以通过 CursorAdapter
在 ListView
中显示返回的 Cursor
了。 CursorAdapter
要求 Cursor
中必须存在一个名为 _ID
的字段。
然后,用户可以在界面中选择需要查询或修改的一行数据。 应用程序从 ListView
后台的 Cursor
中读取相应的数据行,得到该行的 _ID
值,并将它添加到 Content URI 后面,再把访问请求发送给 Provider 。 Provider 然后在用户选中的记录上执行查询或修改操作。
为了便于根据传入的 Content URI 选择相应的 Action, Provider API 中提供了一个工具类 UriMatcher
,该类将 Content URI 映射为整数值。 这样,就可以在 switch
语句中使用整数值来根据 Content URI 或符合匹配规则的 URI 执行所需的 Action。
Content URI 的匹配规则使用以下通配符来进行匹配:
*
: 匹配任意长度的任意合法字符。 #
: 匹配任意长度的数字字符。 举个例子,假定有一个 authority 为 com.example.app.provider
的 Provider, 可以识别出以下 Content 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
的表。 Provider 还可以识别附带记录 ID 的 Content URI,比如 content://com.example.app.provider/table3/1
就表示 table3
中 ID 为 的记录行。
以下都是合法的 Content URI 匹配规则:
content://com.example.app.provider/*
content://com.example.app.provider/table2/*
dataset1
和 dataset2
的 Content URI, 但不会匹配 table1
和 table3
。 content://com.example.app.provider/table3/#
table3
中的某一条记录,比如 content://com.example.app.provider/table3/6
表示 ID 为 的记录行。 以下代码演示了如何使用 UriMatcher
中的方法。 这里分别演示了对全表和单条记录 URI 的处理过程, content://<authority>/<path>
匹配全表 Content URI , content://<authority>/<path>/<id>
则表示单条记录。
addURI()
方法将 authority 和 path 映射为一个整数值。 match()
方法将返回 URI 对应的整数值。 这里使用了 switch
语句来选择查询全表还是单条记录:
1 public class ExampleProvider extends ContentProvider { 2 ... 3 // 创建 UriMatcher 对象。 4 private static final UriMatcher sUriMatcher; 5 ... 6 /* 7 * 在这里调用匹配全部 Content URI 的 addURI() 。 8 * 但本段代码只匹配 table3。 9 */ 10 ... 11 /* 12 * 设置 table3 全表对应整数值 1。 13 * 注意这里的 path 没有使用通配符。 14 */ 15 sUriMatcher.addURI("com.example.app.provider", "table3", 1); 16 17 /* 18 * 设置单条记录对应数字 2。 19 * 这里使用了通配符 “#”。 20 * 所以“content://com.example.app.provider/table3/3”将符合规则,而“content://com.example.app.provider/table3”就不会匹配了。 21 */ 22 sUriMatcher.addURI("com.example.app.provider", "table3/#", 2); 23 ... 24 // 实现 ContentProvider.query() 25 public Cursor query( 26 Uri uri, 27 String[] projection, 28 String selection, 29 String[] selectionArgs, 30 String sortOrder) { 31 ... 32 /* 33 * 根据 URI 对应的整数值确定查询表和排序方向。 34 * 这里只是给出了 table3 的有关语句。 35 */ 36 switch (sUriMatcher.match(uri)) { 37 38 39 // 如果 URI 对应的是 table3 全表 40 case 1: 41 42 if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC"; 43 break; 44 45 // 如果 URI 对应了单条记录 46 case 2: 47 48 /* 49 * 既然 URI 对应单条记录, 就应该给出 _ID 值。 50 * 读取 URI 的 path 部分的最后一段,也即 _ID 值。 51 * 然后把它附加到查询语句的 WHERE 条件中。 52 */ 53 selection = selection + "_ID = " uri.getLastPathSegment(); 54 break; 55 56 default: 57 ... 58 // 如果无法识别 URI,请在这里进行错误处理。 59 } 60 // 执行查询操作 61 }
在 ContentUris
中,提供了一些操作 Content URI 的 id
部分的方法。在 Uri
和 Uri.Builder
类中,还提供了一些解析或新建 Uri
对象的方法。
ContentProvider
的实例对象负责处理其他应用程序的查询请求,由此管理着对结构化数据的访问。 各种形式的数据访问最终都要调用 ContentResolver
,再由它调用 ContentProvider
实例对象的方法来完成。
抽象类 ContentProvider
定义了6个抽象方法,在其实例子类中必须实现这些方法。 除了 onCreate()
之外,其他所有方法都会被需要访问 Content Provider 的客户端应用调用。
query()
Cursor
对象。 insert()
update()
delete()
getType()
onCreate()
ContentResolver
对象发起访问请求之前, Provider 是不会被创建的。 注意, ContentResolver
中存在与上述方法同名的对应方法。
上述方法的实现代码应该满足以下要求:
onCreate()
外,其他所有方法都有可能同时被多个线程调用,因此必须是线程安全的。 有关多线程的内容,请参阅文章进程和线程。 onCreate()
方法中执行耗时较长的任务。 请把初始化工作推迟到实际需要时执行。 在实现 onCreate() 方法一节中将会详细介绍这部分内容。 insert()
调用,直接返回 0 即可。 ContentProvider.query()
方法必须返回一个 Cursor
对象,在查询失败时将会抛出一个 Exception
。如果使用 SQLite 数据库存储数据,只要用 SQLiteDatabase
类的 query()
方法直接返回一个 Cursor
即可。 如果没有找到符合条件的记录,应该返回一个 Cursor
实例,其 getCount()
方法应该返回 0。 只有当查询过程中发生内部错误时,才能返回 null
。
如果没有用 SQLite 数据库存储数据,请使用 Cursor
的子类作为结果返回。 比如, MatrixCursor
类实现了一个游标,其中的每行数据是由 Object
对象组成的数组。 该类使用 addRow()
来添加新的数据行。
请记住, Android 系统有能力保证 Exception
的跨进程传递。 Android 可以有效传递以下这些与查询出错相关的异常:
IllegalArgumentException
(当 Provider 接收到非法的 Content URI 时,可以抛出该异常。) NullPointerException
insert()
方法将在目标表中添加一条新记录,字段名称在参数 ContentValues
中给出。如果 ContentValues
中未能给出字段名称,可能需要在 Provider 代码或是数据库定义中给出缺省值。
此方法应该返回新记录的 Content URI。利用 withAppendedId()
方法,将新记录的 _ID
(或其他主键)附加在表的 Content URI 之后即可实现。
delete()
方法不一定要物理删除记录。 如果 Provider 用于 Sync Adapter,则应考虑将删除记录标记为“delete”,而不是真的删除它。 Sync Adapter 可以感知这些删除记录,并先删除服务器端数据,再在 Provider 中进行删除。
update()
方法的 ContentValues
参数与 insert()
相同, selection
和 selectionArgs
参数与 delete()
和 ContentProvider.query()
的相同。因此这些方法就可以实现代码复用了。
Android 系统会在启动 Provider 时调用 onCreate()
方法。在该方法中,只能执行一些能够迅速完成的初始化工作,创建数据库及加载数据的工作请延至确实收到查询请求时再去执行。 如果在该方法中执行了耗时很长的任务, Provider 的启动速度将会减缓。 这会严重滞缓 Provider 对其他应用的响应速度。
比如,假定使用了 SQLite 数据库,可以在 ContentProvider.onCreate()
方法中创建一个 SQLiteOpenHelper
对象,然后在第一次打开数据库时再创建 SQL 表。 为了方便起见,第一次调用 getWritableDatabase()
时,它会自动调用 SQLiteOpenHelper.onCreate()
方法。
以下两段代码演示了 ContentProvider.onCreate()
与 SQLiteOpenHelper.onCreate()
之间的交互过程。第一段代码实现了 ContentProvider.onCreate()
:
1 public class ExampleProvider extends ContentProvider 2 3 /* 4 * 定义数据库工具类句柄。 5 * MainDatabaseHelper 类在后面的代码中定义。 6 */ 7 private MainDatabaseHelper mOpenHelper; 8 9 // 定义数据库名称 10 private static final String DBNAME = "mydb"; 11 12 // 数据库对象 13 private SQLiteDatabase db; 14 15 public boolean onCreate() { 16 17 /* 18 * 新建一个工具类对象。 19 * 本方法应该尽快返回。 20 * 请注意数据库本身将在调用 SQLiteOpenHelper.getWritableDatabase 时才会被创建并打开。 21 */ 22 mOpenHelper = new MainDatabaseHelper( 23 getContext(), // 应用程序上下文(Context) 24 DBNAME, // 数据库名称 25 null, // 使用默认 SQLite 游标 26 1 // 版本号 27 ); 28 29 return true; 30 } 31 32 ... 33 34 // 实现 Provider 的插入方法 35 public Cursor insert(Uri uri, ContentValues values) { 36 // 在这里插入代码,选择要打开的表、进行差错处理等等。 37 38 ... 39 40 /* 41 * 获取一个可写入的数据库。Gets a writeable database. This will trigger its creation if it doesn't already exist. 42 * 如果数据库不存在,则会触发创建过程。 43 */ 44 db = mOpenHelper.getWritableDatabase(); 45 } 46 }
以下代码实现了 SQLiteOpenHelper.onCreate()
,其中包含了一个工具类:
1 ... 2 // 定义建表的 SQL 语句 3 private static final String SQL_CREATE_MAIN = "CREATE TABLE " + 4 "main " + // 表名 5 "(" + // 字段名 6 " _ID INTEGER PRIMARY KEY, " + 7 " WORD TEXT" 8 " FREQUENCY INTEGER " + 9 " LOCALE TEXT )"; 10 ... 11 /** 12 * 用于创建和管理底层数据的工具类 13 */ 14 protected static final class MainDatabaseHelper extends SQLiteOpenHelper { 15 16 /* 17 * 实例化工具类,用于操作 Provider 的 SQLite 数据库 18 * 这里不要创建或升级数据库。 19 */ 20 MainDatabaseHelper(Context context) { 21 super(context, DBNAME, null, 1); 22 } 23 24 /* 25 * 创建数据库。 26 * 当 Provider 尝试打开数据库但 SQLite 却报告数据库不存在时,将会调用此方法。 27 */ 28 public void onCreate(SQLiteDatabase db) { 29 30 // 创建主表 31 db.execSQL(SQL_CREATE_MAIN); 32 } 33 }
ContentProvider
类有两个方法是用于返回 MIME 类型的:
getType()
getStreamTypes()
getType()
方法 应返回 MIME 格式的 String
,指明了 Content URI 的数据类型。 Uri
参数不一定是准确的 URI 。如果 URI 带有通配符,则应返回符合匹配条件的所有 URI 的数据类型。
对于常见的数据类型而言,比如文本、HTML、JPEG, getType()
应该返回标准的 MIME 类型。 所有的标准类型都在网站 IANA MIME 媒体类型 中列出了。
对于一条以上表记录的 Content URI 而言, getType()
应该返回 Android 的厂商定义(vendor-specific) MIME 类型:
vnd
android.cursor.item/
android.cursor.dir/
vnd.<name>
. <type>
需要给出的部分是 <name>
和 <type>
。 <name>
值应该是全局唯一的, <type>
值应该是在 URI 匹配定义中保持唯一。 较好的建议是把公司名称或应用程序包名称的一部分作为 <name>
值, 并把 URI 关联的表名用作 <type>
值。
比如,假定 Provider 的 authority 部分为 com.example.app.provider
, 表名为 table1
, 那么 table1
中多条记录的 MIME 类型可以为:
vnd.android.cursor.dir/vnd.com.example.provider.table1
table1
中单行记录的 MIME 类型可为:
vnd.android.cursor.item/vnd.com.example.provider.table1
文件的 MIME 类型
如果 Provider 提供文件访问,请实现 getStreamTypes()
。该方法将返回 Provider 可提供文件的 MIME 类型组成的 String
数组,目标文件由 Content URI 给定。 请根据 MIME 类型过滤器参数对 MIME 类型进行过滤,以便只返回客户端需要处理的那些 MIME 类型。
例如,假定某个 Provider 用于提供 .jpg
、 .png
和 .gif
格式的照片文件。 如果另一个应用程序调用 ContentResolver.getStreamTypes()
时带有过滤字符串 image/*
(图片文件),那么 ContentProvider.getStreamTypes()
方法就应该返回数组:
{ "image/jpeg", "image/png", "image/gif"}
如果应用程序只会处理 .jpg
文件,那么可以在调用 ContentResolver.getStreamTypes()
时附带过滤字符串 *//jpeg
,这时 ContentProvider.getStreamTypes()
应该返回:
{"image/jpeg"}
如果 Provider 无法提供过滤字符串要求的 MIME 类型, getStreamTypes()
应该返回 null
。
Contract 类是一种 public final
类,其中以常量的方式定义了 URI、字段名称、MIME 类型及 Provider 用到的其他元数据(meta-data)。 该类在 Provider 和其他应用程序间建立起一种约定,这样即使是 URI 、字段名称等发生了变化,也能保证 Provider 可被正确访问。
因为常量名称通常更便于记忆, Contract 类可以帮助开发人员减少差错,防止列名或 URI 被用错。 由于这是一个类,所以可以包含 Javadoc 文档。 类似 Eclipse 之类的集成开发环境可以自动完成 Contract 类中的常量名称的填写,并显示常量的 Javadoc 注释。
其他开发人员无法直接访问 Contract 类的文件,但可以把 .jar
文件将它们静态编译到自己的应用中去。
ContactsContract
及其内部类就是 Contract 类的范例。
关于 Android 系统的权限控制,将在文章 安全和权限 中详细介绍。数据存储 一文中也介绍了有关各种存储形式的安全和权限知识。 简而言之,主要包括以下几点:
SQLiteDatabase
数据库是其私有数据。 如果需要通过 Content Provider 来控制数据的访问,就应该将数据保存在内部文件或 SQLite 数据库中,或是保存到“云端” (比如远程服务器),并保证这些文件和数据库是应用程序的私有数据。
Provider 默认是没有设置权限的,因此所有的应用程序都可以读写它,即便其底层数据是私有的也没关系。 如果需要为 Provider 添加权限,只要修改 Manifest 文件中 <provider>
的属性或子元素即可。 可以为整个 Provider 、某一张表、甚至某条记录设置权限,也可以三者组合设置。
Provider 的权限是由 Manifest 文件中的一个或多个 <permission>
元素定义的。 为了保证权限名称的唯一性,请使用 Java 风格的域名来定义 android:name
属性。比如,可以把读权限命名为 com.example.app.provider.permission.READ_PROVIDER
。
以下列出了 Provider 权限的作用范围,首先是适用于整个 Provider 的权限,然后逐渐缩小。 权限的作用域越小,优先级就越高:
<provider>
元素的 android:permission
属性,可以用一个权限控制对整个 Provider 的读取和写入。 <provider>
元素的 android:readPermission
和 android:writePermission
属性即可。 该权限优先于 android:permission
的权限设置。 <provider>
的 <path-permission>
子元素来实现。 可分别对每个 Content URI 指定读写权限、读取权限、写入权限,或者三者都赋予。 读取、写入权限优先于读写权限。 并且, path 级别的权限优先于 Provider 级别的权限设置。 临时赋予某个应用程序的权限,即使该应用程序无权访问也没关系。 这种临时授权机制减少了应用程序必须在 Manifest 中申请的权限数量。 如果启用了临时授权,则只有要持续访问所有 Provider 数据的应用程序才需拥有“永久”性访问权限。
现在假定要实现一个 Email Provider 和 App, 需要用外部图片浏览应用显示保存在 Provider 中的图片附件。 如果不需要图片浏览应用申请限,就要让它拥有必要的访问能力,则可以对图片的 Content URI 建立临时权限。 当用户需要显示图片时, Email App 会向图片浏览应用发送一个包含了 Content URI 和权限标志位的 Intent。 然后图片浏览应用会查询 Email Provider 并读取图片,即使它没有 Provider 的读取权限也没关系。
要启用临时授权机制,请设置 <provider>
的 android:grantUriPermissions
属性,或者在 <provider>
元素下添加几条 <grant-uri-permission>
子元素。如果启用了该机制,则在 Provider 的某个关联了临时权限的 Content URI 失效时,必须调用 code> Context.revokeUriPermission() 。
这里的属性值决定着 Provider 可被访问的程度。 如果设为 true
,则系统会对整个 Provider 赋予临时权限,并覆盖其他所有 Provider 级别和 path 级别的权限设定。
如果设为 false
,则必须在 <provider>
元素下添加 <grant-uri-permission>
子元素。每个子元素都定义了一条临时授权。
为了实现对应用程序的临时赋权, Intent 必须包含 FLAG_GRANT_READ_URI_PERMISSION
或 FLAG_GRANT_WRITE_URI_PERMISSION
标志位,两者都带也行。这是通过 setFlags()
方法来实现的。
如果未设置 android:grantUriPermissions
属性,则被视为 false
。
类似于 Activity
和 Service
组件, ContentProvider
的子类必须在 Manifest 文件中用 <provider>
元素进行声明。 Android 系统将从此元素中读取以下信息:
android:authorities
) android:name
) ContentProvider
的类。该类已在Implementing the ContentProvider Class一节中进行了详细介绍。 android:grantUriPermssions
:临时权限标志位。 android:permission
: 读/写整个 Provider 的权限。 android:readPermission
: 读取整个 Provider 的权限。 android:writePermission
:写入整个 Provider 的权限。 有关权限及对应属性值的信息已在实现 Content Provider 权限一节中详细介绍。
android:enabled
:允许系统启动 Provider 的标志。 android:exported
:允许其他应用程序使用该 Provider 的标志。 android:initOrder
:Provider 在其进程中的启动顺序。 android:multiProcess
:允许系统在调用方进程中启动 Provider 的标志。 android:process
:Provider 所在的进程名称。 run. android:syncable
:标明 Provider 数据将于服务器进行同步的标志。 该属性在开发指南中的 <provider>
元素文档中给出了完整的介绍。
android:icon
:包含了 Provider 图标的 Drawable 资源。 在 设置 > 应用程序 > 所有应用程序 的列表中, Provider 的图标将显示在文本标签的旁边。 android:label
:描述 Provider 或其内部数据的说明性文字。 显示在 设置 > 应用程序 > 所有应用程序 的列表中。 该属性在开发指南中的 <provider>
元素文档中给出了完整的介绍。
应用程序可以通过 Intent
直接访问到 Content Provider, 它不需要调用 ContentResolver
或 ContentProvider
的任何方法,而是发送一个 Intent 启动某个 Activity 即可。 这个 Activity 往往是 Provider 所在应用的一部分,它负责完成数据读取工作,并显示在自己的界面中。 根据 Intent 中的 Action,此 Activity 也可以让用户完成修改数据的操作。 在 Intent 中还可以包含“extras”数据,目标 Activity 可在其界面中显示这些附加数据,然后用户就有机会修改这些数据并保存到 Provider 中去。
利用 Intent 有助于保证数据的完整性。 因为 Provider 可以用精确的业务规则来控制数据的插入、修改和删除操作。 如果允许其他应用程序直接修改 Provider 中的数据,则可能会带来非法数据。 为了让开发人员通过 Intent 访问数据,请务必给出完整的文档。 并且要让他们明白,通过 Intent 访问时,使用 Provider 应用的界面,比用自己的代码修改数据更为合适。
对于修改 Provider 数据的 Intent 的处理过程,与其他 Intent 没有什么不同。 关于使用 Intent 的详细信息,请阅读文章 Intent 和 Intent 过滤器 .