Android四大组件之ContentProvider

一、ContentProvider

ContentProvider为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据。如果你不需要在多个应用之间共享数据,你可以直接通过SQLite来操作数据库。

使用ContentProvider,主要有以下几个理由:

  • ContentProvider提供了对底层数据存储方式的抽象。比如下图中,底层使用了SQLite数据库,在用了ContentProvider封装后,即使你把数据库换成MongoDB,也不会对上层数据使用层代码产生影响。


  • Android框架中的一些类需要ContentProvider类型数据。如果你想让你的数据可以使用在如SyncAdapter, Loader, CursorAdapter等类上,那么你就需要为你的数据做一层ContentProvider封装
  • 最主要的原因,是ContentProvider为应用间的数据交互提供了一个安全的环境。它准许你把自己的应用数据根据需求开放给其他应用进行增、删、改、查,而不用担心直接开放数据库权限而带来的安全问题。
onCreate() boolean //当第一次访问ContentProvider,创建完对象之后调用,唯一的生命周期方法
insert(Uri uri, ContentValues values) Uri  //根据Uri插入values对应的数据
delete(Uri uri, String selection, String[] selectionArgs) int    //根据Uri删除select条件所匹配的全部记录
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)    int //根据Uri修改select条件所匹配的全部记录
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) Cursor    //查询,projection:选择的指定的列
getType(Uri uri) String    //返回当前Uri所代表的MIME类型。

二、ContentResolver

Android为我们提供了ContentResolver来统一管理与不同ContentProvider间的操作,通过URI来区别不同的ContentProvider
ContentResolver 类也提供了与ContentProvider类相对应的四个方法:

insert(Uri uri, ContentValues values) Uri    
delete(Uri uri, String selection, String[] selectionArgs) int   
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)  int   
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) Cursor 
ContentResolver角色

ContentProvider中的URI有固定格式,如下图:

Scheme:URI的命名空间标识
Authority:授权信息,用以区别不同的ContentProvider
Path:表名,用以区分ContentProvider中不同的数据表
Id:Id号,用以区别表中的不同数据

三、URI

1、URI

URI:通用资源标志符 —— Uniform Resource Identifier
URI类:java.net.URI,是Java提供的一个类,代表了URI的一个实例
Uri类:android.net.Uri,扩展了JAVA中URI的一些功能来适用于Android开发

2、URI的结构

