文章目录
- 前言
- 一、ContentProvider是什么?
- 二、使用示例
- 1.为应用创建内容提供者
- 2.使用内容提供者
- 2.1 内容URI
- 2.2 Uri参数解析
- 2.2 使用内容URI操作数据
- 3.ContentProvider妙用
- 4 内容URI对应的MIME类型
- 5.ContentProvider重点注意
- 6 演示demo源码
- 总结
前言
随着现在的应用越做越大,出现了多进程的架构,因为Android的应用能申请的内存是有限制的,所以很多APP为了能够最大程度的保证自己的应用能够逃脱系统的围杀,设计出多进程架构,让App可以多使用几份内存。但是由于进程间内存是不共享的。所以需要做进程间的通信(IPC)。而Android进程间的通信有很多种。比如socket,基于Binder的AIDL以及ContentProvider等,而本章内容要介绍的就是内容提供者(ContentProvider)
一、ContentProvider是什么?
内容提供者ContentProvider是Android的四大组件之一,是Android进程间通信的一种实现方式,底层也是基于Binder实现的。它主要用于在不同的应用程序之间实现数据共享的功能,而且它提供了一套完整的机制。允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。不同于文件存储和sharedPreferences存储中两种全局可读可写操作模式,内容提供者可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会泄漏。目前,内容提供者已经成为官方推荐的进程间共享数据的标准方式。
二、使用示例
1.为应用创建内容提供者
创建一个类继承自Android官方提供的ContentProvider类,官方提供的ContentProvider类共有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写。我们以经典例子雇员和雇主的例子介绍内容提供者的使用,假设我们要共享雇员的信息给另外一个程序。代码如下所示:
public class EmployeeProvider extends ContentProvider {
@Override
public boolean onCreate() {
//初始化ContentProvider的时候调用通常会在这里完成对数
//据库的创建和升级操作,返回true表示内容提供者初始
//化成功,false表示失败。注:只有当存在ContentProvider
//尝试访问我们程序中的数据时,ContentProvider才会被初始化
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[]
projection, @Nullable String selection, @Nullable String[]
selectionArgs, @Nullable String sortOrder) {
//query 方法用于从ContentProvider中查询数据。使用Uri参数
//确定查询的表,projection参数确定查询的列,selection和
//selectionArgs用于约束查询哪些行,sortOrder用于对查询结果
//排序,查询的结果存放在Cursor对象中返回
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
//根据传入的内容URI来返回相应的MIME类型
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
//向ContentProvider中添加一条数据,使用Uri参数来确定要添加的表,待添加的数据保存
//在values参数中,添加完成后,返回一个用于表示这条新纪录的Uri
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection,
@Nullable String[] selectionArgs) {
//从ContentProvider中删除数据,使用uri参数来确定删除哪一张表中的数据,selection
//和selectionArgs参数用于约束删除哪些行,被删除的行数作为返回值返回
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values,
@Nullable String selection, @Nullable String[] selectionArgs) {
//更新ContentProvider中已有的数据,使用Uri参数确定更新哪一张表中的数据新数据
//保存在values参数中,selection和selectionArgs参数用于约束更新哪些行,受影响
//的行数会作为返回值返回
return 0;
}
}
上面的六个抽象方法中相信聪明的你一眼就能看出,这是一些增删改查的方法。但是我们几乎可以在每个方法中看到Uri这个参数,我们在使用内容提供者的时候会传入这个参数,接下来先让我们看下如何使用内容提供者。
2.使用内容提供者
我们使用ContentProvider一般都是用来查询数据的,下面就让我们重点看下内容提供者的query方法
Cursor cursor = query(@RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder,
@Nullable CancellationSignal cancellationSignal);
参数解释:
uri:指定查询某个程序下的某一张表
projection:指定查询的列名
selection:指定查询条件,相当于Sql语句中where后面的条件
selectionArgs:给selection中的占位符提供具体的值
sortOrder:指定查询的结果排序方式
cancellationSignal:取消正在进行操作的信号量
访问内容提供者共享的数据需要借助于Android提供的ContentProvider类,我们可以通过Context中的getContentResolver()方法获取该类的实例。ContentResolver中提供了一系列的方法用于对数据进行CRUD(增、删、改、查)操作。类似于Sqlite中对数据的操作。但又不是完全相同。ContentResolver中的增删改查方法是不接受表名参数的,而是使用一个Uri参数代替。这个参数被称为内容URI
2.1 内容URI
内容提供者URI给内容提供者中的数据建立了唯一标识符,它主要由两部分组成:authority和path。
authority 是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用包名的方式来进行命名,比如应用的包名为:com.example.app,那对应的authority可以命名为:com.example.app.provider
path 则是用于对同一应用程序中不同的表做区分的,通常会添加到authority的后面。比如某个成T恤的数据库里面存在两张表:table1和table2,这时可以将path分别命名为/table1 和 /table2;然后把authority和path组合在一起就成了下面两行字符串:
com.example.app.provider/table1
com.example.app.provider/table2
不过目前这两个字符串还是无法辨认它就是内容URI,我们需要在字符串的头部加上协议声明:所以内容URI的标准形式就出来了:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
得到内容URI后我们还需要将它解析程Uri对象才可以作为参数传入。解析的方法也很简单。如下所示
Uri uri = new Uri.parse("content://com.example.app.provider/table1");
只需要调用Uri的静态方法parse()就可以把内容URI字符串解析程URI对象,现在就可以通过这个URI去查询table1中的数据了。
2.2 Uri参数解析
经过上一节内容我们知道,一个标准的内容URI写法是:
content://com.example.app.provider/table1
这就表示调用方期望访问的是com.example.app这个应用的table1表中的数据。其实,我们还可以在内容URI的后面加上一个id,如下所示:
content://com.example.app.provider/table1/1
这就表示调用方期望访问的是com.example.app这个应用的table1表中的id为1的数据
所以,内容URI的格式主要有以上两种,以路径结尾就表示期望访问该表中的所有数据,以id结尾就表示期望访问该表中拥有相应id的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容URI,规则如下:
‘ * ’:表示匹配任意长度的任意字符
‘ # ’:表示匹配任意长度的任意数字
所以一个能匹配任意表的内容URI格式就可以写成
content://com.example.app.provider/table1/*
一个可以匹配任意一行数字内容的URI格式就可以写成
content://com.example.app.provider/table1/#
然后我们就可以借助UriMatcher这个类实现匹配内容URI的功能
2.2 使用内容URI操作数据
1.查询数据
public void query(View view) {
String [] PROJECTION = new String[]{
Employee._ID,
Employee.NAME,
Employee.GENDER,
Employee.AGE
};
Cursor cursor = getContentResolver().query(
Employee.CONTENT_URI,PROJECTION,null,null,Employee.DEFAULT_SORT_ORDER);
StringBuilder sb = new StringBuilder();
if(cursor !=null && cursor.moveToFirst()){
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToPosition(i);
int id = cursor.getInt(0);
String name = cursor.getString(1);
String gender = cursor.getString(2);
int age = cursor.getInt(3);
sb.append("id").append(id).append("name: ")
.append(name).append(" ,gender: ")
.append(gender).append(" ,age: ")
.append(age).append("\n");
}
}
if(cursor != null && !cursor.isClosed()){
cursor.close();
}
//显示数据
setText("");
setText(sb.toString());
}
2.插入数据
public void insert(View view) {
Uri uri = Employee.CONTENT_URI;
ContentValues values = new ContentValues();
values.put(Employee.NAME,"walt");
values.put(Employee.GENDER,"male");
values.put(Employee.AGE,28);
getContentResolver().insert(uri,values);
Toast.makeText(this, "插入成功", Toast.LENGTH_SHORT).show();
}
3.更新数据
public void update(View view) {
//更新ID为1的记录
Uri uri = ContentUris.withAppendedId(Employee.CONTENT_URI,1);
ContentValues values = new ContentValues();
values.put(Employee.NAME,"walt-zhong");
values.put(Employee.GENDER,"male");
values.put(Employee.AGE,18);
int update = getContentResolver().update(uri, values, null, null);
}
4.删除数据
public void delete(View view) {
//删除ID为1的记录
Uri uri = ContentUris.withAppendedId(Employee.CONTENT_URI,2);
int delete = getContentResolver().delete(uri, null, null);
setText("删除成功 result: " + delete);
}
3.ContentProvider妙用
ContentProvider还有一个妙用,那就是可以跨进程调用方法。读者可能会很迷惑对于这个描述。我来假设一个场景,比如我们需要从A应用传递一个坐标给到B应用去做一些操作,比如在VR行业中有种手机模式场景,就是通过在VR眼镜中虚拟出一个手机桌面,让用户在3d的场景中也可以使用2d的手机应用。也称为2D应用3D化。这时候用户可能是通过游戏手柄去操作的,游戏手柄通过蓝牙和VR眼镜相连,当游戏手柄点击到一个专门检测用户点击的A进程后,A进程会把用户点击的屏幕坐标共享给VR眼镜,这时候就可以使用内容提供者的跨进程调用方法,并把坐标当成参数传入就可以了,在使用端解析参数就可以进行相关的操作了。
具体的做法是:
1.通过调用方法的方式,将参数携带的值传到ContentProvider的使用端
public void callMethod(View view) {
Bundle reqBundle = new Bundle();
reqBundle.putInt("actionCode", 1129);
reqBundle.putString("extraData","额外数据");
Bundle func = getContentResolver().call(Employee.CONTENT_URI, "func",
getPackageName(), reqBundle);
// Log.d("zhongxj: ","res= " + func.toString());
}
call(@NonNull Uri uri, @NonNull String method,
@Nullable String arg, @Nullable Bundle extras)
Uri:指定要操作的某个程序下的某一张表
method:方法名称,用于区分是谁调用了call方法
arg:参数,没有的话传null
extra:封装在Bundle的参数,没有的话传null
2.在使用方的ContentProvider中重写call()方法,接收参数,做后续处理
@Nullable
@Override
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
Log.d("zhongxj", "method: " + method);
if ("func".equals(method)) {
int actionCode = extras.getInt("actionCode");
String extraData = extras.getString("extraData");
StringBuilder sb = new StringBuilder();
sb.append("func arg: ").append(arg)
.append("actionCode: ").append(actionCode)
.append("extraData").append(extraData)
.append("methodName: ").append(method);
callFuncRes = sb.toString();
Log.d("zhongxj", "result: " + sb.toString());
}
return null;
}
4 内容URI对应的MIME类型
我们在重写ContentProvider的方法中有一个方法叫getType(),这个方法可以根据传入的URI返回对应的MIME类型,接下来我们一起来了解下什么是MIME类型以及如何定义内容URI对应的MIME类型
MIME(Multipurpose Internet Mail Extensions): 多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。它是一个互联网标准,扩展了电子邮件标准,使其能够支持:非ASCII字符文本;非文本格式附件(二进制、声音、图像等);由多部分(multiple parts)组成的消息体;包含非ASCII字符的头信息(Header information)。
一个内容URI所对应的MIME字符串主要由3部分组成,Android对这3个部分做了如下的格式规定:
(1)必须以vnd开头
(2)如果内容URI以路径结尾,则后接" android.cursor.dir/ " ;如果内容URI以ID结尾,
则后接 " android.cursor.item/ "
(3)最后接vnd..
例如:content://com.example.app.provider/table1 的对应MIME类型可以写成
vnd.android.cursor.dir/vnd.com.example.app.provider.table1
而content://com.example.app.provider/table1/1这个内容URI的MIME类型可以写成
vnd.android.cursor.item/vnd.com.example.app.provider.table1
5.ContentProvider重点注意
写完ContentProvider后需要在AndroidManifest.xml中注册,否则程序会闪退
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.walt.mycontentprovider">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyContentProvider"
tools:targetApi="31">
<activity>
.......
</activity>
<provider
android:authorities="com.walt.provider.Employee"
android:name=".EmployeeProvider"
android:exported="true"
android:enabled="true"/>
</application>
</manifest>
另外,还需注意的是在调用方使用ContentProvider时,在高版本的Android中需要在Androidmanifest.xml中加入标签,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.walt.appdemo2">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyContentProvider">
<activity>
...
</activity>
</application>
<!--高版本中必须加这个标签才能访问到对应应用的数据-->
<queries>
<provider android:authorities="com.walt.provider.Employee"/>
</queries>
</manifest>
6 演示demo源码
为了方便读者了解ContentProvider的使用,我做了一个简单的小demo,供读者参考,demo中有两个应用,分别代表A,B进程,一个创建了内容提供者,另一个使用内容提供者。需要注意的是,当使用方正在使用内容提供者的时候,提供数据的一方,即创建内容提供者的进程应该在后台,不能杀掉,否则,使用方无法获取到数据。
这里需要声明下,demo仅做演示使用,请勿用到项目中,读者需要了解透彻后自己再开发出更好的代码。demo未经过测试,请勿使用:
demo地址
总结
以上就是今天要讲的内容,本文介绍了ContentProvider的使用和一些基本概念。并提供了一个演示demo供读者熟悉ContentProvider,但是在项目中使用ContentProvider时,读者还需自己仔细理解每一个API的使用方法,防止项目出现严重的BUG,博客只能是某个知识入门了解使用的工具,不是绝对的权威,希望读者能够结合自己的思考去实现更酷更好玩的东西。内容提供者作为现在Android应用间共享数据的标准方式,也是官方推荐的方式,希望读者好好的去了解这部分知识,对项目的开发一定会非常的有用。