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(详解)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容