关于ContentProvider使用总结

之前看过一篇文章,上面说了解Android的四大组件的小伙伴才算是真正的入门了,仔细想想按照这个标准自己好像都还没有入门,那么便想通过几篇文章来好好研究下四大组件,这里便作为开篇先研究ContentProvider吧,至于为什么先研究ContentProvider,原因是我对它最不熟悉,所以就拿最不熟悉的开刀吧。

既然是谈使用,那么就想来从使用流程上来做一个总结吧,注意这里不会详细的贴出所有代码,只会讲使用大致流程,想看详细代码的小伙伴,请看别的小伙伴写的这篇文章Android基础学习之Provider(内容提供器)

1、说说ContentProvider的使用步骤

从ContentProvider的名字,来看我们都知道它是干啥的,就是提供内容的嘛,这里的内容就是我们要访问的数据,这里以一个以一个ContentProvider来访问Menu数据为例来说明使用步骤。

第一步、定义Menu

创建一个Menu类来存放数据,并定义id、name、price三个字段以及字段对应的get/set方法,在Menu类中出了定义的字段之外,定义以下代码

// 以下定义provider的MIME类型常量
public static final String MIME_DIR_PREFIX = "vnd.android.cursor.dir";// MIME类型 多条
public static final String MIME_ITEM_PREFIX = "vnd.android.cursor.item";// 单条

public static final String MIME_ITEM = "vnd.pub.menus";// 自定义MIME类型字符串

// 将固定前缀+自定义字符串生成两个类型
public static final String MIME_TYPE_SINGLE = MIME_ITEM_PREFIX + "/" + MIME_ITEM;
public static final String MIME_TYPE_MULTIPLE = MIME_DIR_PREFIX + "/" + MIME_ITEM;

// 用来定义uri常量 content://<authority>/<data path>/..../id
public static final String AUTHORITY = "myfs.pub.menusprovider";// 授权者 表示用哪个provider,要跟清单文件中一致
public static final String PATH_SINGLE = "menus/#"; // (menus:数据库表名)路径下单条记录 #代表数字id
public static final String PATH_MULTIPLE = "menus";// 路径下多条记录 数据库将对应表名
public static final String PATH_MULTIPLE_NAME = "menus/*"; // *代表文本 例如按名称访问等

// 组合成所需要的uri字符串
public static final String CONTENT_URI_STRING = "content://" + AUTHORITY + "/" + PATH_MULTIPLE;

// 将uri字符串转换为uri对象
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);

// 涉及sqlite底层实现的内容
public static final String CREAT_TABLE = "create table menus(id integer primary key autoincrement ,name varchar(50),price integer)";
public static final String DBNAME = "pub.db";//数据库名
public static final String TABLENAME = "menus";//表名
public static final int DB_VER = 1;//数据库版本,用于升级用

//常量字符串,对应数据库中的字段
public static final String KEY_ID = "id";
public static final String KEY_NAME = "name";
public static final String KEY_PRICE = "price";

//定义字段名的数组,ContentResolver在执行查询类型操作时会用到
public static final String[] COLUNMS = { KEY_ID, KEY_NAME, KEY_PRICE };

我们看到在Menu中定义了一大堆的变量,这里可以大概理一下这里的变量目的可以分为

a、MIME类型定义
b、URI类型定义
c、数据库初始化参数
d、Menu字段数组

这里的a、b类型参数是必须的(当然你要强制放其它地方也行,不过我觉得放这里比较好),而c、d其实并不是必须的,定义c类型参数原因是因为这里Provider存取数据是通过数据库来进行的,如果不是通过数据库来,显然这里肯定不需要数据库相关参数,比如如果通过服务器来存取数据那么这里有可能就是定义一些网络访问参数了(当然也可以不放Menu里,参数可以放Provider中,或者其它地方),定义d参数的目的是为了方便ContentResolver的query类型方法方便获取对应字段的传参,所以d类型参数一般是放在Menu中的,调用ContentResover调用query代码如下所示

Cursor cursor = cr.query(Menu.CONTENT_URI, Menu.COLUNMS, null, null, null);

第二步、自定义ContentProvider

Android SDK提供了ContentProvider类,但是不能直接拿来用啊,毕竟SDK也不知道你要对外提供什么数据,于是需要我们自定义一个MenuProvider, 自定义Provider来重载ContentProvider的六个方法

1public abstract boolean onCreate();
着这个方法中进行一些初始化操作,比如初始化数据库,方便Provider从数据库拿到需要对外提供的数据

2public abstract @Nullable String getType(@NonNull Uri uri);
这个方法获取Uri类型参数,输出MIME类型字符串,它会将不同的Uri映射成为不通的MIMI字符串,这个方法后面还会细说

3public abstract @Nullable Uri insert(@NonNull Uri uri, @Nullable ContentValues values);
根据ContentResover中传递过来的Uri中包含的数据进行insert操作

