1、ContentProvider概念
content provider类是android提供的四大组件之一,用于跨进程的内容分享。 通过content provider提供的机制,统一对外的数据接口,可以方便安全的分享或获取其他应用的数据。 一般如果没有暴露数据给第三方的需求,其实很少会用到这个类。
2、ContentProvider类
使用contentprovider同其他组件一样,需继承ContentProvider并实现相关方法即可。 如下
上面的方法中包含了常用的增删改查等,我们根据需要完成相关的方法即可。 contentprovider类只是提供了一种接口规范,但并没有提供具体实现, 故在数据的具体存储可以选择常用的数据库, 也可以选择sp文件,也可以是其他方式。
2.1、Uri类
contentprovider的增删改查的方法中,都包含有一个Uri的参数,通过该参数可以定位到具体的数据,从而进行相关的操作。
借用网络上一张图,图中说明了uri的构成,其中的authority相当于是content provider的唯一标识符, 有点类似于应用的包名。并且两个具有相同authrity的provider是无法同时安装到android设备的。 其次图中的User字段一般是对应表名,这样在contentprovider的增删改查方法中可以通过uri获取相关的表名,从而进行相关的操作。(这里也可以不是表名,而是其他自定义的字符,可以通过uri获取到path字段,约定好操作相关的数据)
2.2、UriMatcher类
UriMatcher是一个辅助类,用于将符合某种形式的Uri映射到某个自定义的code,从而方便操作数据。
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(authority, "helloworld", 0x100); // 这里的path对应之前创建的表名
mUriMatcher.addURI(authority, "helloworld/#", 0x101); // 这里的#匹配数字
mUriMatcher.addURI(authority, "helloworld/*", 0x102); // 这里*号匹配任意字符
// 还可以添加其他类型的uri
int code = mUriMathcer.match(uri); // 此时mUriMatcher会根据uri的类型返回相应的整型值,方便对操作分类。
// 当uri为content://com.test.demo/helloworld会返回0x100
// 当uri为content://com.test.demo/helloworld/2会返回0x101
// 当uri为content://com.test.demo/helloworld/description会返回0x102
2.3、用数据库的方式实现
有了uri就可以定位数据了,然后我们需要确定数据的存储方式。此处我们使用常见的数据库的方式。
2.3.1、定义SqliteOpenHelper
SqliteOpenHelper类是android系统提供用来操作数据库的封装类。 在此类中我们根据业务需要定义和创建好相应的数据库表,以及处理数据库的升级逻辑等。 具体可参考这里。
在content provider中持有该类则可以操作相关的数据库。
2.3.2、实现增删改查
2.3.2.1、查询
contentprovider的查询实现
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Log.d(TAG, "query() returned: " + uri);
int code = mUriMatcher.match(uri);
// 下面的table名可以从这种方式获取
Log.i(TAG, "path is : " + uri.getPathSegments().get(0));
SQLiteDatabase db = mDbHelper.getReadableDatabase();
Cursor cursor = null;
switch (code) {
case 0x100:
// 查所有, 此处写死了表名。也可以通过uri动态获取表名
cursor = db.query("helloworld", projection, selection, selectionArgs, null, null, sortOrder);
break;
case 0x101:
// 查单个数据
String queryId = uri.getPathSegments().get(1); // 通过uri获取查询的id
// 在指定列上查询
cursor = db.query("helloworld", projection, "age = ?", new String[]{queryId}, null, null, sortOrder);
break;
default:
break;
}
return cursor;
}
客户端调用如下
ContentResolver cr = getContentResolver();
//Cursor cursor = cr.query(Uri.parse("content://com.helloworld.database/helloworld"), null, null, null, null); 查询所有的数据
Cursor cursor = cr.query(Uri.parse("content://com.helloworld.database/helloworld/28"), null, null, null, null); 指定条件查询
if (cursor != null) {
while (cursor.moveToNext()) {
int nameColumnId = cursor.getColumnIndex("name");
int ageColumnId = cursor.getColumnIndex("age");
String name = cursor.getString(nameColumnId);
int age = cursor.getInt(ageColumnId);
Log.i(TAG, "----> res : " + name + ", " + age);
}
} else {
Log.i(TAG, "query cursor is null");
}
2.3.2.2、插入
contentprovider的插入实现
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
// contentvalues是键值对,key是列名, name是相当对应列的值
Log.d(TAG, "insert() returned: " + uri);
SQLiteDatabase db = mDbHelper.getWritableDatabase();
int code = mUriMatcher.match(uri);
long rowId = -1L;
switch (code) {
case 0x100:
case 0x101:
rowId = db.insert("helloworld", null, values);
break;
default:
break;
}
// 返回插入数据后的uri
return Uri.withAppendedPath(uri, String.valueOf(rowId));
}
客户端的调用如下
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
// name与age均是数据库表中定义的列名
values.put("name", mEtName.getText().toString());
values.put("age", Integer.parseInt(mEtAge.getText().toString()));
Uri res = cr.insert(Uri.parse("content://com.helloworld.database/helloworld"), values);
2.3.2.3、更新
contentprovider更新的实现
Log.d(TAG, "update() returned: " + uri);
int code = mUriMatcher.match(uri);
SQLiteDatabase db = mDbHelper.getWritableDatabase();
int rowId = -1;
switch (code) {
case 0x100:
// 查所有
rowId = db.update("helloworld", values, selection, selectionArgs);
break;
case 0x101:
// 查单个
String queryId = uri.getPathSegments().get(1); // 通过uri获取查询的id
rowId = db.update("helloworld", values, "age = ?", new String[]{queryId});
break;
default:
break;
}
return rowId;
客户端的调用如下:
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put("name", "haha1");
values.put("age", 1001);
int rowId = cr.update(Uri.parse("content://com.helloworld.database/helloworld"), values, "age = ?", new String[]{100});
2.3.2.4、删除
contentprovider删除的实现
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.d(TAG, "delete() returned: " + uri);
int code = mUriMatcher.match(uri);
SQLiteDatabase db = mDbHelper.getWritableDatabase();
int rowId = -1;
switch (code) {
case 0x100:
rowId = db.delete("helloworld", selection, selectionArgs);
break;
case 0x101:
String queryId = uri.getPathSegments().get(1);
rowId = db.delete("helloworld", "age = ?", new String[]{queryId});
break;
default:
break;
}
return rowId;
}
客户端的调用如下
ContentResolver cr = getContentResolver();
int rowId = cr.delete(Uri.parse("content://com.helloworld.database/helloworld"), "age = ?", new String[]{mEtAge.getText().toString()});
2.3.3、getType(Uri uri)方法
contentprovider抽象类中还提供了一个方法getType()。该方法的注释如下
Implement this to handle requests for the MIME type of the data at the given URI。
如果该uri是针对单条数据的,必需返回以vnd.android.cursor.item开头的字符。 如果是针对多条数据的,则需返回以vnd.android.cursor.dir开头的字符。具体实现示例如下:
// 此处的single和multi是自定义的字符
if (code == 0x100) {
return "vnd.android.cursor.item/single";
} else if (code == 0x101) {
return "vnd.android.cursor.dir/multi";
}
// 联想其他的mime形式,也是如此。 text/css, text/html。
但是这个类有什么作用呢?
在intent中指定相关uri跳转时,系统会先根据uri找到相关的contentprovider,然后调用getType方法拿到这里指定的mime type。 然后再去比对activity中申明的mime,过滤后得到正确的activity然后打开。
个人不是太明白,为什么要通过contentprovider的方法来找到相关activity,可能是没有找到具体的使用场景。 上图摘自此文章。
2.4、用sp文件的方式实现
同理, 先定义authority及uri等, 如下
private static String mStringPath = "string/*/*/"; // string/spname/key/value/, 这里的路径可以自定义,只要在读写时一致即可。
public static final int STRING_CONTENT_URI_CODE = 100;
private static final UriMatcher sURIMatcher;
static {
sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sURIMatcher.addURI(AUTHORITY, mStringPath, STRING_CONTENT_URI_CODE); // 此处只演示string的存取
}
其次,在增删改查的方法中通过uri获取sp文件名、key及默认value值。 如下
Model model = new Model(); // model中持有sp文件名,key及value
model.setSpName(uri.getPathSegments().get(1));
if (uri.getPathSegments().size() > 2) {
model.setKey(uri.getPathSegments().get(2));
}
if (uri.getPathSegments().size() > 3) {
model.setDefValue(uri.getPathSegments().get(3));
}
有了sp文件名以及key、value,就可以向相关文件中写入及读取相关信息了。
如下示例实现query方法。
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Model model = getModel(uri);
if (model == null) return null;
int code = sURIMatcher.match(uri);
Cursor cursor = buildCursor(getContext(), model, code);
return cursor;
}
/**
* 从sp中获取数据
*
* @return
*/
private Cursor buildCursor(Context context, Model model, int code) {
Object value = null;
Object defValue = model.getDefValue();
switch (code) {
case STRING_CONTENT_URI_CODE:
// sp相关的操作
SharedPreferences sp = context.getSharedPreferences("sp_name", Context.MODE_PRIVATE);
if (defValue == null) {
value = sp.getString(model.getKey(), "");
} else {
value = sp.getString(model.getKey(), String.valueOf(defValue));
}
break;
// 其他数据类型同上
default:
break;
}
if (value == null) return null;
String[] columnNames = {COLUMNNAME}; // 指定列名,客户端查询时指定的列名
// 构建cursor
MatrixCursor cursor = new MatrixCursor(columnNames);
Object[] values = {value}; // 指定列名对应的值,注意这里长度要与列名一致
cursor.addRow(values);
return cursor;
}
其他方法同理实现。 在客户端调用时如下
String authority = "com.demo.helloworld.active.app"; // contentprovider中指定的authroity
Uri uri = PreferenceProviderUtil.buildUri(mContext, PreferenceProviderUtil.STRING_CONTENT_URI_CODE, authority, "active_apps", "");
ContentResolver cr = mContext.getContentResolver();
Cursor cursor = cr.query(uri, null, null, null, null);
if (cursor == null) {
return;
}
if (cursor.moveToNext() && cursor.getColumnIndex("ACTIVE_APP") > -1) {
String result = cursor.getString(cursor.getColumnIndex("ACTIVE_APP"));
return
}
// 构建uri,读取时按此规则即可。
public static Uri buildUri(Context context, int code, String authority, String key, Object value) {
Uri uri = null;
String spName = "sp_name";
switch (code) {
case STRING_CONTENT_URI_CODE:
uri = Uri.parse("content://" + authority + "/" + "string/" + spName + "/" + key + "/" + value);
break;
default:
break;
}
return uri;
}
3、ContentResolver类
contentresolver是contentprovider和uri之前的桥梁,contentresolver通过uri来操作contentprovider。
4、用Observer实现对contentprovider数据的监听
tempObserver = new TempObserver(this, sWorker); // 自定义一个observer,用于监听uri上数据变化。
Uri tempUri = Uri.parse("xxxx");
getContentResolver().registerContentObserver(tempUri, true, tempObserver);
此时当uri上有数据变化时,会回调observer中的onChange方法,在此可以执行相关数据变更后的逻辑。
5、contentprovider的其他问题
在使用contentprovider过程有时出现了被依赖的contentprovider进程挂掉时,依赖contentprovider的进程也挂掉的情况。
可以参考这篇文章。
但是这篇文章只分析了系统应用存在这个现象,但是我的情况是第三方应用也出现了这个现象,并且是有时候会挂掉,有时候正常。
暂时还没分析出问题的原因。