2.1、基本结构
[scheme:]scheme-specific-part[#fragment]  
[scheme:][//authority][path][?query][#fragment]  
[scheme:][//host:port][path][?query][#fragment]
http://www.java2s.com:8080/yourpath/fileName.htm?stove=10&path=32&id=4#harvic

在android中,除了scheme、authority是必须要有的,其他都可以不要,Android中可用的每种资源( 图像、视频片段等)都可以用URI来表示

2.2、代码中提取
Uri.parse(String uriString)    //返回一个Uri对象
getScheme()     //获取Uri中的scheme字符串部分,在这里即,http
getSchemeSpecificPart()   //获取Uri中的scheme-specific-part:部分,这里是://www.java2s.com:8080/yourpath/fileName.htm?
getFragment()   //获取Uri中的Fragment部分,即harvic
getAuthority()   //获取Uri中Authority部分,即www.java2s.com:8080
getPath()   //获取Uri中path部分,即/yourpath/fileName.htm
getQuery()   //获取Uri中的query部分,即stove=10&path=32&id=4
getHost()   //获取Authority中的Host字符串,即www.java2s.com
getPost()   //获取Authority中的Port字符串,即8080
getPathSegments() List<String>    //依次提取出Path的各个部分的字符串,以字符串数组的形式输出
getQueryParameter(String key)   //根据传进去path中某个Key的字符串,返回对应的值
2.3、绝对URI和相对URI

绝对URI:以scheme组件起始的完整格式,如http://fsjohnhuang.cnblogs.com。表示以对标识出现的环境无依赖的方式引用资源
相对URI:不以scheme组件起始的非完整格式,如fsjohnhuang.cnblogs.com。表示以对标识出现的环境有依赖的方式引用资源

2.4、不透明URI和分层URI

不透明URI:scheme-specific-part组件不是以正斜杠(/)起始的,如mailto:fsjohnhuang@xxx.com
分层URI:scheme-specific-part组件是以正斜杠(/)起始的,如http://fsjohnhuang.com

2.5、UriMatcher工具类

因为Uri代表了要操作的数据,所以我们很经常需要解析Uri,并从Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher和ContentUris。掌握它们的使用,会便于我们的开发工作。

UriMatcher

UriMatcher本质上是一个文本过滤器,用在contentProvider中帮助我们过滤,分辨出查询者想要查询哪个数据表

UriMatcher(int code)   //code:匹配未成功时返回的标志码,一般置为:UriMatcher.NO_MATCH(值为-1)
//向UriMatcher中注册Uri,其中authority和path组合成一个Uri,code标识该Uri对应的标识码,path中*表示可匹配任意文本,#表示只能匹配数字
addURI(String authority, String path, int code) void    
match(Uri uri) int   //该Uri是否匹配,若匹配返回注册时候的标志码,否则返回-1

matcher.addURI("com.xx","person/id/#", 2) 匹配content://com.xx/person/id/1any://com.xx/person/id/12

ContentUris工具类

其实就是在末尾加上一个id

ContentUris.withAppendedId(Uri contentUri, long id) //为路径加上ID部分
ContentUris.parseId(Uri contentUri) //解析Uri中的ID值

3、URL

URL = URI(scheme组件为部分已知的网络协议) + 与scheme组件标识的网络协议匹配的协议处理器(URL Protocol Handler),是URI子集

  • URI的scheme组件在URL中称为protocol组件,一般http、https、ftp、file、data、jar等。
  • URL Protocol Handler则是一种资源定位器和根据协议建立的约束规则与资源通信的读写机制,用于定位、读写资源。

四、ContentObserver

1、基本认知

内容观察者,观察指定的Uri引起数据库变化后通知主线程,然后根据需求做处理。首先在需要监测ContentProvider的应用中进行注册(ContentResolver调用方法的地方),在ContentProvider中要做的就是当数据变化时进行通知。

ContentResolver相关方法:

//传递一个ContentObserver的子类对象进去,会回调其onChange(boolean selfChange)
registerContentObserver(Uri uri, boolean notifyForDescendents, ContentObserver observer)
//取消对注册的那个Uri的观察,这里传进去的就是在registerContentObserver中传递进去的ContentObserver对象。
unregisterContentObserver(ContentObserver observer) 
notifyChange(Uri uri, ContentObserver observer)//在Provider中调用,通知观察uri的观察者,observer可以传null

registerContentObserver方法是注册一个观察者实例,当指定的Uri发生改变时,这个实例会回调实例对象做相应处理。uri:需要观察的Uri,notifyForDescendents:如果为true表示以这个Uri为开头的所有Uri都会被匹配到,如果为false表示精确匹配,即只会匹配这个给定的Uri。
举个例子,假如有这么几个Uri:
content://com.example.studentProvider/student
content://com.example.studentProvider/student/#
content://com.example.studentProvider/student/10
content://com.example.studentProvider/student/teacher
假如观察的Uri为content://com.example.studentProvider/student,当notifyForDescendents为true时则以这个Uri开头的Uri的数据变化时都会被捕捉到,在这里也就是①②③④的Uri的数据的变化都能被捕捉到,当notifyForDescendents为false时则只有①中Uri变化时才能被捕捉到。

2、实现一个ContentObserver

直接创建一个类继承ContentObserver,实现其onChange(boolean selfChange)方法,当指定的Uri的数据发生变化时会回调这个方法。在此方法中,调用构造函数ContentObserver(Handlerhandler)中传入的 Handler对象发送消息到Handler中,做相应的处理。

五、数据共享

如何让其他应用也可以访问此应用中的数据?

1、android:sharedUserId

向此应用设置一个android:sharedUserId,然后需要访问此数据的应用也设置同一个sharedUserId,具有同样的sharedUserId的应用间可以共享数据。

不足:
1)不够安全
2)无法做到对不同数据设置不同读写权限的管理

2、android:exported

  • android:exported 设置此provider是否可以被其他应用使用。
  • android:readPermission 该provider的读权限的标识
  • android:writePermission 该provider的写权限标识
  • android:permission 该provider的读写权限标识
  • android:grantUriPermissions 临时权限标识,true时,意味着该provider下所有数据均可被临时使用;false时,则反之。但可以通过设置<grantUriPermission>标签来指定哪些路径可以被临时使用。举个例子,比如你开发了一个邮箱应用,其中含有附件需要第三方应用打开,但第三方应用又没有向你申请该附件的读权限,但如果你设置了此标签,则可以在start第三方应用时,传入FLAG_GRANT_READ_URI_PERMISSION或FLAG_GRANT_WRITE_URI_PERMISSION来让第三方应用临时具有读写该数据的权限。

实例:
1)声明一个权限
<permission android:name="me.pengtao.READ" android:protectionLevel="normal"/>
2)配置