4public abstract int delete(@NonNull Uri uri, @Nullable String selection, 
            @Nullable String[] selectionArgs);
删除操作

public abstract int update(@NonNull Uri uri, @Nullable ContentValues values,
            @Nullable String selection, @Nullable String[] selectionArgs);
修改操作

public abstract @Nullable Cursor query(@NonNull Uri uri, @Nullable String[] projection,
            @Nullable String selection, @Nullable String[] selectionArgs,
            @Nullable String sortOrder);
查询操作

这里可以看出需要重载crud对应的四个方法,然后有一个getType和onCreate方法。

针对getType定义了一下代码

// 构造一个matcher对象
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);

// 定义code用来找对对应各种MIME类型的编码

private static final int MULTIPLE_MENUS = 1;
private static final int SINGLE_MENUS = 2;

// 静态块,在类加载时执行一次,将uri和code绑定起来,以便解析
static {
    sURIMatcher.addURI(Menu.AUTHORITY, Menu.PATH_MULTIPLE, MULTIPLE_MENUS);
    sURIMatcher.addURI(Menu.AUTHORITY, Menu.PATH_SINGLE, SINGLE_MENUS);
}

UriMatcher对象可对Uri进行操作,这里的addURI操作可将Uri地址和这里定义的MULTIPLE_MENUS 、SINGLE_MENUS 行成一个一一对应的关系,方便在getType方法中根据Uri来返回MIME字符串类型。

@Nullable
@Override
public String getType(@NonNull Uri uri) {
    // 返回uri对应的MIME类型
    int match = sURIMatcher.match(uri);
    switch (match) {
        case MULTIPLE_MENUS:
            return Menu.MIME_TYPE_MULTIPLE;
        case SINGLE_MENUS:
            return Menu.MIME_TYPE_SINGLE;
        default:
            throw new IllegalArgumentException("Unknown uri:" + uri);
    }
}

第三步、配置AndroidManifest.xml

到第二步,就已经定义好了自定义Provider,同其它四大组件一样,使用Provider需要在AndroidManifest中进行对应的配置。

<provider
    android:name="com.yoryky.demo.provider.MenuProvider"
    android:authorities="myfs.pub.menusprovider"
    android:exported="true"
    android:readPermission="com.ql.provider.READ" >
    <!-- 指定授权者 -->
    <!-- 给予其他应用程序只读取自己数据的权限 -->
</provider>

这里的的exported表示其它应用程序可以访问该Provider,而readPermission表示其它应用数据只有读数据的权限,并且还需要在自己的AndroidManifest.xml文件中配置

<uses-permission android:name="com.ql.provider.READ" />

才能够读该Provider提供的数据。

第四步、ContentResolver调用数据

我们使用Provider的目的是通过Uri来向外部提供一个统一的数据访问接口,那么我们便不能通过直接调用Provider中的crud方法来实现数据访问,不然我们各种定义Uri什么的就前功尽弃,并且也达不到屏蔽数据来源的目的,所以这里请出来了ContentResolver来使用不同的Provider访问以及操作数据,这样不同的Provider不管是其中数据来自数据库还是服务器什么的,这里的ContentResolver都不需要管,ContentResolver只需要通过在crud方法中传入作为唯一标识符Uri,然后代码就会执行到对应的Provider中去,并调用对应的方法返回结果。这里举一个通过ContentResolver来执行insert的操作吧。

private ContentResolver = getContentResolver();
private void addData(String name, int price) {
    Menu[] mms = { new Menu(name, price)};
    for (Menu m : mms) {
        ContentValues cv = new ContentValues();
        cv.put(Menu.KEY_NAME, m.getName());
        cv.put(Menu.KEY_PRICE, m.getPrice());
        Uri uri = cr.insert(Menu.CONTENT_URI, cv); // 最终
        // cp.insert(url,values)
        Log.d(TAG, "add data:" + uri.toString());
    }
}

这里的ContentResolver个人感觉是全局变量,而且是个全局的单例,这个没有验证,如果这句话有问题,请指教。

到这里ContentProvider的使用就算是告一段落了。

2、URI/MIME以及与getType方法之间的关系

可能之前没怎么了解ContentProvider的同学看到这里已经一头雾水了,前面讲的Uri是啥、MIME又是啥,这里就先来说说这两个概念吧。

Uri

URI是一个用来标示唯一路径的类,前面我们说了ContentResolver通过这个URI去寻找对应Provider执行对应方法并返回结果,要是先这个目的,就得确保URI标示的路径必须是唯一的,不然JVM怎么能知道你要去执行哪个Provider、哪个方法呢。这里我们来看看URI作为唯一标识符的路径格式。

这里写图片描述

