一、为什么要有ContentProvider?
功能需求:一个应用需要访问另一个应用的数据库表数据 data/data/应用包名/database/xxx.db
实际情况:一个应用的数据库文件是私有的,其他应用不能直接访问。
二、什么是ContentProvider?
ContentProvider:内容提供者,是四大组件之一
ContentProvider
类并不会直接与外部进程交互,而是通过ContentResolver
类
当前应用使用ContentProvider将数据库表数据操作暴露给其它应用访问
其它应用需要使用ContentResolver来调用CnotentProvider的方法
它们之间的调用通过uri来进行交流的。
三、ContentProvider的作用?
进程间进行数据(这些数据可以是数据库(Sqlite)、文件、XML、网络数据等)交互&共享 即跨进程通信
四、ContentProvider的原理?
ContentProvider的底层原理就是 Binder机制
五、具体使用
①统一资源标识符Uri 唯一标识ContentProvider 其中的数据
外界通过Uri找到对应的ContentProvider其中的数据,再进行数据操作
Uri模式存在匹配通配符* & #
*:匹配任意长度的任何有效的字符串
#:匹配任意长度的数字字符的字符串
②MIME数据类型
作用:指定某个扩展名的文件用某种应用程序打开
如指定.html文件采用text应用程序打开、指定.pdf文件采用WPS打开
具体使用:ContentProvider根据URI返回MIME类型
ContentProvider.geType(uri) ;
MIME类型是一个包含2部分的字符串
text/html
text/xml
③ContentProvider类
主要是以表格形式组织数据,同时也支持文件数据,只是表格形式用得比较多
每个表格中包含多张表,每张表包含行&列,分别对应记录&字段
主要方法:
进程间共享数据的本质就是:添加、删除、获取&修改更新数据
<-- 4个核心方法 -->
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)
// 外部应用 获取 ContentProvider 中的数据
// 注:
// 1. 上述4个方法由外部进程回调,并运行在ContentProvider进程的Binder线程池中(不是主线程)
// 2. 存在多线程并发访问,需要实现线程同步
// a. 若ContentProvider的数据存储方式是使用SQLite & 一个,则不需要,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无法进行线程同步
// b. 若ContentProvider的数据存储方式是内存,则需要自己实现线程同步
<-- 2个其他方法 -->
public boolean onCreate()
// ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvider时 由系统进行调用
// 注:运行在ContentProvider进程的主线程,故不能做耗时操作
public String getType(Uri uri)
// 得到数据类型,即返回当前 Url 所代表数据的MIME类型
④ContentResolver简介
- 即通过
URI
即可操作 不同的ContentProvider
中的数据- 外部进程通过
ContentResolver
类 从而与ContentProvider
类进行交互
为什么要使用通过ContentResolver
类与ContentProvider
类进行交互,而不直接访问ContentProvider
类?
答:
- 一般来说,一款应用要使用多个
ContentProvider
,若需要了解每个ContentProvider
的不同实现从而再完成数据交互,操作成本高 & 难度大 - 所以再
ContentProvider
类上加多了一个ContentResolver
类对所有的ContentProvider
进行统一管理。
ContentResolver的具体使用?
// 外部进程向 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)
// 使用ContentResolver前,需要先获取ContentResolver
// 可通过在所有继承Context的类中 通过调用getContentResolver()来获得ContentResolver
ContentResolver resolver = getContentResolver();
// 设置ContentProvider的URI
Uri uri = Uri.parse("content://cn.scu.myprovider/user");
// 根据URI 操作 ContentProvider中的数据
// 此处是获取ContentProvider中 user表的所有记录
Cursor cursor = resolver.query(uri, null, null, null, "userid desc");
实例说明:
使用ContentProvider实现进程内通信,数据源来自于Sqlite
①创建数据库类
package com.example.wcystart.wcystart;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
/**
* Created by ${wcystart}
* date:on 2019/1/14
* description: 使用Android内嵌的Sqlite存储数据
*/
public class DBHelper extends SQLiteOpenHelper {
//数据库名
private static final String DATABASE_NAME = "profession.db";
//表名
public static final String USER_TABLE = "user";
public static final String JOB_TABLE = "job";
//数据库版本号
private static final int DATABASE_VERSION = 1;
public DBHelper(Context context){
super(context,DATABASE_NAME,null,DATABASE_VERSION);
}
public DBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
//创建用户表和职业表
db.execSQL("create table "+USER_TABLE+"(_id integer primary key autoincrement,"+ " name text)");
db.execSQL("create table "+JOB_TABLE+"(_id integer primary key autoincrement,"+"job text)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
②自定义ContentProvider类
package com.example.wcystart.wcystart;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* Created by ${wcystart}
* date:on 2019/1/14
* description:自定义ContentProvider类
*/
public class WcystartProvider extends ContentProvider {
private Context mContext;
DBHelper mDbhelper = null;
SQLiteDatabase mDb = null;
//设置ContentProvider的唯一标识
public static final String UNIQUE = "com.example.wcystart.wcystart.WcystartProvider";
public static final int User_Code = 1;
public static final int Job_Code = 2;
//在ContentProvider中注册Uri UriMatcher
private static final UriMatcher mMatcher;
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//初始化
mMatcher.addURI(UNIQUE, "user", User_Code);
mMatcher.addURI(UNIQUE, "job", Job_Code);
//若Uri资源路径=content://com.example.wcystart.wcystart.WcystartProvider/user,则返回注册码User_code
//若Uri资源路径=content://com.example.wcystart.wcystart.WcystartProvider/job,则返回注册码Job_code
}
/**
* 初始化ContentProvider
*
* @return
*/
@Override
public boolean onCreate() {
mContext = getContext();
//在ContentProvider创建时对数据库进行初始化
//运行在住线程,不能做耗时操作
mDbhelper = new DBHelper(mContext);
mDb = mDbhelper.getWritableDatabase();
//初始化两个表的数据,先清空表,再各加一个记录
mDb.execSQL("delete from user");
mDb.execSQL("insert into user values(1,'Jane');");
mDb.execSQL("insert into user values(2,'Json');");
mDb.execSQL("delete from job");
mDb.execSQL("insert into job values(1,'Android');");
mDb.execSQL("insert into job values(2,'Java');");
return true;
}
/**
* 查询数据
* @param uri
* @param projection
* @param selection
* @param selectionArgs
* @param sortOrder
* @return
*/
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
//根据Uri匹配Uri_code,从而匹配ContentProvider中相应的表名
String table=getTableName(uri);
return mDb.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
/**
* 添加数据
*
* @param uri
* @param values
* @return
*/
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
//根据Uri匹配URI_Code,从而匹配ContentProvider中的表名
String table = getTableName(uri);
//向该表中添加数据
mDb.insert(table, null, values);
//当Uri的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
mContext.getContentResolver().notifyChange(uri, null);
return uri;
}
private String getTableName(Uri uri) {
String tableName = null;
switch (mMatcher.match(uri)) {
case User_Code:
tableName = DBHelper.USER_TABLE;
break;
case Job_Code:
tableName = DBHelper.JOB_TABLE;
break;
}
return tableName;
}
/**
* 删除数据
* @param uri
* @param selection
* @param selectionArgs
* @return
*/
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
/**
* 更新数据
* @param uri
* @param values
* @param selection
* @param selectionArgs
* @return
*/
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
③注册创建的ContentProvider类
<!-- 注册ContentProvider -->
<provider
android:name=".WcystartProvider"
android:authorities="com.example.wcystart.wcystart.WcystartProvider" />
④进程内访问ContentProvider的数据
package com.example.wcystart.wcystart;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
public class ProviderActivity extends AppCompatActivity {
private Button mBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_provider);
mBtn=findViewById(R.id.btn);
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
opreteTable();
}
});
}
private void opreteTable() {
//对User表进行操作
Uri uri_user=Uri.parse("content://com.example.wcystart.wcystart.WcystartProvider/user");
//插入表中数据
ContentValues values=new ContentValues();
values.put("_id",3);
values.put("name","wcy");
//获取ContentResolver
ContentResolver contentResolver = getContentResolver();
//通过contnetResolver根据Uri向ContentProvider中插入数据
contentResolver.insert(uri_user,values);
//通过ContentResolver向ContentProvider中查询数据
Cursor cursor=contentResolver.query(uri_user,new String[]{"_id","name"},null,null,null);
while (cursor.moveToNext()){
System.out.println("query user:"+cursor.getInt(0)+" "+cursor.getString(1));
//将user表中的所有数据都打印出来
}
//关闭cursor
cursor.close();
//对job表进行操作
Uri uri_job=Uri.parse("content://com.example.wcystart.wcystart.WcystartProvider/job");
//插入表中数据
ContentValues jobValue=new ContentValues();
jobValue.put("_id",3);
jobValue.put("job","dancer");
ContentResolver jobResolver = getContentResolver();
jobResolver.insert(uri_job,jobValue);
Cursor jobCursor = jobResolver.query(uri_job, new String[]{"_id", "job"}, null, null, null);
while (jobCursor.moveToNext()){
System.out.println("query user:"+jobCursor.getInt(0)+" "+jobCursor.getString(1));
//将user表中的所有数据都打印出来
}
jobCursor.close();
}
}
打印结果:
自此实现了进程内的通信。
那怎样实现进程间通信呢? 进程间通信,也就是说得有两个进程,进程1和进程2,进程1用来创建ContentProvider和存储数据(Sqlite),进程2来访问进程1中ContentProvider存储的数据。
进程1,创建ContentProvider和Sqilte存储数据源跟进程内是一样的,区别就在于,在功能清单文件中注册ContentProvider时的变化。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.wcystart.wcystart">
<permission android:name="com.example.wcystart.wcystart.Read" />
<permission android:name="com.example.wcystart.wcystart.Write" />
<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/AppTheme">
<!-- 注册ContentProvider -->
<!--设置exported="true"可以被其他的进程使用 -->
<provider
android:name=".WcystartProvider"
android:authorities="com.example.wcystart.wcystart.WcystartProvider"
android:exported="true"
android:permission="com.example.wcystart.wcystart.PROVIDER" />
<activity android:name=".ProviderActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
进程2
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.wcystart.otherproject">
<uses-permission android:name="com.example.wcystart.wcystart.Read"/>
<uses-permission android:name="com.example.wcystart.wcystart.Write"/>
<uses-permission android:name="com.example.wcystart.wcystart.PROVIDER"/>
<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/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
进程2访问ContentProvider的类
package com.example.wcystart.otherproject;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private Button mBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtn = findViewById(R.id.btn);
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//访问进程1中ContentProvider存储的数据
requestOtherProcessData();
}
});
}
private void requestOtherProcessData() {
//对User表进行操作
Uri uri_user=Uri.parse("content://com.example.wcystart.wcystart.WcystartProvider/user");
//插入表中数据
ContentValues values=new ContentValues();
values.put("_id",4);
values.put("name","bob");
//获取ContentResolver
ContentResolver contentResolver = getContentResolver();
//通过contnetResolver根据Uri向ContentProvider中插入数据
contentResolver.insert(uri_user,values);
//通过ContentResolver向ContentProvider中查询数据
Cursor cursor=contentResolver.query(uri_user,new String[]{"_id","name"},null,null,null);
while (cursor.moveToNext()){
System.out.println("query user:"+cursor.getInt(0)+" "+cursor.getString(1));
//将user表中的所有数据都打印出来
}
//关闭cursor
cursor.close();
//对job表进行操作
Uri uri_job=Uri.parse("content://com.example.wcystart.wcystart.WcystartProvider/job");
//插入表中数据
ContentValues jobValue=new ContentValues();
jobValue.put("_id",4);
jobValue.put("job","cooker");
ContentResolver jobResolver = getContentResolver();
jobResolver.insert(uri_job,jobValue);
Cursor jobCursor = jobResolver.query(uri_job, new String[]{"_id", "job"}, null, null, null);
while (jobCursor.moveToNext()){
System.out.println("query user:"+jobCursor.getInt(0)+" "+jobCursor.getString(1));
//将user表中的所有数据都打印出来
}
jobCursor.close();
}
}
先运行进程1,在运行进程2
但是报一个错,权限的问题
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.wcystart.otherproject, PID: 28819
java.lang.SecurityException: Permission Denial: opening provider com.example.wcystart.wcystart.WcystartProvider from ProcessRecord{4b086b6 28819:com.example.wcystart.otherproject/u0a862} (pid=28819, uid=10862) requires com.example.wcystart.wcystart.PROVIDER or com.example.wcystart.wcystart.PROVIDER
at android.os.Parcel.readException(Parcel.java:1954)
at android.os.Parcel.readException(Parcel.java:1900)
at android.app.IActivityManager$Stub$Proxy.getContentProvider(IActivityManager.java:4779)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:6645)
at android.app.ContextImpl$ApplicationContentResolver.acquireProvider(ContextImpl.java:2786)
at android.content.ContentResolver.acquireProvider(ContentResolver.java:1773)
at android.content.ContentResolver.insert(ContentResolver.java:1551)
at com.example.wcystart.otherproject.MainActivity.requestOtherProcessData(MainActivity.java:40)
at com.example.wcystart.otherproject.MainActivity.access$000(MainActivity.java:12)
at com.example.wcystart.otherproject.MainActivity$1.onClick(MainActivity.java:24)
at android.view.View.performClick(View.java:6291)
at android.view.View$PerformClick.run(View.java:24931)
at android.os.Handler.handleCallback(Handler.java:808)
at android.os.Handler.dispatchMessage(Handler.java:101)
at android.os.Looper.loop(Looper.java:166)
at android.app.ActivityThread.main(ActivityThread.java:7425)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:245)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:921)
我明明权限都写了啊,而且进程1和进程2权限对应着呢,但是为什么还要报这个权限没添加的错呢?
经过测试之后,发现了问题所在
在进程1中:虽然给provider定义了权限,但是还得在应用中再定义下:
补充说明:android:protectionLevel="normal"它是干嘛用的呢?
normal:这是最低风险的权限,如果应用声明了此权限,也不会提示安装应用的用户授权(例如,如果声明了定位权限,则应用到定位功能时,会明确提示用户,是否授予定位权限,但是protectionLevel为normal的不会明确提示,直接默认授予),系统直接默认该应用有此权限;
dangerous:这种级别的权限风险更高,拥有此权限可能会访问用户私人数据或者控制设备,给用户带来负面影响,这种类型的权限一般不会默认授权(但是我测了好多次,有时候还是会默认授权);
signature:这种权限级别,只有当发请求的应用和接收此请求的应用使用同一签名文件,并且声明了该权限才会授权,并且是默认授权,不会提示用户授权
signatureOrSystem:这种权限应该尽量避免使用,偏向系统级
在补充说明:permission与user-permission的区别
permission:用于自定义权限
user-permission:申请权限
这样进程2中添加上相应的权限之后才能够操作进程1的数据
进程2对进程1的数据先插入在查询之后的打印结果为:
总结ContentProvider存储数据的特点
优点:安全
ContentProvider
为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给 其他应用 进行 增、删、改、查,而不用担心因为直接开放数据库权限而带来的安全问题
访问高效、简单
因为其解耦了底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的。