简介
用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序的数据,同时保证被访数据的安全性,使用ContentProvider是Android实现跨程序共享数据的标准方式。不同于文件存储和SharedPreferences,ContentProvider可以选择只对哪一部分数据进行共享。
为什么需要ContentProvider?
这一点是我们学习当中非常重要的一点,知道目的,我们才知道前进的方向
我们知道,一个软件系统的架构通常是这样的:
为了降低上层业务对底层数据的依赖,需要增加一个数据访问层来解耦,我们今天要说的ContentProvider充当的就是数据访问层的角色。ContentProvider提供了一些通用的接口来实现对底层数据(其实是数据库中的表结构数据)进行操作。
进程间 进行数据交互 & 共享,即跨进程通信
如上图我们知道通过ContentProvider我们可以进行下列两项操作
- 进程内的通信
- 进程间共享数据区
原理
ContentProvider
的底层是采用 Android
中的Binder
机制 不懂请 https://blog.csdn.net/weixin_39460667/article/details/82916441
Content Provider组件在不同应用程序之间传输数据是基于匿名共享内存机制来实现的(在应用程序进程之间以匿名共享内存的方式来传输数据效率是非常高的,因为它们之间只需要传递一个文件描述符就可以了):
首先在第三方应用程序这一侧,当它需要访问Content Provider中的数据时,它会在本进程中创建一个CursorWindow对象,它在内部创建了一块匿名共享内存,同时,它实现了Parcel接口,因此它可以在进程间传输。接下来第三方应用程序把这个CursorWindow对象(连同它内部的匿名共享内存文件描述符)通过Binder进程间调用传输到Content Provider这一侧。这个匿名共享内存文件描述符传输到Binder驱动程序的时候,Binder驱动程序就会在目标进程(即Content Provider所在的进程)中创建另一个匿名共享文件描述符,指向前面已经创建好的匿名共享内存,因此,就实现了在两个进程中共享同一块匿名内存。
在Content Provider这一侧,利用在Binder驱动程序为它创建好的这个匿名共享内存文件描述符,然后在进行去读取数据信息,待我们要返回的时候,会采用一个binder对象,把数据传回给已经创建的匿名共享内存区。
好啦,原理啥的大概的讲了一遍,开始来讲一下他的具体使用方法
ContentProvider用法
-
使用现有的ContentProvider来读取和操作相应程序中的数据。
-
创建自己的ContentProvider给我们的程序的数据提供外部访问接口。
接下来在介绍 ContentProvider 之前先对另一个类 ContentResolver 进行介绍一下
ContentResolver
为什么我要先介绍一下这个类呢
原因:ContentProvider
类并不会直接与外部进程交互,而是通过ContentResolver
类
可能你们会问我说为什么要使用通过ContentResolver类从而与ContentProvider类·进行交互,而不直接访问ContentProvider
类?
原因两点:
一般来说,一款应用要使用多个ContentProvider
,若需要了解每个ContentProvider
的不同实现从而再完成数据交互,操作成本高 & 难度大
所以再ContentProvider
类上加多了一个 ContentResolver
类对所有的ContentProvider
进行统一管理。
你也可以这么理解:
因为ContentProvider作为一个数据源处理中心,不止给App内部使用,还会被其他App访问。所以这里的ContentProvider就好比一个server,其他的App在访问这个ContentProvider的时候都必须先获得一个Server的Client才行,这里的ContentResolver就是Android提供给开发者使用来得到一个ContentProvider Client的管理工具类。
现在就来使用现有的ContentProvider来读取和操作相应程序中的数据。用的4个方法
// 外部进程向 ContentProvider 中添加数据
public Uri insert(Uri uri, ContentValues values)
// 外部进程 删除 ContentProvider 中的数据
public int delete(Uri uri, String selection, String[] selectionArgs)
// 外部进程更新 ContentProvider 中的数据
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
// 外部应用 获取 ContentProvider 中的数据
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
看到这里也许你会这样的吐槽我说,哇靠,博主你这个是什么态度,直接贴·过来的几个方法,就这样就收尾了,这也太不可靠了把,哈哈哈,想到这里,大家先别着急,看我下方对各个参数的详细解说。
url,这个在ContentProvider最为重要,所以说得相对的详细一点
第一个最主要的参数呢,就是url ,这个url跟我们平时的url有点不太一样,这里让我给大家介绍一下
名称:统一资源标识符(URI)Uniform Resource Identifier
作用:唯一标识 ContentProvider
& 其中的数据 ,功能跟我们平时rul差不多,只是对象发生了变化。
外界进程通过
URI
找到对应的ContentProvider
& 其中的数据,再进行数据操作
自定义 URL= content://com.example.app.provider/user/1
content: 意称为主题(Schema):ContentProvider的URL前缀(Android规定)
com.example.app.provider : 意称为授权信息(Authority):用于对不同的应用程序做区分的,一般会采用程序包名进行命名。比如某个程序的包名是com.example.app,那么该程序的authority可以命名为:com.example.app.provider。
User: 意称为表名(Path):则是用于对同一应用程序中不同的表做区分的,通常添加到authority后面,比如某个程序的数据库有2张表,table1和table2,path分别命名为/table1和/table2,然后把authority和path进行组合,内容URI就变成了com.example.app.provider/table1和com.example.app.provider/table2。
1: 意称为记录(ID)表中的某个记录(若无指定,则返回全部的记录)
注意
在得到了内容URI字符串后,需要将他解析成Uri对象才可以作为参数传入。
Uri uri=Uri.parse("content://com.example.app.provider/table1");
我们用具体的例子来对他进行介绍。
查询
现在就可以使用这个Uri对象来查询table1表中的数据了。
Cursor cursor=getContentResolver().query(
uri,
projection,
selection,
selectionArgs,
sortOrder);
这些参数和SQLiteDatabase中query()方法里的参数很像。
query()参数 | 对应SQL部分 | 描述 |
uri | from table_name | 指定查询某个应用程序下的某一张表 |
projection | select column1,column2 | 指定查询的列名 |
selection | where column=value | 指定where的约束条件 |
selectionArgs | - | 为where中的占位符提供具体的值 |
sortOrder | order by column1,column2 | 指定查询结果的排序方式 |
查询完成后返回的是一个Cursor对象。读取的思路是通过移动游标的位置来遍历Cursor的所有行,然后取出相应列的数据
if(cursor!=null){
while(cursor.moveToNext()){
String column1=cursor.getString(cursor.getColumnIndex("column1"));
int column2=cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}
添加
Uri insert(Uri uri, ContentValues values)
向表中插入一条数据 column1 列的值为 text column2的值为 1;
ContentValues values=new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);
更新
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
把column1的值清空
ContentValues values=new ContentValues();
values.put("column1"," ");
getContentResolver().update(uri,values,"column1=? and column2=?",new String[]{"text",1});
删除
delete(Uri uri, String selection, String[] selectionArgs)
删除 column2 等于 1 的那一列数据。
getContentResolver().delete(uri,"column2=?",new String[]{"1"});
NEXT 介绍 Android
提供了 3 个用于辅助 ContentProvide
的工具类
辅助类
ContentUris
UriMatcher
ContentObserver
ContentUris类
作用:操作 URI
具体使用
核心方法有两个:withAppendedId()
& parseId()
// withAppendedId()作用:向URI追加一个id
Uri uri = Uri.parse("content://cn.scu.myprovider/user")
Uri resultUri = ContentUris.withAppendedId(uri, 7);
// 最终生成后的Uri为:content://cn.scu.myprovider/user/7
// parseId()作用:从URL中获取ID
Uri uri = Uri.parse("content://cn.scu.myprovider/user/7")
long personid = ContentUris.parseId(uri);
//获取的结果为:7
UriMatcher类
作用
在ContentProvider
中注册URI
根据 URI
匹配 ContentProvider
中对应的数据表
具体使用
// 步骤1:初始化UriMatcher对象
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
//常量UriMatcher.NO_MATCH = 不匹配任何路径的返回码
// 即初始化时不匹配任何东西
// 步骤2:在ContentProvider 中注册URI(addURI())
int URI_CODE_a = 1;
int URI_CODE_b = 2;
matcher.addURI("cn.scu.myprovider", "user1", URI_CODE_a);
matcher.addURI("cn.scu.myprovider", "user2", URI_CODE_b);
// 若URI资源路径 = content://cn.scu.myprovider/user1 ,则返回注册码URI_CODE_a
// 若URI资源路径 = content://cn.scu.myprovider/user2 ,则返回注册码URI_CODE_b
// 步骤3:根据URI 匹配 URI_CODE,从而匹配ContentProvider中相应的资源(match())
@Override
public String getType(Uri uri) {
Uri uri = Uri.parse(" content://cn.scu.myprovider/user1");
switch(matcher.match(uri)){
// 根据URI匹配的返回码是URI_CODE_a
// 即matcher.match(uri) == URI_CODE_a
case URI_CODE_a:
return tableNameUser1;
// 如果根据URI匹配的返回码是URI_CODE_a,则返回ContentProvider中的名为tableNameUser1的表
case URI_CODE_b:
return tableNameUser2;
// 如果根据URI匹配的返回码是URI_CODE_b,则返回ContentProvider中的名为tableNameUser2的表
}
}
ContentObserver类
定义:内容观察者
作用:观察 Uri
引起 ContentProvider
中的数据变化 & 通知外界(即访问该数据访问者)
当
ContentProvider
中的数据发生变化(增、删 & 改)时,就会触发该ContentObserver
类
具体使用
// 步骤1:注册内容观察者ContentObserver
getContentResolver().registerContentObserver(uri);
// 通过ContentResolver类进行注册,并指定需要观察的URI
// 步骤2:当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
public class UserContentProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
db.insert("user", "userid", values);
getContext().getContentResolver().notifyChange(uri, null);
// 通知访问者
}
}
// 步骤3:解除观察者
getContentResolver().unregisterContentObserver(uri);
// 同样需要通过ContentResolver类进行解除
至此,关于`ContentProvider`的使用已经讲解完毕
下面我们来进行案例说明
进程内的数据共享
前言:ContentProvider 天生就是与 Sqlite 配合使用,因为他的参数跟sqlite的简直一模一样(我下方用的是Greendao来进行查询,大家莫怪)
1、创建数据库类
/**
* Created by jie on 2018/8/21.
*/
public class DBManager {
private Context mContext;
private DaoMaster mDaoMaster;
private DaoSession mDaoSession;
private UserDao userDao;
private JobDao jobDao;
private DBManager(Context context) {
mContext = context;
}
private static volatile DBManager instance = null;
public static DBManager getInstance(Context context) {
if (instance == null) {
synchronized (DBManager.class) {
if (instance == null) {
instance = new DBManager(context);
}
}
}
return instance;
}
public UserDao getUserDao() {
return userDao;
}
public JobDao getJobDao() {
return jobDao;
}
public void init() {
DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(mContext, "user.db");
mDaoMaster = new DaoMaster(helper.getWritableDb());
mDaoSession = mDaoMaster.newSession();
userDao = mDaoSession.getUserDao();
jobDao = mDaoSession.getJobDao();
}
}
2、自定义ContentProvider
/**
* Created by jie on 2018/10/2.
*/
public class mProvider extends ContentProvider {
private static final String TAG = "mProvider";
private Context mContext;
public static final String AUTOHORITY = "com.example.jie.foreverdemo.myprovider";
public static final int User_Code = 1;
public static final int Job_Code = 2;
private static final UriMatcher mMatcher;
private JobDao jobDao;
private UserDao userDao;
private DBManager instance;
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 初始化
mMatcher.addURI(AUTOHORITY, "User", User_Code);
mMatcher.addURI(AUTOHORITY, "Job", Job_Code);
// 若URI资源路径 = content://cn.scu.myprovider/user ,则返回注册码User_Code
// 若URI资源路径 = content://cn.scu.myprovider/job ,则返回注册码Job_Code
}
@Override
public boolean onCreate() {
mContext = getContext();
instance = DBManager.getInstance(mContext);
instance.init();
jobDao = instance.getJobDao();
userDao = instance.getUserDao();
Job job = new Job("深圳", 6);
User user = new User("jie", 12);
jobDao.insert(job);
userDao.insert(user);
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
List<User> list = userDao.queryBuilder().build().list();
Log.e("query: ",list.get(0).getName()+" "+list.get(0).getAge()+" "+list.get(0).getId());
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
String tableName = getTableName(uri);
switch (tableName){
case "user":
User user = new User();
user.setId((Long)contentValues.get("id"));
user.setName(String.valueOf(contentValues.get("name")));
user.setAge((Integer)contentValues.get("age"));
userDao.insert(user);
mContext.getContentResolver().notifyChange(uri, null);
break;
case "job":
Job job = new Job();
job.setDays((Integer) contentValues.get("days"));
job.setLocation(String.valueOf(contentValues.get("location")));
jobDao.insert(job);
mContext.getContentResolver().notifyChange(uri, null);
break;
default:
break;
}
return uri;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
return 0;
}
/**
* 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
*/
private String getTableName(Uri uri){
String tableName = null;
switch (mMatcher.match(uri)) {
case User_Code:
tableName = "user";
break;
case Job_Code:
tableName = "job";
break;
}
return tableName;
}
}
3、为这个ContentProvider在清单中进行注册
<provider
android:name=".ContentProvider.mProvider"
android:authorities="com.example.jie.foreverdemo.myprovider" />
4、使用ContentProvider获取数据
package com.example.jie.foreverdemo.ContentProvider;
import android.content.ContentResolver;
import android.content.DialogInterface;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import com.example.jie.foreverdemo.R;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
/**
* Created by jie on 2018/10/2.
*/
public class MActivity extends AppCompatActivity implements View.OnClickListener{
@BindView(R.id.query)
Button query;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contentprovider);
ButterKnife.bind(this);
InitClick();
}
private void InitClick() {
query.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.query:
Uri uri_user = Uri.parse("content://com.example.jie.foreverdemo.myprovider/User");
// 获取ContentResolver
ContentResolver resolver = getContentResolver();
// 通过ContentResolver 向ContentProvider中查询数据
resolver.query(uri_user, null, null, null, null);
break;
default:
break;
}
}
}
结果
进程间的数据共享
这里其实跟上面的操作是大同小异的,最主要的知识点呢 就是权限设置问题
举个案例 如果我们想 线程二 访问 线程一 的数据 我们就可以通过如下来进行操作
线程一 会先设置访问自己的权限
<!--在系统中注册读内容提供者的权限-->
<permission
android:name="com.example.jie.foreverdemo.myprovider.READ_CONTENT" //指定权限的名称
android:label="Permission for read content provider"
android:protectionLevel="normal"
/>
给provider设置权限
这里设置的对权限的值为上面我们为内容提供者设置的权限值一样
<provider
android:name=".provider.IPCPersonProvider"
android:authorities="net.sxkeji.shixinandroiddemo2.provider.IPCPersonProvider"
android:exported="true"
android:grantUriPermissions="true"
android:process=":provider"
android:readPermission="com.example.jie.foreverdemo.myprovider.READ_CONTENT">
这样线程一就设置结束了
那么我们如果在线程二想进行访问那应该如何呢
<uses-permission android:name="com.example.jie.foreverdemo.myprovider.READ_CONTENT"/>
这样我们的线程二就可以访问线程一的数据内容了呢
好啦 上面说了那么多其实我们想想我们为什么要使用这个ContentProvider这个内容提供器呢,我们访问自身的数据库,我们直接访问数据内容不好吗,为什么要绕这么远的路还要借助这么一个内容提供器呢。其实呀,我个人感觉就是说,ContentProvider 被创造出来的目的不是为了访问自身的数据内容,而是为了保障应用程序之间的数据的相互访问,为什么这么说呢,因为你想,你不可能直接敞开你的数据库让别人操作吧,你还是得自己规划好,自己的什么内容可以被外界应用程序所访问,这样才能达成,最好的结果。
借用一个大佬的一句话,不浮躁,砥砺前行,加油!
When you are content to be simply yourself and don’t compare or compete, everyone will respect you.
当你满足于做自己而不去比较或竞争时,每个人都会尊重你。