写在前面的话:说两点。1、很荣幸自己的两篇文章《 Android官方文档之App Components(Intents and Intent Filters)》、《Android官方文档之App Components(Common Intents)》被郭霖老师转载了,一方面说明我的博客内容得到了认可,另一方面也鞭策我继续写出质量更高的博文;2、最近在翻译官方文档,今天翻墙一看,发现页面改版了,而且居然默认显示了中文的文档(其实改版以前也有官方的中文文档,只是默认显示英文而已,另外中文官方文档只翻译了一部分,还有一些未翻译),我就在想,到底还是否继续翻译,我的答案是肯定的:之前已经翻译了10多篇文档,在这期间,我不仅提升了英文的阅读水平,还对Android的知识有了新的收获,所以我将继续翻译下去,当然了,如果遇到不会翻译的地方我就会查看官方中文文档,我争取翻译得比官方文档还好:)
Content Provider管理着应用程序需要访问的数据仓库。这需要您在程序中继承
ContentProvider类
,并在manifest中注册组件。该类就是其他应用程序与您的应用程序数据库之间的接口(interface between your provider and other applications)。通过ContentProvider
,其他应用程序可以对本应用的数据库进行方便的操作,这需要使用ContentResolver,有关这部分的内容,您可以参考我翻译的官方文档:《Android官方文档之Content Providers》;如需访问本文的官方原文,您可以点击这个链接:《Creating a Content Provider》。
创建ContentProvider前需要考虑的事(Before You Start Building)
- 如您的程序需要向外界应用提供以下内容之一,那么程序中需要创建
ContentProvider
:- 需要向其他应用程序提供复杂的数据或文件( offer complex data or files to other applications);
- 希望用户能从您的应用向其他应用复制复杂的数据(allow users to copy complex data from your app into other apps);
- 希望使用搜索框架提供提供定制的搜索建议( provide custom search suggestions using the search framework)。
!请注意:若只是应用需要访问自身SQLite数据库,那么无需创建ContentProvider。
以下是创建provider的方法:
- 为您的应用程序设计原始存储。ContentProvider以如下两种方式提供数据:
- 文件数据(File data):在应用程序的的私有空间中,数据以不同形式的文件存储,如照片(photos)、音频(audio) 、视频(videos)等。为了能让其他应用程序读取您的程序中的这些文件,需要provider作为媒介。
- 结构化数据(”Structured” data):数据存储于数据库、数据或其他类似的结构中。存储的数据支持行列表的形式(Store the data in a form that’s compatible with tables of rows and columns)。每行代表一个实体(entity),如个人信息等。每列表示每个实体的不同属性,如人的身高、体重等。这些表结构及其数据信息可以存储于SQLite数据库中。
- 实现
ContentProvider
中的方法; - 为
ContentProvider
定义authority字符串,该字符串包含了content URI以及表中列的字段。若您希望ContentProvider可以处理intent,那么还需定义intent的action、data、flag等。并为provider定义其他应用程序需要访问它的权限。
设计数据存储(Designing Data Storage)
Android中提供了如下存储技术:
- Android提供了SQLite数据库API以访问面向表的数据存储(to store table-oriented data),
SQLiteOpenHelper
可帮助您创建数据库,而SQLiteDatabase
类是访问数据库的基础类。provider提供给外界程序的内容表现为一张或多张表。 - Android提供了多个面向文件存储的API,关于文件存储,您可以参考这个链接 Data Storage,若您希望创建一个访问媒体文件(如音乐或多媒体)的provider,那么该provider还能提供访问表和其他文件的方法。
- 使用
java.net
或android.net
包中的API可以访问网络数据,您可以将网络数据同步到本地(如同步到本地的数据库中),并将本地数据以表或文件的形式提供给程序。
数据设计的注意事项(Data design considerations)
- 必须为数据表提供主键(primary key)以保证数据的唯一性,您也可以将此值链接至其他表的相关行中(外键,foreign key),尽管可以为该列的字段取任何名字,但推荐使用
BaseColumns._ID
作为其字段。因为当需要将表中数据绑定到ListView上时,ListView需要一个列名为_ID
的字段。
- 若您需要提供bitmap图像,或者大型的面向文件的数据,最好将这些数据存储在一个文件中并直接提供而不是将它们存储在表中。
- 使用二进制大型对象数据类型(Binary Large OBject (BLOB) )存储大小或结构会发生变化的数据,您可以使用 BLOB 列来存储缓冲协议( protocol buffer)或JSON结构的数据(JSON structure)。
设计Content URIs(Designing Content URIs)
content URI是一个URI对象,它指定了provider中的某个数据(集、表),Content URIs 包含两部分:authority和path,authority用于标识provider的唯一性,path用于指定该provider中的某张表或是某个文件。content URI还有一个id部分是可选的,它指定了表中的特定行。
ContentProvider
类中每一个访问数据的方法(增删改查)都会回传一个URI参数,用于具体指定您需要访问哪张表(哪一行)或是哪个文件。
设置authority(Designing an authority)
一个provider通常只有一个authority,并且遵循android系统内部命名规范。为了防止authority之间的命名冲突,您应当使用公司的Internet域名的倒序来作为authority的前缀,或者使用应用的包名作为前缀。比如,您的应用程序包名为
com.example.<appname>
,那么应当为provider的authority设置为com.example.<appname>.provider
。
设置path(Designing a path structure)
path用于指定provider中的某张表。比如,继续按上例来说,若provider中包含两张表:
table1
、table2
,其content URI应分别为com.example.<appname>.provider/table1
、com.example.<appname>.provider/table2
。您无需为每级path都设置一张表。
处理content URI的ID(Handling content URI IDs)
通常,在content URI后追加ID可以访问该URI所指向的provider中的某张表的某一行数据信息。该ID将匹配表中名为“
_ID
”的字段中的值,并访问该值所在的行( providers match the ID value to the table’s _ID column, and perform the requested access against the row that matches)。
其他应用程序访问provider的常见情景是:app通过
ContentResolver
访问content URI指定的ContentProvider
中的某张表(或表中的某几行),并返回一个Cursor
对象,再利用CursorAdapter
将Cursor
对象作为数据源绑定至ListView
上。而CursorAdapter
要求绑定的Cursor
必须包含_ID
字段。
用户可以在UI上查询或修改ListView中的项,系统将查询该项对应的
Cursor
中的行,并将该行的_ID
追加至content URI后访问provider中某张表的某一行,以达到查询或修改该行数据的目的。
Content URI的格式 (Content URI patterns)
为了区分不同content URI 的访问请求,系统提供了协助provider的
UriMatcher
类,并将content URI和特定的整型值一一对应(maps content URI “patterns” to integer values)。您可以使用switch语句处理不同整型值对应的content URI所需查询的内容。content URI使用通配符查询传递来的查询URI:
- “*”表示任意长度的任意字符;
- “#”表示任意长度的数字。
比如说,您可能会使用如下content URI来查询表中的某张表中的某些数据:
content://com.example.app.provider/table1
: 查询table1;content://com.example.app.provider/table2/dataset1
:查询table2中的dataset1;content://com.example.app.provider/table2/dataset2
:查询table2中的dataset2;content://com.example.app.provider/table3: A table called table3
:查询table3;content://com.example.app.provider/table3/1
:查询table3中行ID为1的条目;
在provider中,下面的写法可以匹配第一个URI:
content://com.example.app.provider/*
在provider中,下面的写法可以匹配第二个和第三个URI:
content://com.example.app.provider/table2/*
在provider中,下面的写法可以匹配第五个URI:
content://com.example.app.provider/table3/#
下面演示了
UriMatcher
的用法,addURI()
方法将传入的URI与一个唯一的整型标识对应,match()
方法将返回URI对应的整型值,示例如下:
public class ExampleProvider extends ContentProvider {
...
// Creates a UriMatcher object.
private static final UriMatcher sUriMatcher;
...
/*
* The calls to addURI() go here, for all of the content URI patterns that the provider
* should recognize. For this snippet, only the calls for table 3 are shown.
*/
...
/*
* Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
* in the path
*/
sUriMatcher.addURI("com.example.app.provider", "table3", 1);
/*
* Sets the code for a single row to 2. In this case, the "#" wildcard is
* used. "content://com.example.app.provider/table3/3" matches, but
* "content://com.example.app.provider/table3 doesn‘t.
*/
sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
...
// Implements ContentProvider.query()
public Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {
...
/*
* Choose the table to query and a sort order based on the code returned for the incoming
* URI. Here, too, only the statements for table 3 are shown.
*/
switch (sUriMatcher.match(uri)) {
// If the incoming URI was for all of table3
case 1:
if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
break;
// If the incoming URI was for a single row
case 2:
/*
* Because this URI was for a single row, the _ID value part is
* present. Get the last path segment from the URI; this is the _ID value.
* Then, append the value to the WHERE clause for the query
*/
selection = selection + "_ID = " uri.getLastPathSegment();
break;
default:
...
// If the URI is not recognized, you should do some error handling here.
}
// call the code to actually do the query
}
实现ContentProvider类(Implementing the ContentProvider Class)
ContentProvider
用于处理其他应用程序的访问请求,而最终将检索结果传给ContentResolver
,ContentProvider
的主要方法如下:
需实现的方法(Required methods)
ContentProvider
是抽象类,该类中包含6个抽象方法需要您实现,除了onCreate()
方法外,其他应用程序欲访问您定制的ContentProvider
时,其余的5个方法均被调用(All of these methods except onCreate() are called by a client application that is attempting to access your content provider)。
query()
:从provider中查询数据,并将结果以Cursor
对象返回;insert()
:向provider
的某张表中插入一行新的数据,方法回传的Uri参数用于指向需要插入的表,而ContentValue参数用于回传插入的内容,该方法返回新插入行的Uri地址;update()
:用于修改某些行,并返回修改的行数;delete()
:删除某些行,并返回删除的行数。getType()
:返回content URI对应的MIME 类型(Return the MIME type corresponding to a content URI),该方法的具体含义请您参考后续章节。onCreate()
:该方法用于初始化ContentProvider
,当ContentProvider
被实例化后,该方法将被立刻回调。!请注意:只有ContentResolver
试图访问您的ContentProvider
,该实例才会被创建(Notice that your provider is not created until a ContentResolver object tries to access it)。
在ContentResolver中,包含着与上述同名的方法及方法签名(these methods have the same signature as the identically-named ContentResolver methods)。
为了实现上述方法,您需要考虑这些事:
- 除了
onCreate()
方法外,其余方法均可在多个线程中同时调用,这就需要保证线程安全(can be called by multiple threads at once, so they must be thread-safe)。 - 避免在
onCreate()
方法中做耗时操作(Avoid doing lengthy operations in onCreate())。当方法中涉及的资源用到时再加载(Defer initialization tasks until they are actually needed)。 - 尽管您必须实现这些方法,但是您可以不做任何操作。比如说,当您需要向表中插入数据时,若只是在
insert()
中返回0,则数据插入操作不会成功。
实现query()方法(Implementing the query() method)
ContentProvider.query()
方法返回一个Cursor
对象,若返回失败,将方法将抛出一个异常。
若您是使用SQLite数据库存储数据,那么调用SQLiteDatabase
的query()
方法返回的Cursor
对象可以直接作为该方法的返回值(you can simply return the Cursor returned by one of the query() methods of the SQLiteDatabase class)。若未查询到匹配结果,那么返回的Cursor
对象中,其getCount()
方法应返回0。若出现异常,Cursor
对象应返回null。
若您并没有使用SQLite数据库存储数据,那么应根据具体数据的结构,使用系统提供的Cursor
子类对象作为返回值。比如,MatrixCursor
是Cursor
的一个子类,它可以指向每一行都是一个Object对象数组的矩阵结构(each row is an array of Object)。调用该类的addRow()
方法可添加一行数据。
!请注意:Android 系统必须能够跨进程边界传播 Exception,Android 可以为以下异常执行此操作,这些异常可能有助于处理查询错误:1、
IllegalArgumentException
:您可以选择在提供程序收到无效的内容 URI 时引发此异常;2、NullPointerException:空指针异常
实现insert()方法(Implementing the insert() method)
insert()
方法可为指定的表中添加一行数据,使用回传参数ContentValues
配置值。若ContentValues
的键并没有表中的任何字段与之对应,您应当设定一个默认值。
实现delete()方法(Implementing the delete() method)
删除指定表中的某一行或多行数据。方法返回int值,表示删除的行数。
实现update()方法(Implementing the update() method)
方法利用
ContentValues
以键值对的方式修改指定表中的数据。返回int型变量,表示修改的行数。
实现onCreate()方法(Implementing the onCreate() method)
当初始化provider时,系统将回调
onCreate()
方法,在该方法中不要执行耗时操作,您可以在resolver访问该provider时,再创建数据库,并延迟数据加载。
比方说,您可以在ContentProvider.onCreate()
方法中创建一个SQLiteOpenHelper
对象,第一次打开数据库时创建SQL表,实现方式是调用getWritableDatabase()
方法,调用该方法会立即触发SQLiteOpenHelper.onCreate()
方法的回调,可以在该方法中创建数据库和表。
下面将以代码片段的方式展示上述实现过程:
public class ExampleProvider extends ContentProvider
/*
* Defines a handle to the database helper object. The MainDatabaseHelper class is defined
* in a following snippet.
*/
private MainDatabaseHelper mOpenHelper;
// Defines the database name
private static final String DBNAME = "mydb";
// Holds the database object
private SQLiteDatabase db;
public boolean onCreate() {
/*
* Creates a new helper object. This method always returns quickly.
* Notice that the database itself isn‘t created or opened
* until SQLiteOpenHelper.getWritableDatabase is called
*/
mOpenHelper = new MainDatabaseHelper(
getContext(), // the application context
DBNAME, // the name of the database)
null, // uses the default SQLite cursor
1 // the version number
);
return true;
}
...
// Implements the provider‘s insert method
public Cursor insert(Uri uri, ContentValues values) {
// Insert code here to determine which table to open, handle error-checking, and so forth
...
/*
* Gets a writeable database. This will trigger its creation if it doesn‘t already exist.
*
*/
db = mOpenHelper.getWritableDatabase();
}
}
以下是
SQLiteOpenHelper.onCreate()
中的代码:
...
// A string that defines the SQL statement for creating a table
private static final String SQL_CREATE_MAIN = "CREATE TABLE " +
"main " + // Table‘s name
"(" + // The columns in the table
" _ID INTEGER PRIMARY KEY, " +
" WORD TEXT"
" FREQUENCY INTEGER " +
" LOCALE TEXT )";
...
/**
* Helper class that actually creates and manages the provider‘s underlying data repository.
*/
protected static final class MainDatabaseHelper extends SQLiteOpenHelper {
/*
* Instantiates an open helper for the provider‘s SQLite data repository
* Do not do database creation and upgrade here.
*/
MainDatabaseHelper(Context context) {
super(context, DBNAME, null, 1);
}
/*
* Creates the data repository. This is called when the provider attempts to open the
* repository and SQLite reports that it doesn‘t exist.
*/
public void onCreate(SQLiteDatabase db) {
// Creates the main table
db.execSQL(SQL_CREATE_MAIN);
}
}
实现与URI对应的MIME类型(Implementing ContentProvider
MIME Types)
ContentProvider
中包含了两种返回MIME类型的方法:
getType()
:该方法必须实现(One of the required methods that you must implement for any provider);getStreamTypes()
:若您的provider提供了文件访问,需实现该方法(A method that you’re expected to implement if your provider offers files.)。
表的MIME类型(MIME types for tables)
getType()
方法返回值为String
类型,表示传入的Uri对象所对应的MIME类型。回传的Uri参数既可以是带有通配符的Uri,也可以是一个具体的Uri。
常见的MIME类型有 text,、HTML、JPEG 等。如需查看MIME的全部类型及解析,您可以点击这个链接:《IANA MIME Media Types》。
对于
getType()
方法返回的MIME类型,应遵循以下格式:
- Type 部分:
vnd
- Subtype 部分:
- 若回传的Uri指向表的某一行,那么格式应为:
android.cursor.item/
- 若回传的Uri指向表的多行(一行以上),那么格式应为:
android.cursor.dir/
- 若回传的Uri指向表的某一行,那么格式应为:
- Provider-specific 部分:
vnd.<name>.<type>
。其中<name>
应是唯一的,<type>
能体现指向Uri的唯一性。好的做法是将<name>
命名为公司名的倒序或是应用程序的包名,将<type>
命名为指定的表名。
如您的provider的authority是
com.example.app.provider
,若需要访问table1中的多行,那么MIME类型可以这样写:
vnd.android.cursor.dir/vnd.com.example.provider.table1
如需访问table1中的单行,MIME类型可以这样写:
vnd.android.cursor.item/vnd.com.example.provider.table1
文件的MIME类型(MIME types for files)
若provider可以访问文件,应该实现
String[] getStreamTypes (Uri uri,
String mimeTypeFilter)
方法。该方法返回一个String
数组,数组中的每一项表示传入的Uri所指向的文件类型(for the files your provider can return for a given content URI),若其他应用程序只对某个特定类型的文件感兴趣,那么应给getStreamTypes()
方法的第二个参数传入感兴趣的文件的MIME类型。
比如说,provider提供了 .jpg、.png 和 .gif
类型的图像文件,其他应用程序调用ContentResolver.getStreamTypes()
方法并传入过滤参数image/*
,那么系统将返回如下所示的String
数组:
{ "image/jpeg", "image/png", "image/gif"}
若应用程序只对
.jpg
格式的文件感兴趣,那么可以传入*\/jpeg
作为过滤参数,则方法将返回
{"image/jpeg"}
若provider提供的文件类型无法匹配您传入的过滤文件类型,那么方法应返回
null
(If your provider doesn’t offer any of the MIME types requested in the filter string, getStreamTypes() should return null)。
实现Contract类(Implementing a Contract Class)
Contract类是一个用
public final
关键字修饰的类,存在于provider所在的应用程序中,该类的内部定义了provider中需要使用到的URIs、column names、 MIME types、 meta-data
等字符串常量。设置常量的好处是:当需要修改查询的范围或内容时,只需修改这个类中的字符串即可。
设置Contract类的另一个好处是协助开发者对字符串常量的记忆( mnemonic names for its constants)。
为ContentProvider设置权限(Implementing Content Provider Permissions)
- 默认情况下,存储在设备内部存储的文件对您的应用货provider是私有的;
- 程序中创建的
SQLiteDatabase
数据库只有您自己的程序和provider可以操作; - 默认情况下,外部存储中的文件是公开的(data files that you save to external storage are public and world-readable),您无需使用provider访问外存中的文件。因为这些文件使用系统提供的API就能访问。
实现权限(Implementing permissions)
所有应用程序都可以访问您的provider。默认情况下,您的provider没有任何权限。可以在manifest文件的
<provider>
标签中修改默认属性。您可以设置:其他应用是否可以访问provider提供的所有内容,或仅能访问特定的表,甚至只能访问某个指定的行。
这需要在manifest中设置<permission>
标签,并由android:name
属性指定,指定的权限应具有唯一性。如为provider添加只读的权限:
com.example.app.provider.permission.READ_PROVIDER
下面描述了provider权限的作用域,作用域越小的权限,优先级越高(More fine-grained permissions take precedence over ones with larger scope):
- 统一的读写provider级别权限(Single read-write provider-level permission):在
<provider>
标签中由android:permission
属性指定。 - 单独的读写provider级别权限(Separate read and write provider-level permission):在
<provider>
标签中由android:readPermission 和android:writePermission
属性指定。该权限高于上面的统一读写权限(They take precedence over the permission required byandroid:permission
)。 - path级别的权限(Path-level permission):在provider的content URI中设置只读、只写、或可读可写的权限。在
<provider>
的子标签<path-permission>
中设定您需要控制的每一个URI。您可以为每一URI指定只读权限、或只写权限、或可读可写权限,其中只读或只写得权限高于可读可写权限。而path级别权限高于provider级别权限。
<provider>
标签(The <provider>
Element)
您的程序中定义的ContentProvider需要在manifest中使用
<provider>
标签注册。该标签中可配置的内容如下:
- Authority:使用
android:authorities
属性配置。该属性为provider提供一个唯一的标识。 - Provider class name:使用
android:name
属性定义。属性值为定制的ContentProvider
的全限定类名。 - Permissions:指定了其他应用需要访问该provider所需声明的权限。
android:grantUriPermssions
:临时权限;android:permission
:provider范围内的读写权限;android:readPermission
:provider范围的只读权限;android:writePermission
:provider范围的只写权限。
- Startup and control attributes:下列属性决定了系统何时以及如何启动provider、provider的特性、以及运行时设置:
android:enabled
:是否允许系统启动provider;android:exported
:是否允许其他应用程序访问provider;android:initOrder
:指定同一进程中,相对于其他provider的启动顺序;android:multiProcess
:是否允许在与client端相同的进程中启动provider;android:process
:provider运行的进程名;android:syncable
:provider中的数据与服务器上的数据同步。
- Informational attributes:为provider提供可选的图标和标题。
android:icon
:指定一个drawable资源,该图标出现在设置 > 应用 > 全部 中应用列表内的provider标签旁;android:label
:描述provider(和)或其数据的信息标签。 该标签出现在设置 > 应用 > 全部中的应用列表内。