<provider
android:authorities="me.pengtao.contentprovidertest"
android:name=".provider.TestProvider"
android:readPermission="me.pengtao.READ"
android:exported="true">
</provider>

3)其他应用中可以使用以下权限来对TestProvider进行访问<uses-permission android:name="me.pengtao.READ"/>

对不同的数据表有不同的权限操作,要如何做呢?Android为这种场景提供了provider的子标签<path-permission>,path-permission包括了以下几个标签。

<path-permission android:path="string"
android:pathPrefix="string"
android:pathPattern="string"
android:permission="string"
android:readPermission="string"
android:writePermission="string" />

六、开发ContentProvider步骤

1、步骤一:暴露数据

  1. 开发一个ContentProvider的子类,默认需要实现上面的6个方法。
    数据访问的方法(如:insert和update)可能被多个线程同时调用,此时必须是线程安全的。其他方法(如: onCreate())只能被应用的主线程调用,它应当避免冗长的操作。ContentResolver(内容解析者)请求被自动转发到合适的内容提供者实例,所以子类不需要担心跨进程调用的细节。

实例代码
实现的ContentProvider子类中:

private final static int TEST = 100;

static UriMatcher buildUriMatcher() {
    final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
    final String authority = TestContract.CONTENT_AUTHORITY;//必须和清单文件中配置的一致
    matcher.addURI(authority, TestContract.PATH_TEST, TEST);
    return matcher;
}

@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
    Cursor cursor = null;
    switch (buildUriMatcher().match(uri)) {
        case TEST://匹配不同的表
            cursor = db.query(TestContract.TestEntry.TABLE_NAME, projection, selection, selectionArgs, sortOrder, null, null);
            break;
    }
    return cursor;
}

@Nullable
@Override
public Uri insert(Uri uri, ContentValues values) {
    final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    Uri returnUri;
    long _id;
    switch ( buildUriMatcher().match(uri)) {
        case TEST:
            _id = db.insert(TestContract.TestEntry.TABLE_NAME, null, values);
            if ( _id > 0 )
                returnUri = TestContract.TestEntry.buildUri(_id);
            else
                throw new android.database.SQLException("Failed to insert row into " + uri);
            break;
        default:
            throw new android.database.SQLException("Unknown uri: " + uri);
    }
    return returnUri;
}

  1. 在清单文件中配置
<provider    
    android:authorities="me.pengtao.contentprovidertest" //一般为包名.含义 
    android:name=".provider.TestProvider"
    android:exprorted="true"/>//是否允许其他应用调用

2、步骤二:获取ContentResolver对象,并使用

  1. 获取ContentResolver对象。Context的方法:getContentResolver(),因此Activity,Service都能获得该对象
  2. 调用ContentResolver的insert,delete,update,query方法
ContentValues contentValues = new ContentValues();
contentValues.put(TestContract.TestEntry.COLUMN_NAME, "peng");
contentValues.put(TestContract.TestEntry._ID, System.currentTimeMillis());
getContentResolver().insert(TestContract.TestEntry.CONTENT_URI, contentValues);
Cursor cursor = getContentResolver().query(TestContract.TestEntry.CONTENT_URI, null, null, null, null);
try {
    Log.e("ContentProviderTest", "total data number = " + cursor.getCount());
    cursor.moveToFirst();
    Log.e("ContentProviderTest", "total data number = " + cursor.getString(1));
} finally {
    cursor.close();
}

参考文献

ContentProvider从入门到精通
Uri详解之——Uri结构与代码提取
Android开发之内容提供者——创建自己的ContentProvider(详解)

推荐阅读更多精彩内容