ContentProvider的使用

1、ContentProvider概念

content provider类是android提供的四大组件之一,用于跨进程的内容分享。 通过content provider提供的机制,统一对外的数据接口,可以方便安全的分享或获取其他应用的数据。 一般如果没有暴露数据给第三方的需求,其实很少会用到这个类。

2、ContentProvider类

使用contentprovider同其他组件一样,需继承ContentProvider并实现相关方法即可。 如下
contentprovider方法
上面的方法中包含了常用的增删改查等,我们根据需要完成相关的方法即可。 contentprovider类只是提供了一种接口规范,但并没有提供具体实现, 故在数据的具体存储可以选择常用的数据库, 也可以选择sp文件,也可以是其他方式。

2.1、Uri类

contentprovider的增删改查的方法中,都包含有一个Uri的参数,通过该参数可以定位到具体的数据,从而进行相关的操作。
借用网络上一张图uri图中说明了uri的构成,其中的authority相当于是content provider的唯一标识符, 有点类似于应用的包名。并且两个具有相同authrity的provider是无法同时安装到android设备的。 其次图中的User字段一般是对应表名,这样在contentprovider的增删改查方法中可以通过uri获取相关的表名,从而进行相关的操作。(这里也可以不是表名,而是其他自定义的字符,可以通过uri获取到path字段,约定好操作相关的数据)
uri解析

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然后打开。

getType方法
个人不是太明白,为什么要通过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;
}

github有一个比较不错的实现。

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的进程也挂掉的情况。
可以参考这篇文章。
但是这篇文章只分析了系统应用存在这个现象,但是我的情况是第三方应用也出现了这个现象,并且是有时候会挂掉,有时候正常。
暂时还没分析出问题的原因。

  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值