第五篇 Android视图手册之ContentProvider
作为Android四大组件之一的ContentProvider,虽然在平常使用的过程中亮相并不多,但这不影响其在Android系统中的地位。因为平时我们的应用数据都是内部自我消化和独立的,一旦涉及和其他应用进行数据交互时,ContentProvider的作用就体现出来了。
概述
ContentProvider(内容提供者),作用是为不同的应用之间数据共享,提供统一的接口,通过uri来标识其它应用要访问的数据,通过ContentResolver的增、删、改、查方法实现对共享数据的操作。还可以通过注册ContentObserver来监听数据是否发生了变化来对应的刷新页面。
其中ContentProvider只是承担了一个中间工作者的角色,内部的数据源的操作由提供的应用开发者自己实现,例如Sqlite,文件,XMl,网络等等。
统一资源标识符(URI)
在了解ContentProvider之前,我们先来简单认知一下什么是统一资源标识符(URI)。
URI本质上是一个字符串,这个字符串的作用是唯一地标记资源的位置或者名字。它不仅能够标记万维网的资源,也可以标记其他的,如邮件系统,本地文件系统等任意资源,而资源既可以是存在磁盘上的静态文本,页面数据也可以是由java,php提供的动态服务。
一个正常的URI标识如下图所示,往往由这四部分组成:
而在Android中 我们如果想用ContentProvider进行数据共享,就需要按下述规则进行:
- 协议名:要用content作为前缀(这是Android规定的)
- 主机名:要用对应ContentProvider在注册清单中的Authority值(在清单文件中配置)
- 路径名:这里需填入资源路径,在你创建ContentProvider时可以进行相应资源路径的加入
- 查询参数:这个参数可以不进行填入,因为查询参数ContentProvider已经提供方式给我们去调用了,当然你如果想进行填入的话也可以,自己拿到解析后的值进行对应逻辑处理就行。
示例: content://com.ftd.test.myprovider/test
使用步骤
大致步骤如下:
- 自定义
ContentProvider
类 - 在清单文件
AndroidManifest
中进行注册 - 进行数据访问 进程内 | 进程间
ContentProvider自定义
首先我们先需要继承ContentProvider,实现其中的方法,如下:
package com.ftd.test;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* 内容提供者
* tangxianfeng
* 2022.10.29
*/
public class MyProvider extends ContentProvider {
/**
* 进行创建
*/
@Override
public boolean onCreate() {
return false;
}
/**
* 获取数据类型
*/
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
/**
* 查询数据
*/
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
/**
* 插入数据
*/
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
/**
* 删除数据
*/
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
/**
* 更新数据
*/
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
各个方法的作用
- onCreate()
ContentProvider 创建的时候会进行调用的方法,一般可用于数据库等需要初始化的操作。 - getType(Uri)
返回MIME类型对应内容的URI,这里的getType一般用做类型的自定义返回,当我们某些情况传入的类型是需要校验的时候,可以通过重写该方法实现,一般可不做处理。 - query(Uri, String[], String, String[], String)
查询数据,返回一个数据Cursor对象。 - insert(Uri, ContentValues)
插入一条数据 - delete(Uri, String, String[])
根据条件删除数据 - update(Uri, ContentValues, String, String[])
根据条件更新数据
如下为我用游标进行简单模拟的一些操作,主要涉及查询和插入,实际情况中我们可以用sqlite等数据库进行替代。
package com.ftd.test;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* 内容提供者
* tangxianfeng
* 2022.10.29
*/
public class MyProvider extends ContentProvider {
private static final String AUTHORITY = "com.ftd.test.myprovider"; //用于区分不同的ContentProvider
//UriMatch主要为了区配URI,比如应用A提供了数据,但是并不是说有的应用都可以操作这些数据,只有提供了满足应用A的URI,才能访问A的数据,而UriMatch就是做这个区配工作的
private static final UriMatcher sUriMatcher;
//用以表明数据类型
public static final int USER_DIR = 0;
public static final int USER_ITEM = 1;
private MatrixCursor TestCursor;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//只对这三种uri进行处理 如果匹配上 通过 sUriMatcher.match(uri)可以得到 对应的Code
sUriMatcher.addURI(AUTHORITY, "test", USER_ITEM);
sUriMatcher.addURI(AUTHORITY, "test1", USER_ITEM);
sUriMatcher.addURI(AUTHORITY, "testmulti", USER_DIR);
}
/**
* 进行创建
*/
@Override
public boolean onCreate() {
//这里可以创建数据库,也可以创建其他类型的数据,如下为我用游标创建的一个表
String[] tableCursor = new String[] {
"time", "food", "where" };
TestCursor = new MatrixCursor(tableCursor);
TestCursor.addRow(new Object[] {
"2022", "烤鸭", "北京" });
TestCursor.addRow(new Object[] {
"2021", "肉夹馍", "陕西" });
TestCursor.addRow(new Object[] {
"2020", "大饼", "山东" });
return true;
}
/**
* 获取数据类型
*/
@Nullable
@Override
public String getType(@NonNull Uri uri) {
if(sUriMatcher.match(uri)==USER_DIR) {
// 查询多条数据
return "vnd.android.cursor.dir/testmulti";
}else{
// 查询一条数据
return "vnd.android.cursor.item/testsingle";
}
}
/**
* 查询数据
* uri 代表资源位置的uri
* projection 代表返回内容(conlumn名)
* selection 设置条件
* selectionArgs是selection的内容
* sortOrder是根据column排序
*/
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
switch (sUriMatcher.match(uri)) {
case USER_DIR:
return TestCursor;
case USER_ITEM:
//条件查询等情况处理
break;
}
return null;
}
/**
* 插入数据
*/
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
if (values==null){
return null;
}
switch (sUriMatcher.match(uri)) {
case USER_DIR:
break;
case USER_ITEM:
String time = (String) values.get("time");
String food = (String) values.get("food");
String where = (String) values.get("where");
TestCursor.addRow(new Object[] {
time, food, where });
getContext().getContentResolver().notifyChange(uri, null);//通知指定URI数据已改变
break;
default:
}
return null;
}
/**
* 删除数据
* selection 设置条件
* selectionArgs是selection的内容
*/
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;//此处就不赘述了,拿到条件后根据条件对数据进行删除即可
}
/**
* 更新数据
* selection 设置条件
* selectionArgs是selection的内容
*/
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;//此处就不赘述了,拿到条件后根据条件对数据进行更新即可
}
}
在清单文件中进行注册
<provider
android:authorities="com.ftd.test.myprovider"
android:name="com.ftd.test.MyProvider"
android:exported="true"
android:grantUriPermissions="true"
android:permission="ftd.Provider"
android:readPermission="ftd.Provider"
android:writePermission="ftd.Provider"
android:enabled="true"
android:multiprocess="true"/>
前文提到的android:authorities 就是在这里设置的,用作唯一标识。
ContentProvider而言,有很多权限控制,可以AndroidManifest.xml文件中对节点的属性进行配置,一般使用如下一些属性设置:
- android:grantUriPermssions:临时许可标志。
- android:permission:Provider读写权限。
- android:readPermission:Provider的读权限。
- android:writePermission:Provider的写权限。
- android:enabled:标记允许系统启动Provider。
- android:exported:标记允许其他应用程序使用这个Provider。
- android:multiProcess:标记允许系统启动Provider相同的进程中调用客户端。
数据交互操作
在进行数据交互操作时,我们可以通过ContentResolver统一对数据进行管理和操做:
- 查询
// 获取ContentResolver
ContentResolver resolver = getContentResolver();
// 通过ContentResolver 向ContentProvider中查询数据
Cursor cursor = resolver.query( Uri.parse("content://com.ftd.test.myprovider/testmulti"), null, null, null, null);
while (cursor.moveToNext()){
System.out.println("query data:" + cursor.getString(0) +" "+ cursor.getString(1)+" "+ cursor.getString(2));
// 将表中数据全部输出
}
cursor.close();
// 关闭游标
- 插入
// 获取ContentResolver
ContentResolver resolver = getContentResolver();
// 插入表中数据
ContentValues values = new ContentValues();
values.put("time", "2002");
values.put("food", "瘦肉丸");
values.put("where", "温州");
// 通过ContentResolver 根据URI 向ContentProvider中插入数据
resolver.insert(Uri.parse("content://com.ftd.test.myprovider/test"),values);
- 删除
// 获取ContentResolver
ContentResolver resolver = getContentResolver();
// 删除表中数据
resolver.delete(Uri.parse("content://com.ftd.test.myprovider/test"),"time=",new String[]{
"2022"});
- 更新
// 获取ContentResolver
ContentResolver resolver = getContentResolver();
// 更新表中数据
ContentValues values = new ContentValues();
values.put("time", "2002");
values.put("food", "瘦肉丸");
values.put("where", "温州");
resolver.update(Uri.parse("content://com.ftd.test.myprovider/test"),values,,"time=",new String[]{
"2022"});
到此一个ContentProvider的大致使用操作就结束了。
小知识
1、MIME类型
MIME:全称Multipurpose Internet Mail Extensions,多功能Internet邮件扩充服务。它是一种多用途网际邮件扩充协议,在1992年最早应用于电子邮件系统,但后来也应用到浏览器。MIME类型就是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。
简单来说,MIME类型就是用来标识当前的Activity所能打开的文件类型。
举个栗子:
<activity
android:name=".TESTActivity"
android:label="@string/test">
<intent-filter>
<action android:name="com.ftd.test"/>
<categoryandroid:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/bmp"/>
</intent-filter>
</activity>
这里指定了data域的MimeType值是"image/bmp",即在利用隐式Intent匹配时,只有指定MimeType是"image/bmp"时,才能启用这个Activity,也就是说,这个Activity只能打开image/bmp类型的文件。
典型的调用方式如下:
Intent intent =new Intent();
intent.setAction(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
Intent.ACTION_DIAL这个activity会通过tel:10086获取自己用的通讯录的contentprovider的type. 如果与intentfilter中指定的mimeType一致, 那么证明传过来的这个参数uri是可用的, 那么就打开拨号这个界面。
而在getType中,我们可以看到getType这个函数会根据传进来的URI,相应生成一个代表MimeType的字符串;而此字符串的生成也有规则:
- 如果是单条记录应该返回以vnd.android.cursor.item/ 为首的字符串
- 如果是多条记录,应该返回vnd.android.cursor.dir/ 为首的字符串
MIME前面的一部分我们按照Google的要求来写,后面一部分就可以根据我们自己的实际需要来写。
2、ontentProvider的底层是采用 Android中的Binder机制来实现的,如果想具体研究该原理,可先去了解Binder机制
可能遇到的相关问题
1、提供 ContentProvider的应用未唤醒能拿到数据吗
不能,Android中A应用向B应用去通过ContentProvider去获取数据时,是需要先唤醒B应用,在通过B的ContentProvider去获取资源数据。如果B的进程没有被唤醒,就拿不到数据了,此时后台会发现Log:ActivityThread: Failed to find providerinfo for xxx.
2、ContentProvider的getType的作用?
- 隐式调用activity传入data数据. 且这个data数据, 是某个ContentProvider的uri参数
- 为了防止data是activity无法处理, 所以activity才需要设置mime进行data校验
- 为了acvity在校验自定义ContentProvider时能有结果, 所以才要实现ContentProvider的getType.
- 如果当该类型需要涉及存储,读取等权限时,也可以在跳转activity前先进行判断申请。
3、为什么要使用通过ContentResolver类从而与ContentProvider类进行交互,而不直接访问ContentProvider类?
一般来说,一款应用要使用多个ContentProvider,若需要了解每个ContentProvider的不同实现从而再完成数据交互,操作成本高 & 难度大
所以再ContentProvider类上加多了一个 ContentResolver类对所有的ContentProvider进行统一管理。
4、ContentProvider的优点
1)安全
ContentProvider为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给 其他应用 进行 增、删、改、查,而不用担心因为直接开放数据库权限而带来的安全问题
2)访问简单 & 高效
对比于其他对外共享数据的方式,数据访问方式会因数据存储的方式而不同:
采用 文件方式 对外共享数据,需要进行文件操作读写数据;
采用 Sharedpreferences 共享数据,需要使用sharedpreferences API读写数据
这使得访问数据变得复杂 & 难度大。
而采用ContentProvider方式,其 解耦了 底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问简单 & 高效