Android内部存储是仅对于应用本身提供读写的,而此时另一个进程需要读写这个应用的内部数据时ContentProvider就是可供选择的方法之一(AIDL (基于Binder) 、ContentProvider(基于Binder)、Socket等),比较来说ContentProvider更简便一些,但提供的功能比较有限(只有增、删、改、查)。这里我把ContentProvider认为是Android系统两个进程间传输命令和数据的的一个载体。
ContentProvider是Android四大组件之一;
应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。
其实简便来说,ContentProvider就是实现进程间数据交互 和共享,即跨进程通信
ContentProvider实现形式可以说就是一个典型的C/S模式,进程一(C端)通过Uri找到另一进程二(S端),来处理相关数据或者业务。在使用ContentProvider前,我们先了解一下跟ContentProvider有关的一些内容。
1、Uri介绍
Uri定义:Uri(Uniform Resource Identifier)即统一资源标识符,是一个用于标识某一互联网资源名称的字符串。
Uri作用:标识 ContentProvider,即一进程需通过统一资源标识符Uri来找到另一进程的ContentProvider,然后再操作对应的数据。
Uri的使用:在Android系统中Uri分为系统预置(系统预置主要为android系统上所有可用的每种资源 :图像、视频等)
和自定义数据处理两类。
在Android系统中提供了两个用于操作Uri的工具类,分别为UriMatcher 和ContentUris ;因此以下简单的过一下Uri的命名和两个工具类的使用。
如果你想更详细的了解Uri,可以移驾
1.1、Uri的命名规则
首先我们先了解一下URI的格式:
Uri的格式:[scheme:][//host:port][path][?query][#fragment]
或[scheme:][//authority][path][?query][#fragment]
,通过以下这幅例子图加强理解:
–# 号为通配符
–* 号为任意字符
通过以上这幅图,我们可以大致看出看来,在Android平台,URI主要分三个部分:scheme, authority(host:port)和path(path/pathid)
部分 | 说明 |
---|---|
scheme | Android中固定为content://,或者为自己自定义名 |
authority(host:port) | 授权:指定内容提供者的名称,例如联系人,浏览器等。第三方的内容提供者可以是全名,如:com.example.test |
path | ContentProvider中数据表的表名 |
pathid | 数据表中数据的标识,可选字段,如果为空则返回所有数据 |
1.2、UriMatcher类
UriMatcher 类主要用于匹配Uri,主要两个方法——addURI和match方法。
1、首先初始化UriMatcher类;
//常量UriMatcher.NO_MATCH 不匹配任何路径的返回码
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
2、其次需注册需要的Uri;注册表和对应的位置符号码
int URI_PATH_ID_1 = 1;
int URI_PATH_ID_2 = 2;
/***
*addURI方法参数说明
*第一个参数authority:为String authority(应用包名)
*第二个参数path:操作的表单路径(如果没有“/”即为表名)
*第三个参数code:当调用UriMatch的match(uri)方法后,会返回uri匹配成功后返回的code码,
* 如果没有匹配match方法将返回-1
*/
matcher.addURI("com.exmaple.test", "tabname1", URI_PATH_ID_1);
matcher.addURI("com.exmaple.test", "tabname2", URI_PATH_ID_2);
这里需要注意,在sdk版本18开始,path参数支持以'/'开头,即addURI("com.yfz.Lesson", "/people", PEOPLE);
3、最后与已经注册的Uri进行匹配;
Uri uri = Uri.parse("content://com.exmaple.test/tabname");
int match = matcher.match(uri);
switch (match) {
// 如果根据Uri匹配的返回码是URI_PATH_ID_1,则在ContentProvider中的名为tabname1的表或路径,相关数据
case URI_PATH_ID_1:
deal_tabname1_method();
// 如果根据Uri匹配的返回码是URI_PATH_ID_2,则在ContentProvider中的名为tabname2的表或路径,相关数据
case URI_PATH_ID_2:
deal_tabname2_method();
default:
throw new IllegalArgumentException("UnSupport Uri : " + uri);
}
1.3、ContentUris类
ContentUris 类用于获取Uri路径后面的ID部分,主要有withAppendedId、parseId、appendId三个方法,
例如Uri "content://com.exmaple.test/tabname"
Uri uri = Uri.parse("content://com.exmaple.test/tabname")
//withAppendedId
//通过withAppendedId方法,为该Uri加上ID
Uri resultUri = ContentUris.withAppendedId(uri, 1);
//结果resultUri的路径为:content://com.exmaple.test/tabname/1
//appendId
//appendId方法用于通过Uri.Builder方式生成的Uri使用
Uri.Builder builUri = new Uri.Builder();
ub.authority("com.exmaple.test").appendPath("tabname");
Uri.Builder resultUri = ContentUris.appendId(builUri,1);
//结果resultUri的路径为:content://com.exmaple.test/tabname/1
//parseId
//从路径中获取ID: parseId(uri)
long pathId = ContentUris.parseId(resultUri);
//结果pathId的只为:1
2、ContentProvider类
ContentProvider是一个抽象类,主要用于S端接受C端的命令,操作相关数据或业务。
ContentProvider抽象类主要有query、insert、delete、update、onCreate、getType几个方法。
//外部进程(B端)应用获取ContentProvider(S端)中的数据
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder)
//外部进程(B端)ContentProvider(S端)中添加数据
public Uri insert(Uri uri, ContentValues values)
//外部进程(B端)删除ContentProvider(S端)中的数据
public int delete(Uri uri, String selection, String[] selectionArgs)
//外部进程(B端)更新ContentProvider(S端)中的数据
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
public boolean onCreate()
// ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvider时 由系统进行调用
public String getType(Uri uri)
// 得到数据类型,即返回当前 Url 所代表数据的MIME类型
备注:ContentProvider是Android中的四大组件之一,所以需要在AndroidManifest.xml文件中进行注册。注册的时候,与注册Activity类似
3、ContentResolver类
ContentProvider类并不会直接与外部进程交互,通过调用Content的getContentResolver()方法获取ContentResolver对象实例,获取对象后根据Uri对ContentProvider进行CRUD(增删改查)操作。
ContentResolver提供了类似ContentResolver的query、insert、delete、update、getType几个方法。
1、获取ContentResolver实例对象
// 可通过在所有继承Context的类中 通过调用getContentResolver()来获得ContentResolver
ContentResolver resolver = getContentResolver();
2、设置ContentResolver对应的Uri
// 设置ContentProvider的URI
Uri uri = Uri.parse("content://com.exmaple.test/tabname");
3、根据URI操作ContentProvider中的数据
// 此处是获取ContentProvider中 tabname表的所有记录,此处只例句query方法,其他insert、delete、update类似
Cursor cursor = resolver.query(uri, null, null, null, null);
4、ContentObserver类
提供的用来监听ContentProvider变化的抽象类。可以通过ContentResolver的registerContentObserver和unregisterContentObserver方法来注册和注销ContentObserver监听器。当被监听的ContentProvider发生变化时,就会回调对应的ContentObserver的onChange回调方法。
1、自定义Handler,注意此为了简便不考虑内存泄漏问题,使用者一定要注意
private class ObserverHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.d(TAG,"handleMessage msg = " + msg);
}
}
2、获取ContentResolver实例对象
// 可通过在所有继承Context的类中 通过调用getContentResolver()来获得ContentResolver
ContentResolver resolver = getContentResolver();
3、注册监听
ObserverHandler mHandler = new ObserverHandler();
ContentObserver mContentObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
mHandler.obtainMessage(CONTENT_PROVIDER_CHANGED).sendToTarget();
}
};
//开始注册
/***
* 第一个参数uri:需要观察的Uri
* 第二个参数notifyForDescendents:如果为true表示以这个Uri为开头的所有Uri都会被匹配到,
* 如果为false表示精确匹配,即只会匹配这个给定的Uri。
* exp:1、content://com.exmaple.test/tabname
* 2、content://com.exmaple.test/tabname/10
* 假如观察的Uri为content://com.exmaple.test/tabname,
* 当notifyForDescendents为true时则以这个Uri开头的Uri的数据变化时都会被捕捉到,
* 在这里也就是1和2的Uri的数据的变化都能被捕捉到;
* 当notifyForDescendents为false时则只有1中Uri变化时才能被捕捉到。
* 第三个参数ContentObserver:为观察者对象
*
*/
getContentResolver().registerContentObserver(STUDENT_URI,true,mContentObserver);
4、通知ContentProvider数据的访问者
//注意,该步骤是在URI对应的ContentProvider自定义类发生的,用于通知外部进程数据已发生变化;
public class UserContentProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
getContext().getContentResolver().notifyChange(uri, null); // 通知访问者
}
5、//注销监听
getContentResolver().unregisterContentObserver(mContentObserver);
}
5、ContentProvider具体使用
对于以上我们对ContentProvider需要用的一些工具和类做了简单的说明,现在开始用完整的例子的来说明ContentProvider的使用,首先看一下如下图,ContentProvider使用的整个步骤:
依照以上步骤图,首先先实现应用B的相关业务。
5.1.1、应用B_创建数据库
数据的创建如果不熟悉的,可以查看《Android存储-SQLite数据库存储数据》
public class DBHelper extends SQLiteOpenHelper {
// 数据库名
private static final String DATABASE_NAME = "data_name.db";
// 表名
public static final String TABLE_NAME_1 = "tabname1";
public static final String TABLE_NAME_2 = "tabname2";
//数据库版本号
public DBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// 创建两个表格:用户表 和职业表
db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME_1 +
"(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " param TEXT)");
db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME_2 +
"(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " param TEXT)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
5.1.2、应用B_实现ContentProvider类
public class CustProvider extends ContentProvider {
// 设置ContentProvider的Uri标识
private final static String AUTHORITY = "com.exmaple.test";
//设置表对于的位置码
private final static int URI_PATH_ID_1 = 1;
private final static int URI_PATH_ID_2 = 2;
private Context mContext;
private SQLiteDatabase mDataBase;
private final static UriMatcher sUriMatcher;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY,"tabname1",URI_PATH_ID_1);
sUriMatcher.addURI(AUTHORITY,"tabname2",URI_PATH_ID_2);
}
@Override
public boolean onCreate() {
mContext = getContext();
mDataBase = new DBHelper(mContext).getWritableDatabase();
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder) {
int uriType = sUriMatcher.match(uri);
Cursor cursor;
switch (uriType) {
case URI_PATH_ID_1:
cursor = mDataBase.query(DBHelper.TABLE_NAME_1,projection,selection,
selectionArgs,null,null,sortOrder,null);
break;
case URI_PATH_ID_2:
cursor = mDataBase.query(DBHelper.TABLE_NAME_2,projection,selection,
selectionArgs,null,null,sortOrder,null);
break;
default:
throw new IllegalArgumentException("UnSupport Uri : " + uri);
}
mContext.getContentResolver().notifyChange(uri,null);
return cursor;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
int uriType = sUriMatcher.match(uri);
long lRow;
switch (uriType) {
case URI_PATH_ID_1:
lRow = mDataBase.insert(DBHelper.TABLE_NAME_1,null, values);
break;
case URI_PATH_ID_2:
lRow = mDataBase.insert(DBHelper.TABLE_NAME_2,null, values);
break;
default:
throw new IllegalArgumentException("UnSupport Uri : " + uri);
return null;
}
mContext.getContentResolver().notifyChange(uri,null);
return ContentUris.withAppendedId(uri, row);
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
int uriType = sUriMatcher.match(uri);
int rowDelete;
switch (uriType) {
case URI_PATH_ID_1:
rowDelete = mDataBase.delete(DBHelper.TABLE_NAME_1,selection,selectionArgs);
break;
case URI_PATH_ID_2:
rowDelete = mDataBase.delete(DBHelper.TABLE_NAME_2,selection,selectionArgs);
break;
default:
throw new IllegalArgumentException("UnSupport Uri : " + uri);
return rowDelete;
}
mContext.getContentResolver().notifyChange(uri,null);
return rowDelete;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values,
@Nullable String selection, @Nullable String[] selectionArgs) {
int uriType = sUriMatcher.match(uri);
int rowUpdate;
switch (uriType) {
case URI_PATH_ID_1:
rowUpdate = mDataBase.update(DBHelper.TABLE_NAME_1,values,selection,selectionArgs);
break;
case URI_PATH_ID_2:
rowUpdate = mDataBase.update(DBHelper.TABLE_NAME_2,values,selection,selectionArgs);
break;
default:
throw new IllegalArgumentException("UnSupport Uri : " + uri);
return rowUpdate;
}
mContext.getContentResolver().notifyChange(uri,null);
return rowUpdate;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
}
5.1.2、应用B_在AndroidManifest中声明ContentProvider
1、无权声明
<!-- 声明ContentProvider -->
<application
...
<provider
android:name=".CustProvider"
<!--authorities ContentProvider的Uri标识符-->
android:authorities="com.exmaple.test"
android:process=":provider"
<!--exported true当前内容提供者可以被其它应用使用,false则只能被内部使用-->
android:exported="true"/>
...
</application>
2、带权限声明
<!-- CustProvider 访问权限声明 -->
<permission
android:name="com.exmaple.test.PROVIDER"
android:protectionLevel="dangerous"
/>
<permission
android:name="com.exmaple.test.Read"
android:protectionLevel="normal"
/>
<permission
android:name="com.exmaple.test.Write"
android:protectionLevel="normal"
/>
<!-- 声明ContentProvider -->
<application
...
<provider
android:name=".CustProvider"
<!--authorities ContentProvider的Uri标识符-->
android:authorities="com.exmaple.test"
<!--注意:外界需要声明同样的读和写或读、写的权限才可进行相应操作,否则会报错-->
<!--声明外界进程可访问该Provider的权限(读 和 写)-->
android:permission="com.exmaple.test.PROVIDER"
<!--权限可细分为读或写的权限-->
android:readPermisson="com.exmaple.test.Read"
android:writePermisson="com.exmaple.test.Write"
android:process=":provider"
<!--true当前内容提供者可以被其它应用使用,false则只能被内部使用-->
android:exported="true"/>
...
</application>
权限声明时protectionLevel设置的是最低风险权限(normal),关于其他等级权限和说明:
权限等级 | 权限说明 |
---|---|
normal | 低风险权限,只要申请了就可以使用,安装时不需要用户确认 |
dangerous | 高风险权限,安装时需要用户确认授权才可使用 |
signature | 只有当申请权限应用与声明此权限应用的数字签名相同时才能将权限授给它 |
signatureOrSystem | 签名相同或者申请权限的应用为系统应用才能将权限授给它 |
这里的authorities唯一标识该内容提供者,这样其它的应用才可以找到该内容提供者并操作它的数据;exported为true当前内容提供者可以被其它应用使用,默认为true。如果对于AndroidMainifest清单不是很了解的可以移步《Android清单文件中相关属性含义(Provider)》
备注:exported如果为false,无需权限定义。如果exported如果为true,可进行权限定义,也可忽略。但凡定义了权限,那么外部进程(应用A)就一定要进行相关权限声明,不然会报错。
5.2.1、应用A_在AndroidManifest中声明ContentProvider相关权限
如果是共享进程(应用B)设置了相关权限,那么外部进程(应用A)一定要在AndroidManifest中声明相关权限才行,如果共享进程(应用B)设置了相关权限,那么就无需理会。
//外部进程权限声明
<uses-permission android:name="com.exmaple.test.PROVIDER"/>
//或单独声明一下两个权限
//
//
<application
...
</application>
5.2.1、应用A_访问ContentProvider的自定义类
public class CRUDContentProvider {
private Context mContext;
//1、声明Uri
private final static String AUTHORITY = "com.exmaple.test";
private final static Uri TABNAME_1_URI = Uri.parse("content://" + AUTHORITY + "/tabname1");
private final static Uri TABNAME_2_URI = Uri.parse("content://" + AUTHORITY + "/tabname2");
ContentResolver resolver;
public CRUDContentProvider(Context mContext) {
this.mContext = mContext;
init();
}
//2、获取ContentResolver
private init() {
resolver = resolver.getContentResolver();
}
//3、完成1和2两部就可以进行对共享进程的增删改操作了,一下例举增加的操作
private void insert() {
//向表1中插入数据
ContentValues contentValues = new ContentValues();
contentValues.put("param","firstdata1");
resolver.insert(TABNAME_1_URI,contentValues);
//向表2中插入数据
ContentValues contentValues = new ContentValues();
contentValues.put("param","firstdata2");
resolver.insert(TABNAME_2_URI,contentValues);
}
private void query() {
//查询表1中数据
Cursor cursor = resolver.query(TABNAME_1_URI, new String[]{"param"},null,null,null);
while (cursor.moveToNext()) {
String param = cursor.getString(cursor.getColumnIndex("param"));
}
//查询表2中数据
Cursor cursor = resolver.query(TABNAME_2_URI, new String[]{"param"},null,null,null);
while (cursor.moveToNext()) {
String param = cursor.getString(cursor.getColumnIndex("param"));
}
}
private void update() {
//更新表1中param为firstdata1数据
ContentValues contentValues = new ContentValues();
contentValues.put("param","updatadata1");
resolver.update(TABNAME_1_URI,contentValues,"param = ?",new String[] {"firstdata1"});
//更新表2中param为firstdata2数据
ContentValues contentValues = new ContentValues();
contentValues.put("param","updatadata2");
resolver.update(TABNAME_2_URI,contentValues,"param = ?",new String[] {"firstdata2"});
}
private void delete() {
//删除表1中param为updatadata1数据
resolver.delete(TABNAME_1_URI,"param = ?",new String[]{"updatadata1"});
//删除表2中param为updatadata2数据
resolver.delete(TABNAME_2_URI,"param = ?",new String[]{"updatadata2"});
}
}
到此ContentProvider的使用就基本叙述完了。
ContentProvider为应用间的数据交互提供了一个较为安全的机制:允许外部进程访问共享进程且对其增、删、改、查操作,由于内部数据存储的空间是有有限的,某种程度上也需要防止外部进程恶意读写数据。
ContentProvider方式降低了底层数据的存储方式耦合。相对于文件的读写和Sharedpreferences读写来说更加的高效一些,但是ContentProvider的使用过程较文件和Sharedpreferences又复杂些,防止这几种数据获取方式各有优缺点,最终因项目情况而定。