ContentProvider是Android里的四大组件之一。
顾名思义,其作为内容提供者,为不同应用之间提供了一个数据访问通道。
对于其的使用,无非也就是两方面:
- 在我们自己的应用里,通过ContentProvider访问外部应用的数据。
- 我们在自己的应用里编写ContentProvider,提供给其它想要访问我们应用某些数据的应用使用。
那么,首先我们来看一下,如何最基本的通过ContentProvider访问其它应用里的数据。
从代码编写上来说,其实十分简单。例如,最常见的例子,我们希望通过ContentProvider在自己的应用里访问系统通讯录里的联系人数据。
那么,代码可能是这样的:
private void readContactsByContentProvider() {
Cursor cursor = null;
try {
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null,
null);
while (cursor.moveToNext()) {
String contactName = cursor
.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String phoneNunber = cursor
.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contacts.add(contactName + "\n" + phoneNunber);
}
} catch (Exception e) {
// TODO: handle exception
} finally {
if (cursor != null) {
cursor.close();
}
}
}
把上面的代码分解一下,我们发现通过ContentProvider来访问外部数据的过程其实很简单:
- 首先,我们会使用到一个Uri。
- 接着,我们需要获取到一个ContentResolver,直译也就可以理解为内容解释器。
- 结合上面二者,就可以通过Resolver调用增删改查方法,来操作数据了。
我们注意到两个关键的东西,Uri和ContentResolver。实际上,通过ContentProvider访问数据的原理,也就是基于他们来实现的。
Uri我们都知道意思是,统一资源标识符。
在我们这里谈到的话题里,该Uri实际上就是代表的内容提供者为我们提供的用以访问某对应资源的资源路径。
再看到关键字,“统一”。既然统一,则代表其具有命名规范,而Android里ContentProvider使用到的Uri命名规范,通常为:
- content://权限名/路径名
这样的命名规范实际上十分清晰:
- content:// 我们可以看做是一种协议声明,代表通过ContentProvider访问数据。就好像我们在通过Http协议访问资源时,Uri的格式是:http://*一样。
- 权限名:其格式通常为“应用包名/provider”。同样的,通过其命名规范,我们就可以想得到,它是为了区分访问不同应用间的内容而设定的。因为基本来说,每个应用的包名永远是保持唯一的。这个就如同网页的域名一样,我们通过http://www.baidu.com是为了标明,我们想要访问的目标资源是百度的网站上的。
- 路径名:路径名是为了区分不同数据库表而存在的。这个依然很好理解,因为一个应用里往往存在多张不同的表,所以权限名用于区分应用,而路径名则可以保证我们区分同一个应用的不同的表。正如同通过”http://www.baidu.com/A“和”http://www.baidu.com/B“区分我们访问的是同样存在于百度网站上面的,不同的资源。
到这里,我们已经清楚的知道了ContentPorvider里的Uri的使用原理和规则。
既然有了资源标识符,那么,我们自然需要一个解释器来解析该标识符,并做出正确操作。
以我们前面使用到的例子中的代码而言:
getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null,
null);
简单的还原一下,我们可以得到等价的如下代码:
ContentResolver().query(URI, null, null, null,
null);
这样,我们更能得到重点,我们来解析一下:
1.首先我们已经得到了访问目标资源的URI。
2.然后我们通过getContentResolver得到了解释器。
3.通过解释器,解析URI,我们将得到对应的目标数据路径。
4.现在我们已经有了一条操作数据的通道,那么接下来的就是操作数据,如上面的例子中,我们是通过query方法进行操作,也就是执行了查询操作。
到这里,对于通过ContentProvider访问外部应用数据的操作,我们就告一段落了。
现在,来到了更有意思的话题,假如我们想提供数据给外部应用访问,我们应当怎么去编写Provider呢?
我们先最基本的解析其过程:
- 首先创建一个自己的provider类,继承自ContentProvider。
- 接着,重写其中的6个方法,分别是:
//主要在这里面创建或更新数据库
1.onCreate()
//没什么好说的,在这里面实际上就是在操作我们自己应用的数据库,根据需求逻辑实现即
2.insert();update();delete();query()。
// 方法故名思议,返回一个类型。这个类型是什么呢,实际上就是代表的Uri对象所对应的MIME类型。Android里规定,其格式应当为:
Uri若以路径结尾:vnd. + android.cursor.dir + / vnd. + 包名(权限名) + . + 路径
Uri若以id结尾:vnd. + android.cursor.item + / vnd. + 包名(权限名) + . + 路径
3.getType()- 最后,当我们完成我们自己的provider类的编写工作后,将其注册到项目配置文件里面。就搞定了,例如:
<provider
android:name="com.tsr.contentprovideruse.providers.MyContentProvider"
android:authorities="com.tsr.contentprovideruse.provider"
android:exported="true" />
了解了实现过程之后,我们来进行一下实战。假设现在有如下需求:
在我们的应用里,有表”Student“存放有学生信息。
现在有另一个应用希望访问该表里的学生信息情况。
于是,我们完成了ContentProvider的编写,得到如下代码:
public class MyContentProvider extends ContentProvider {
private MyDBOpenHelper mDBOpenHelper;
private static final String URI_AUTHORITY = "com.tsr.contentprovideruse.provider";
private static UriMatcher mURIMatcher;
private static final int STUDENT_DIR = 0;
private static final int STUDENT_ITEM = 1;
static {
mURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mURIMatcher.addURI(URI_AUTHORITY, "student", BOOK_DIR);
mURIMatcher.addURI(URI_AUTHORITY, "student/#", BOOK_ITEM);
}
@Override
public boolean onCreate() {
mDBOpenHelper = new MyDBOpenHelper(getContext(), "testDB.db", null, 1);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = mDBOpenHelper.getReadableDatabase();
Cursor cursor = null;
switch (mURIMatcher.match(uri)) {
case STUDENT_DIR: {
cursor = db.query("student", projection, selection, selectionArgs, null, null, sortOrder);
}
break;
case STUDENT_ITEM: {
String id = uri.getPathSegments().get(1);
cursor = db.query("student", projection, "id = ?", new String[] { id }, null, null, sortOrder);
}
break;
default:
break;
}
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mDBOpenHelper.getReadableDatabase();
Uri returnUri = null;
switch (mURIMatcher.match(uri)) {
case STUDENT_DIR:
case STUDENT_ITEM:
long newStudentID = db.insert("student", null, values);
returnUri = Uri.parse("content://" + URI_AUTHORITY + "student/" + newStudentID);
break;
default:
break;
}
return returnUri;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = mDBOpenHelper.getReadableDatabase();
int deleteRows = 0;
switch (mURIMatcher.match(uri)) {
case STUDENT_DIR: {
deleteRows = db.delete("student", selection, selectionArgs);
}
break;
case STUDENT_ITEM: {
String studentID = uri.getPathSegments().get(1);
deleteRows = db.delete("student", "id = ?", new String[] { studentID });
}
break;
default:
break;
}
return deleteRows;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = mDBOpenHelper.getReadableDatabase();
int updateRows = 0;
switch (mURIMatcher.match(uri)) {
case STUDENT_DIR: {
updateRows = db.update("student", values, selection, selectionArgs);
}
break;
case STUDENT_ITEM: {
String studentID = uri.getPathSegments().get(1);
updateRows = db.update("student", values, "id = ?", new String[] { studentID });
}
break;
default:
break;
}
return updateRows;
}
@Override
public String getType(Uri uri) {
switch (mURIMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd." + URI_AUTHORITY + ".student";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd." + URI_AUTHORITY + ".student";
default:
break;
}
return null;
}
}
经过之前的解释,实际上provider里的各个方法的重写,我们都已经清楚了其原理。
所以,这里我们主要来看一下UriMatcher的使用:
前面,我们已经说到,关于ContentProvider的实际原理,实际上就是通过解释器解析Uri,得到资源路径,然后使用对应的方法进行数据操作。
那么,究竟是怎么样对Uri进行解析的,UriMatcher就是一个关键角色。
在我们上面完成的代码当中,我们注意到:
- 1.首先,我们在静态代码块中初始化了一个UriMatcher对象。
- 2.然后,通过调用其实例方法addURI将其添加到其中。
- 3.我们注意到,该方法接受了三个参数,分别是Uri当中的权限名、路径名,以及一个唯一标识符。
- 4.所谓的唯一标识符,可以看做是由传入的”权限名+路径名”组成的Uri对象的身份证,他们保持唯一对应。
- 5.最后,在增删改查的方法中,就可以通过该”身份证”,进行解析了。我们注意到通过UriMatcher的match()方法,实际上就是在验证”身份证”信息,match方法会去验证传入的uri参数,如果验证成功,就会返回该唯一身份标识。
- 6.于是,通过该标识,我们就可以解析得到,该次访问操作想要访问的资源目的地是哪里。从而进行对应的数据库操作。
可能这样的解析过程,还是显得较为抽象。那么,我们再次回头看到哪行熟悉的代码:
getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null,
null);
现在,我们已经完全能够明白,这行代码到底是在做怎么样的工作了,分析一下,:
- 得到解释器对象,通过Uri中的权限名,知道了访问的将是哪个应用的内容,于是找到了对应的ContentProvider。
- 执行的是query方法,于是我们定义的MyContentProvider类的query()方法得到执行。
- 于是通过switch语句,判断通过mURIMatcher.match(uri)解析得到的唯一标识,我们知道了访问的路径。
- 访问的路径,也知道了,剩下的就是根据其余的参数与条件,向对应的数据库表做数据操作而已了。