这里我们知道了其格式为scheme://authority/path这样表示的,scheme为固定的content(也不知道有没有别的scheme),而authority和path为自定义路径,自定义不代表随便定义,一定要确保定义的URI在实际使用中的唯一性,所以鉴于android中不同apk的包名不能相同所以authority一般定义为包名,而path则定义对应应用中的数据类名(当然可以带上/id这个索引,就像图片中的/2来表示单条数据)。

Menu类中关于Uri的定义有

// 用来定义uri常量 content://<authority>/<data path>/..../id
public static final String AUTHORITY = "myfs.pub.menusprovider";// 授权者 表示用哪个provider,要跟清单文件中一致
public static final String PATH_SINGLE = "menus/#"; // (menus:数据库表名)路径下单条记录 #代表数字id
public static final String PATH_MULTIPLE = "menus";// 路径下多条记录 数据库将对应表名
public static final String PATH_MULTIPLE_NAME = "menus/*"; // *代表文本 例如按名称访问等

// 组合成所需要的uri字符串
public static final String CONTENT_URI_STRING = "content://" + AUTHORITY + "/" + PATH_MULTIPLE;

// 将uri字符串转换为uri对象
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);

这里我们看到最有生成的CONTENT_URI就是我们上面讲的那种格式,根据这个CONTENT_URI,ContentResover就能够找到对应的MenuProvider中去。

MIME

那么这个MIME是个啥,根据维基百科上的解释,MIME是多用途互联网邮件扩展(MIME,Multipurpose Internet Mail Extensions)是一个互联网标准,这句话说了等于没说是吧,实际上知道HTTP协议的童鞋应该知道MIME是用来表示文档类型是txt、js、html、css、png、pdf等的,也就是说MIME就是用来表示文档类型的。关于MIME想了解更多的童鞋可以去看MIME 参考手册

这里我们再来看看Menu类中定义的MIME相关的变量吧

public static final String MIME_DIR_PREFIX = "vnd.android.cursor.dir";// MIME类型 多条
public static final String MIME_ITEM_PREFIX = "vnd.android.cursor.item";// 单条

public static final String MIME_ITEM = "vnd.pub.menus";// 自定义MIME类型字符串

// 将固定前缀+自定义字符串生成两个类型
public static final String MIME_TYPE_SINGLE = MIME_ITEM_PREFIX + "/" + MIME_ITEM;
public static final String MIME_TYPE_MULTIPLE = MIME_DIR_PREFIX + "/" + MIME_ITEM;

一个MIME Type由媒体类型(type)与子类型(subtype)组成,它们之间使用反斜杠/分割,形式如下:

type/subtype

常见的如text/html,而我们这里的vnd.android.cursor.dir以及vnd.android.cursor.item表示android中的自定义MIME,其含义分别为

vnd.android.cursor.dir 代表返回结果为多列数据

vnd.android.cursor.item  代表返回结果为单列数据

这里的subtype就随意了,我们这里使用vnd.pub.menus来作为subtype。

Provider的getType方法

上面讲了Uri以及MIME,前面也提到了getType方法是讲输入Uri输出对应的MIME类型

@Nullable
@Override
public String getType(@NonNull Uri uri) {
    // 返回uri对应的MIME类型
    int match = sURIMatcher.match(uri);
    switch (match) {
        case MULTIPLE_MENUS:
            return Menu.MIME_TYPE_MULTIPLE;
        case SINGLE_MENUS:
            return Menu.MIME_TYPE_SINGLE;
        default:
            throw new IllegalArgumentException("Unknown uri:" + uri);
    }
}

为什么要说这个getType,因为不说这个方法,我们压根不知道为什么需要在Menu类中定义MIME,实际上我倒现在都没搞明白这个getType的使用原理,因为我测试用ContentResolver各种调用Provider这个getType都没有触发。

这里就直接贴出来其他童鞋得出来的结论吧

作用1:提高性能

getType的作用应该是这样的,以指定的两种方式开头,android可以顺利识别出这是单条数据还是多条数据,比如我们的查询结果是一个Cursor,我们可以根据getType方法中传进来的Uri判断出query方法要返回的Cursor中只有一条数据还是有多条数据,这个有什么用呢?如果我们在getType方法中返回一个null或者是返回一个自定义的android不能识别的MIME类型,那么当我们在query方法中返回Cursor的时候,系统要对Cursor进行分析,进而得出结论,知道该Cursor只有一条数据还是有多条数据,但是如果我们按照Google的建议,手动的返回了相应的MIME,那么系统就不会自己去分析了,这样可以提高一丢点的系统性能。
作用2:了解ContentProvider返回类型(单条还是多条数据)
我们有可能不知道ContentProvider返回给我们的是什么,这个时候我们可以先调用ContentProvider的getType,根据getType的不同返回值做相应的处理。

