Android四大组件之一ContentProvider

一、为什么要有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简介

  1. 即通过 URI 即可操作 不同的ContentProvider 中的数据
  2. 外部进程通过 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为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给 其他应用 进行 增、删、改、查,而不用担心因为直接开放数据库权限而带来的安全问题

访问高效、简单

因为其解耦了底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的。

猜你喜欢

转载自blog.csdn.net/qq_37982823/article/details/86417460