ContentProvider 提供了一个接口用来发布数据,通过ContentResolver 来使用该数据。它们允许将使用数据的应用程序组件和底层数据源分离开来,并提供一种通用的机制来允许一个应用程序共享它们的数据或者使用其它应用程序供的数据。
一、Uri介绍
Uri代表要操作的数据,Uri主要包含了两部分信息:
- 需要操作的ContentProvider
- 对ContentProvider中的什么数据进行操作
URI主要分三个部分:scheme
, authority
and path
。其中authority
又分为host
和port
。
- 格式如下:
scheme://host:port/path
ContentProvider(内容提供者)的scheme已经由Android所规定, scheme为:content://主机名(或叫Authority)用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。路径(path)可以用来表示我们要操作的数据,路径的构建应根据业务而定。
如下以person 表为例
- 要操作person表中id为10的记录,可以构建这样的路径:/person/10
- 要操作person表中id为10的记录的name字段, person/10/name
- 要操作person表中的所有记录,可以构建这样的路径:/person
- 要操作xxx表中的记录,可以构建这样的路径:/xxx
使用Uri类中的parse()方法把一个字符串转换成Uri,如下:
Uri uri = Uri.parse("content://com.cfox.contentprovid.PersonProvider/person")
当然要操作的数据不一定来自数据库,也可以是文件、xml或网络等其他存储方.
二、UriMatcher和ContentUris使用介绍
因为Uri代表了要操作的数据,所以我们经常需要解析Uri,并从Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher和ContentUris 。掌握它们的使用,会便于我们的开发工作。
UriMatcher
UriMatcher类用于匹配Uri,它的用法如下:
-
首先第一步把你需要匹配Uri路径全部给注册上
如下://常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码 UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH); //如果match()方法匹配content://com.cfox.contentprovid.PersonProvider/person路径,返回匹配码为1 sMatcher.addURI("com.cfox.contentprovid.PersonProvider", "person", 1);//添加需要匹配uri,如果匹配就会返回匹配码 //如果match()方法匹配content://com.cfox.contentprovid.PersonProvider/person/230路径,返回匹配码为2 sMatcher.addURI("com.cfox.contentprovid.PersonProvider", "person/#", 2);//#号为通配符 switch (sMatcher.match(Uri.parse("content://com.cfox.contentprovid.PersonProvider/person/10"))) { case 1 break; case 2 break; default://不匹配 }
注册完需要匹配的Uri后,就可以使用sMatcher.match(uri)
方法对输入的Uri进行匹配。如果匹配就返回匹配码,匹配码是调用 addURI()
方法传入的第三个参数,假设匹配content://com.cfox.contentprovid.PersonProvider
路径,返回的匹配码为1。
ContentUris使用介绍
ContentUris类用于操作Uri路径后面的ID部分,它有两个比较实用的方法:
-
withAppendedId(uri, id)用于为路径加上ID部分:
Uri uri = Uri.parse("content://com.cfox.contentprovid.PersonProvider/person") Uri resultUri = ContentUris.withAppendedId(uri, 10); //生成后的Uri为:content://com.cfox.contentprovid.PersonProvider/person/10
-
parseId(uri)方法用于从路径中获取ID部分:
Uri uri = Uri.parse("content://com.cfox.contentprovid.PersonProvider/person/10") long personid = ContentUris.parseId(uri);//获取的结果为:10
三、ContentProvider(内容提供者)共享数据
ContentProvider在android中的作用是对外共享数据,可以通过ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过ContentProvider对你应用中的数据进行增删改查。关于数据共享,以前我们学习过文件操作模式,知道通过指定文件的操作模式为Context.MODE_WORLD_READABLE或Context.MODE_WORLD_WRITEABLE同样也可以对外共享数据。那么,这里为何要使用ContentProvider对外共享数据呢?是这样的,如果采用文件操作模式对外共享数据,数据的访问方式会因数据存储的方式而不同,导致数据的访问方式无法统一,如:采用xml文件对外共享数 据,需要进行xml解析才能读取数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读取数据。使用ContentProvider对外共享数据的好处是统一了数据的访问方式。
ContentProvider对外共享数
第一步需要继承ContentProvider并重写下面方法:
public class BaseContentProvider extends ContentProvider {
@Override
public boolean onCreate() {
// TODO Auto-generated method stub
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
// TODO Auto-generated method stub
return null;
}
@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO Auto-generated method stub
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// TODO Auto-generated method stub
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
// TODO Auto-generated method stub
return 0;
}
}
ContentProvider类主要方法的作用:
- public boolean onCreate():该方法在ContentProvider创建后就会被调用,Android开机后,ContentProvider在其它应用第一次访问它时才会被创建,同时,要讲返回值修改成true。
- public Uri insert(Uri uri, ContentValues values):该方法用于供外部应用往ContentProvider添加数据。
- public int delete(Uri uri, String selection, String[] selectionArgs):该方法用于供外部应用从ContentProvider删除数据。
- public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs):该方法用于供外部应用更新ContentProvider中的数据。
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder):该方法用于供外部应用从ContentProvider中获取数据。
- public String getType(Uri uri):该方法用于返回当前Url所代表数据的MIME类型。
如果操作的数据属于集合类型,那么MIME
类型字符串应该以vnd.android.cursor.dir/
开头。
例如:要得到所有person记录的Uri为content://com.cfox.contentprovid.PersonProvider/person
那么返回的MIME类型字符串应该为:"vnd.android.cursor.dir/person"
如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以vnd.android.cursor.item/开头
例如:得到id为10的person记录,Uri为content://com.cfox.contentprovid.PersonProvider/person/10
那么返回的MIME类型字符串为:"vnd.android.cursor.item/person"
public String getType(Uri uri) {
UriMatcher uriMatcher = getUriMatcher();
int match = uriMatcher.match(uri);
switch (match) {
case ITEMS:
return "vnd.android.cursor.dir/" + person;
case ITEMS_ID:
return "vnd.android.cursor.item/" + person;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
第二步需要在AndroidManifest.xml使用对该ContentProvider进行配置,为了能让其他应用找到该ContentProvider ,ContentProvider采用了authorities(主机名/域名)对它进行唯一标识,你可以把ContentProvider看作是一个网 站(想想,网站也是提供数据者),authorities 就是他的域名:
四、使用ContentResolver操作ContentProvider中的数据
当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用ContentResolver 类来完成,要获取ContentResolver 对象,可以使用Activity提供的getContentResolver()方法。 ContentResolver 类提供了与ContentProvider类相同签名的四个方法:
public Uri insert(Uri uri, ContentValues values):该方法用于往ContentProvider添加数据。
public int delete(Uri uri, String selection, String[] selectionArgs):该方法用于从ContentProvider删除数据。
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs):该方法用于更新ContentProvider中的数据。
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder):该方法用于从ContentProvider中获取数据。
这些方法的第一个参数为Uri,代表要操作的ContentProvider
和对其中的什么数据进行操作, 例如 Uri是:Uri.parse("content://com.cfox.contentprovid.PersonProvider/person /10")
,那么将会对主机名为com.cfox.contentprovid.PersonProvider
的ContentProvider
进行操作,操作的数 据为person
表中id为10的记录。 使用ContentResolver
对ContentProvider
中的数据进行添加、删除、修改和查询操作。
五、监听ContentProvider中数据的变化
如果ContentProvider
的访问者需要知道ContentProvider
中的数据发生变化,可以在ContentProvider
发生数据变化时调用getContentResolver().notifyChange(uri, null)
来通知注册在此URI上的访问者,例子如下:
public class BaseContentProvider extends ContentProvider {
略.........
@Override
public Uri insert(Uri uri, ContentValues values) {
//添加数据变化通知
getContext().getContentResolver().notifyChange(uri, null);
return 返回一个Uri;
}
略........
}
如果ContentProvider
的访问者需要得到数据变化通知,必须使用ContentObserver
对数据(数据采用uri描述)进行监听,当监听到数据变化通知时,系统就会调用ContentObserver
的onChange()
方法:
public class MainActivity extends Activity {
private Handler mHandler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.getContentResolver().registerContentObserver(PersonProvider.uri, false, new MyObserver(mHandler));
}
public class MyObserver extends ContentObserver{
public MyObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
//数据改变时调用
System.out.println("changing ------>");
}
}
}
六、ContentProvider 进行批量操作
-
批量插入
在ContentProvider
中有特定的方法进行批量插入bulkInsert
, 在ContentProvider
中从写这个方法就可以实现批量插入的功能。下面来看一下, 在实际开发中的一种应用。@Override public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { int result; synchronized (this) { SQLiteDatabase db = mDBHelper.getWritableDatabase(); try { db.beginTransaction(); result = super.bulkInsert(uri, values); db.setTransactionSuccessful(); }finally { db.endTransaction(); } } return result; }
从上面代码中可以看出,是为了使用事务批量插入数据提高插入效率。代码很简单,不详细介绍,这里说一下
super.bulkInsert(uri, values);
这个行代码, 下面看一下ContentProvider
类中是怎么实现的。public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { int numValues = values.length; for (int i = 0; i < numValues; i++) { insert(uri, values[i]); } return numValues; }
看到
insert
相信很多人,就明白了,这里是遍历ContentValues
中的每一个value , 最后还是通过insert
方法进行操作。调用方法
通过ContentResolver 中bulkInsert
方法进行批量操作:ContentResolver mcr = getContentResolver(); ---- ContentValues value1 = new ContentValues(); ContentValues value2 = new ContentValues(); ContentValues value3 = new ContentValues(); ContentValues[] contentValues = {value1, value2, value3}; int num = mcr.bulkInsert(mUri, contentValues);
-
批量操作
在ContentProvider中提供了一个功能更加丰富的批量操作方法applyBatch
,使用这个可以进行增、删、 改、 批量操作。还是先从一个使用例子开始。@Override public ContentProviderResult[] applyBatch(@NonNull ArrayList
operations) throws OperationApplicationException { ContentProviderResult[] results; synchronized (this) { SQLiteDatabase db = mDBHelper.getWritableDatabase(); try { db.beginTransaction(); results = super.applyBatch(operations); db.setTransactionSuccessful(); }finally { db.endTransaction(); } } return results; } 看了上面代码和
批量插入
有些类似,在这些代码中要注意两段代码,ContentProviderOperation
和super.applyBatch(operations);
下面先来看看如何使用:
private static final String AUTHORITY = "com.cfox.contentprovid.PersonProvider"; ---- ContentResolver mcr = getContentResolver(); ---- ArrayList
operations = new ArrayList (); ContentValues values = new ContentValues(); operations.add(ContentProviderOperation.newInsert(mUri).withValues(values).build()); ContentValues values = new ContentValues(); operations.add(ContentProviderOperation.newUpdate(mUri) .withSelection("name = ?" , new String[]{"xxx"}) .withValues(values).build()); operations.add(ContentProviderOperation.newDelete(mUri) .withSelection("name = ?" , new String[]{"xxx"}).build()); try { mcr.applyBatch(AUTHORITY, operations); } catch (OperationApplicationException e) { e.printStackTrace(); } catch (RemoteException e) { e.printStackTrace(); } 上面代码不讲解,下面通过源码介绍一下在
ContentProvider
中是怎么进行批量操作的。
先看一下ContentProviderOperation
中的newInsert
、newUpdate
、newDelete
三个方法实现:public static Builder newInsert(Uri uri) { return new Builder(TYPE_INSERT, uri); } public static Builder newUpdate(Uri uri) { return new Builder(TYPE_UPDATE, uri); } public static Builder newDelete(Uri uri) { return new Builder(TYPE_DELETE, uri); }
三个方法都是创建了一个
Builder
实例,返回的也是Builder
对象而不是ContentProviderOperation
,不同的是传入的type
不同。那么在什么时候创建ContentProviderOperation
呢?在我们调用Builder
实例中的build()
方法是创建。下面来看一下具体代码:public static class Builder { private final int mType; .... //省略部分代码 private Builder(int type, Uri uri) { if (uri == null) { throw new IllegalArgumentException("uri must not be null"); } mType = type; mUri = uri; } .... //省略部分代码 }
在
build
的时候将mType
设置给ContentProviderOperation
中的mType
.所以才有下面使用mType
判断是什么操作。在看一下ContentProvider 中
applyBatch
方法实现:public @NonNull ContentProviderResult[] applyBatch( @NonNull ArrayList
operations) throws OperationApplicationException { final int numOperations = operations.size(); final ContentProviderResult[] results = new ContentProviderResult[numOperations]; for (int i = 0; i < numOperations; i++) { results[i] = operations.get(i).apply(this, results, i); } return results; } 上面代码就是遍历出
list
中的ContentProviderOperation
然后调用apply
方法,但是,到这里要注意apply
方法中有一个参数是this
。继续查看ContentProviderOperation
中apply
方法实现:public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs, int numBackRefs) throws OperationApplicationException { ContentValues values = resolveValueBackReferences(backRefs, numBackRefs); String[] selectionArgs = resolveSelectionArgsBackReferences(backRefs, numBackRefs); if (mType == TYPE_INSERT) { Uri newUri = provider.insert(mUri, values); if (newUri == null) { throw new OperationApplicationException("insert failed"); } return new ContentProviderResult(newUri); } int numRows; if (mType == TYPE_DELETE) { numRows = provider.delete(mUri, mSelection, selectionArgs); } else if (mType == TYPE_UPDATE) { numRows = provider.update(mUri, values, mSelection, selectionArgs); } else if (mType == TYPE_ASSERT) { .... //省略部分代码, 如果感兴趣,自己阅读研究 } else { Log.e(TAG, this.toString()); throw new IllegalStateException("bad type, " + mType); } if (mExpectedCount != null && mExpectedCount != numRows) { Log.e(TAG, this.toString()); throw new OperationApplicationException("wrong number of rows: " + numRows); } return new ContentProviderResult(numRows); }
看到这里,就会恍然大悟,原来是在这里通过
mType
这个属性,判断增、删、改的,让后在通过传入的ContentProvider
调用继承ContentProvider
实现的insert
、update
、delete
方法。
七、ContentProvider call 方法
先看一个使用例子:
ContentProvider 实现:
@Override
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
extras.putString("data", "this is data");
return extras;
}
在activity 中调用call 实现:
ContentResolver mcr = getContentResolver();
----
Bundle bundleArg = new Bundle();
bundleArg.putString("good", "hello");
Bundle bundle = mcr.call(mUri, "callMethod","call,,,,,,",bundleArg);
Log.d(TAG, "callMethod: data:" + bundle.get("data"));
调用和被调用参数都是相同的,注意:所有参数不可为NULL
, 可以使用这个方法使用Bundle
进行数据交互。
八、ContentProvider 文件共享
在ContentProvider
中用于共享大的文件的时候比较多。文件会以流的形式传递。
下面是一个简单的使用例子:
ContentProvider
中示例:
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
String basePath = Environment.getExternalStorageDirectory().getPath() + "/aaa.pdf";
File file = new File(basePath);
ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return descriptor;
}
activity 中调用示例:
Uri mUri = "";
----
ContentResolver mcr = getContentResolver();
----
String basePath = Environment.getExternalStorageDirectory().getPath() + "/bb.pdf";
File file = new File(basePath);
if (!file.isFile()) {
try {
boolean b = file.createNewFile();
Log.d(TAG, "AssetFile: b:" + b);
} catch (IOException e) {
e.printStackTrace();
}
}
try {
AssetFileDescriptor descriptor = mcr.openAssetFileDescriptor(mUri, "r");
InputStream in = descriptor.createInputStream();
OutputStream out = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0 , len);
}
out.flush();
in.close();
out.close();
}catch (Exception e) {
e.printStackTrace();
}finally {
Log.d(TAG, "AssetFile: successs....");
}
上面代码不解释了,写个demo体会一下,很简单的, 这篇文章只是引路,告诉你ContentProvider
中有这些东西,具体在项目中如何使用,还要结合实际场景。
九、几点注意
- 不要在UI线程中调用, 容易出现ANR。
-
ContentProvider
中onCreate
在运行进程的主线程中执行。如果``ContentProvider和应用运行在同一进程,所以尽量不要在
onCreate中执行耗时的操作或者不要做任何事情,如果在
onCreate`中执行耗时的代码,会影响应用启动速度。 - 如果
ContentProvider
和调用者在同一个进程,则ContentProvider
和调用者在同一个线程。 - 如果
ContentProvider
和调用者不在同一个进程,onCreate
在进程的主线程中执行,insert
、update
、delete
、query
、call
等在同一个线程中执行(非主线程)。
附:
在android4.2 在使用到 ContentProvider 的时候遇到的问题,报错: Permission Denial: opening provider uid=10033) .... that is not exported from uid 10036
在AndroidManifest.xml
中 android:exported
属性,这个属性用于指示该服务是否能被其他程序应用组件调用或跟他交互; 取值为(true | false),如果设置成true,则能够被调用或交互,否则不能;设置为false时,只有同一个应用程序的组件或带有相同用户ID的应用程序才能启动或绑定该服务。
- 重要:它的默认值是依赖于该服务所包含的过滤器
- 如果没有过滤器则意味着该服务只能通过指定明确的类名来调用,也就是说该服务只能在应用程序内部使用(因为其他外部使用者不会知道该服务的类名),此时它的默认值是false
- 如果至少包含了一个过滤器,则意味着该服务可以给外部的其他应用提供服务,因此默认值是true。
本文Uri 部分参考 :http://blog.sina.com.cn/s/blog_9f233c070101euqx.html
注:以上内容,如有不对之处请及时指出,相互交流学习。