好吧,看到这两条作用,我还是勉强接受getType是有用的吧(虽然心里还是不服,毕竟我的getType断点没有捕获啊,哈哈哈)。

3、Uri 操作类介绍

Android SDK提供了两个工具类来对Uri进行操作,分别是UriMatcher以及ContentUris,这里也来简单的介绍下吧。

UriMatcher

UriMatcher工具类主要是用来匹配Uri,它的使用比较简单,第一步就是把需要使用到Uri路径全给添加上,将Uri和自定义的SINGLE_MENUS以及MULTIPLE_MENUS绑定在一起。

// 静态块,在类加载时执行一次,将uri和code绑定起来,以便解析
static {
    sURIMatcher.addURI(Menu.AUTHORITY, Menu.PATH_MULTIPLE, MULTIPLE_MENUS);
    sURIMatcher.addURI(Menu.AUTHORITY, Menu.PATH_SINGLE, SINGLE_MENUS);
}

然后通过调用math方法就可以获取uri对应的SINGLE_MENUS或者MULTIPLE_MENUS,来执行不同的操作

@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
    int count = 0;
    switch (sURIMatcher.match(uri)) {
        case SINGLE_MENUS:
            String id = uri.getPathSegments().get(1);// 从uri取出id字符串
            count = sqdb.update(Menu.TABLENAME, values, Menu.KEY_ID + "=" + id, selectionArgs);
            getContext().getContentResolver().notifyChange(uri,null);
            break;
        case MULTIPLE_MENUS:
            count = sqdb.update(Menu.TABLENAME, values, selection, selectionArgs);
            getContext().getContentResolver().notifyChange(uri,null);
            break;
        default:
            throw new IllegalArgumentException("Unknown uri:" + uri);
    }
    return count;
}

ContentUris

ContentUris类用于操作Uri路径后面的id,它有两个比较实用的方法:

withAppendedId(uri, id)用于为路径加上id

//生成后的Uri为:content://myfs.pub.menusprovider/menus/1  
Uri uri = Uri.parse("content://myfs.pub.menusprovider/menus")  
Uri insertedUserUri = ContentUris.withAppendedId(uri, 1);  

parseId(uri)方法用于从路径中获取id

//获取的结果为2
Uri insertedUserUri = Uri.parse("content://myfs.pub.menusprovider/menus/2")  
long userId = ContentUris.parseId(insertedUserUri);

4、ContentObserver和ContentResolver的结合使用

看到ContentObserver这个类名应该能够反应过来这是一个观察者吧,那么这里的ContentResolver就只能很不好意思的作为被观察者了,所以这两个类可以组成一个观察订阅模式。

先给出一个ContentObserver的自定义类的实现吧

class MyContentObserver extends ContentObserver {

    public MyContentObserver(Handler handler) {
        super(handler);
    }

    @Override
    public void onChange(boolean selfChange) {
        super.onChange(selfChange);
        /Log.e(TAG, "remote observer find content provider data change!!!");
        queryAll();
    }
}

然后我们来看看这个观察订阅模式如何构建

首先,实例化ContentObserver以及ContentResolver这两个类

observer=new MyContentObserver(new Handler());//实例化ContentObserver对象
cr = getContentResolver(); //实例化ContentResolver 对象

然后,订阅或者说注册

cr.registerContentObserver(Menu.CONTENT_URI, true, observer);//注册内容提供器观察者对象

最后,在Activity onDestroy生命周期中记得取消注册

cr.unregisterContentObserver(observer);//取消监听

好运行代码,添加数据,怎么ContentObserver的onChange方法没反应呢,原来还需要手动触发监听,在MenuProvider的增、删、改操作中添加如下代码

getContext().getContentResolver().notifyChange(uri,null);

即可触发ContentObserver中的onChange方法(这里也加深了我认为getContentResolver获取到的是一个全局变量的想法)。

到这里这边文章的主要内容就结束了,不得不说,关于ContentProvider不是我学习中觉得最难的知识,但一定是最难写博客的知识,不知道有多少童鞋能够坚持看到这里。

总得来说个人觉得看完这篇文章希望你能搞清楚一下几点

1、Provider主要意义在于通过Uri屏蔽数据获取细节,向其它App开放数据

2、ContentProvider、ContentResolver以及ContentObserver这几个类之间的关系

3、UriMatcher以及ContentUris这两个工具类的作用

4、为了实现getType我们得去自定义单列和多列的MIME;

搞清楚了这几个问题,也就算这篇文章没有白写吧,还望各位多交流。

5、参考文献

1、ContentProvider数据库共享之——读写权限与数据监听

2、Android基础学习之Provider(内容提供器)

3、android之ContentProvider和Uri详解

4、 对ContentProvider中getType方法的一点理解

5、MIME 参考手册

6、对android中MIME类型的理解

7、认识安卓中的MIME Type

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值