Android — 之内容提供器(Content Provider)

时间:2022-05-26 09:20:21

简介

  内容提供器(Content Provider)主要同于在不同的应用程序之间实现数据共享的一种机制,允许一个程序访问另一个程序中的私有数据,同时可以保证被访问的数据的安全。使用Content Provider我们可以选择只把程序中某一部分数据共享出去,隐私的数据并不向外暴露接口。
  假设我们已经向外暴露了共享数据的接口,那么通过什么来访问呢?这里先简单剧透一下,Android中是使用ContentResorver类开启访问的。
  另外,因为可以共享出去的数据,可以是文件,也可以是数据库等,而我们开发中一般是使用数据库,所以这里接下来说的ContentProvider的数据就是数据库了。

Uri

  那么,问题来了:Android系统中是有很多的ContentProvider的,它们都向外共享着数据,而我们怎么知道我们要访问的ContentProvider的是哪一个呢?
  举个例子吧,度娘是一个搜索引擎,谷哥也是一个搜索引擎,那么我们怎么访问并区别它们呢?是的,就是通过ip地址:
  
  度娘ip:www.baidu.com 
  谷哥ip:www.google.com
  
  OK,现在把谷歌和度娘区分了后,假设我们要使用度娘,要该怎么区分我们要访问的数据是关于”海贼王”,还是关于“银魂”的呢?使用度娘分别搜一下,地址栏中出现如下:

  银 魂:www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=银魂 …
  海贼王:www.baidu.com/s?ie=utf-8&f=8&rsv_bp=0&rsv_idx=1&tn=baidu&wd=海贼王 …

  正如加粗的部分所示,通过ip后跟着的不同的路径来区分的。
  到这里基本上就可以知道怎么访问度娘里海贼王的数据了,但是还得要提一嘴的是,我们还要指明使用什么协议来进行数据的请求,http或者https。那么,一个完整的访问海贼王的URL连接就是:
  https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=0&rsv_idx=1&tn=baidu&wd=海贼王 …

  哎呀,好巧啊~ Android的ContentProvider也采取了类似的机制,这个机制就是使用自定义的内容 URI
  URI为内容提供器中的数据建立了唯一标识符,它主要由两部分组成:

  • authority:权限或者说地址,类比于ip, 是用于对不同的应用程序(的Content Provider)做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名,如com.example.app。

  • path:路径,类比于ip后的“…银魂”,则是用于对同一应用程序(的Content Provider)中不同的数据(如数据库中的数据表)做区分的,通常都会添加到权限的后面。比如某个程序的数据库里存在两张表, table1 和 table2,这时就可以将路径分别命名为/table1和/table2

    然后把authority和path进行组合, 那么
    访问table1中的数据就可以使用: com.example.app.provider/table1
    访问table2中的数据就可以使用:com.example.app.provider/table2

    同时,内容提供器也是有协议的:content,因此一个访问table1中数据标准的URI的可以写成:content://com.example.app/table1
    内容 URI 可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据有木有!!!
    最后根据URI字符串得到Uri对象
    Uri uri = Uri.parse(“content://com.example.app.provider/table1”)
    只需要调用 Uri.parse(String uriString)方法即可。

另外,我们还可以在这个内容 URI 的后面加上一个 “id”,如下所示:
content://com.example.app.provider/table1/1
这就可以表示期望访问的是 com.example.app 这个应用的 table1 表中 id 为 1 的数据
  当然,这是自定义的,你也可以用其他的方式来这么表示,只不过,这算是规范吧。

  内容 URI 的格式主要就只有以上两种,以路径结尾就表示期望访问该表中所有的数据,以 id 结尾就表示期望访问该表中拥有相应 id 的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容 URI,规则如下。
   *:表示匹配任意长度的任意字符
   #:表示匹配任意长度的数字
  所以,一个能够匹配任意表的 URI 就可以写成:
    content://com.example.app/*
  一个能够匹配 table1 表中任意一行数据 URI 就可以写成:
    content://com.example.app/table1/#
  

ContentProvider 类介绍

  
  首先,ContentProvider是一个抽象的类,那么我们就需要自定义一个类如MyProvider来继承它,并且实现其中的方法(6个抽象方法全部要实现):
  

package com.yu.contentprovidertest;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.Nullable;

/**
* Created by yu on 2016-04-21.
*/

public class MyProvider extends ContentProvider {

//初始化
@Override
public boolean onCreate() {
return false;
}

//查
@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}

//插
@Nullable
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}

//删
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}

//改
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}

// MIME ,其实卵用不大,就是为了告诉ContentResolver你操作的是什么样的数据
@Nullable
@Override
public String getType(Uri uri) {
return null;
}

}
  • 简单介绍一下这六个方法:

    • onCreate()
      初始化内容提供器的时候调用,而且,只有当存在ContentResolver 尝试访问我们程序中的数据时,内容提供器才会被初始化。通常会在这里完成对数据库的创建和升级等操作,返回 true 表示内容提供器初始化成功,返回 false 则表示失败。

    • Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
      从内容提供器中查询数据。参数及返回值:

      • uri :确定要查询的数据是什么,如是查询table1还是table2;
      • projection :确定要查询哪些列(表中的字段),相当于SQL语句中的from后跟的查询项,null表示查所有;
      • selection 和 selectionArgs :用于约束查询哪些行,相当于where条件,null表示没有约束;
      • sortOrder :用于对结果进行排序,null表示不排序;
      • 返回Cursor:查询的结果存放在 Cursor对象中返回。
    • Uri insert(Uri uri, ContentValues values)
      向内容提供器中添加一条数据。参数及返回值:

      • uri :确定数据要添加到的表;
      • values:待添加的数据封装在 values 参数中;
      • 返回URI:返回一个用于表示这条新记录的 URI。
    • int delete(Uri uri, String selection, String[] selectionArgs)
      从内容提供器中删除数据。参数及返回值:

      • uri :确定删除哪一张表中的数据;
      • selection和 selectionArgs :用于约束删除哪些行;
      • 返回int: 表示被删除的行数。
    • int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
      更新内容提供器中已有的数据。参数及返回值:

      • uri :来确定更新哪一张表中的数据;
      • 新数据保存在 values 参数中;
      • selection 和 selectionArgs 参数用于约束更新哪些行;
      • 返回int:表示受影响的行数。
    • String getType(Uri uri)
      根据传入的内容 URI 来返回相应的 MIME 类型

    细心的你,应该已经发现了,每个方法都有一个Uri对象有木有

    注册ContentProvider,ContentProvider是android的四大组件之一,也就拥有四大组件共有的特性,那就是必须在清单文件件中进行注册才能使用

 <provider
android:name=".MyProvider"
android:authorities="com.yu.contentprovidertest"
android:exported="true"/>

  name: 要注册的内容提供器的类
  authorities: 权限/地址,也就是上面URI中的authority,外部要想访问该内容提供器的数据,首先要匹配地址才能通过。
  exported:设为true表示可以从内容提供器导出数据,也就是可以查询。默认是false,不能导出数据。

共享的数据

  既然要使用ContentProvider共享数据,那么首先要提供数据源才行,因此,先来创建一个数据库(bookstore.db),并创建一张表(book)吧。新建一个类,继承自SQLiteOpenHelper 。

package com.yu.contentprovidertest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

/**
* Created by yu on 2016-04-22.
* 数据库帮助类,用于创建升级数据库数据
*/

public class MyDBHelper extends SQLiteOpenHelper {

/**
* 创建book表的语句,表中的字段:
* id : 标识每本书的键值id,自增
* author: 本书作者
* price : 价格
* pages : 页数
* name : 书名
*/

public static final String CREATE_BOOK = "create table book (" +
"id integer primary key autoincrement," +
"author text," +
"price read," +
"pages integer," +
"name text)";

/**
* 构造方法
* context:上下文环境
* name:数据库的名字(bookstrore)
* factory:游标工厂,null表示使用默认的游标工厂
* version:数据库的版本,当这个值比原来数据库版本大时,onUpgrade方法会调用来升级数据库
**/

public MyDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}

//只创建数据库时调用一次,如果数据库已经存在了,就不再调用
@Override
public void onCreate(SQLiteDatabase db) {
//创建数据库时执行创建book表的语句
db.execSQL(CREATE_BOOK);
Log.d("MyDBHelper","create db");
}

//升级数据库时调用,当数据库newVersion比oldVersion大时会调用
//因此要在这个方法中完成升级数据库的逻辑
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

}
}

  这样要共享的数据源就已经创建好了,接下来就是实现ContentProvider中的方法。

实现ContentProvider

  在实现ContentProvider之前,还是得先来学习一个知识点:怎么匹配URI,什么意思呢?就是当ContentResolver通过URI(如content://com.yu.contentprovidertest/book)发起访问的时候,ContentProvider需要知道ContentResolver想访问什么,这就用到了UriMatcher这个类。
  UriMatcher类提供了一个 addURI()方法,用来添加匹配规则,这个方法接收三个参数,可以分别把权限、路径和一个自定义代码传进去。
  UriMatcher 还提供了 match()方法,可以将一个 Uri 对象传入,返回值是某个能够匹配这个 Uri 对象所对应的自定义代码,利用这个代码,我们就可以判断出ContentResolver期望访问的是哪张表中的数据,还是哪张表中的某一条数据了。如
 uriMatcher.addURI(“com.yu.contentprovidertest”,”book”,1); 这个规则就可以匹配ContentResolver的 content://com.yu.contentprovidertest/book 请求。
uriMatcher.match(uri)如果匹配成功就返回自定义的代码 1。

好了,接下来就直接贴上有注释的代码了

package com.yu.contentprovidertest;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

/**
* Created by yu on 2016-04-22.
*/

public class MyProvider extends ContentProvider {

public static final String TAG = "ContentProvider";
private SQLiteOpenHelper helper;
//要访问的数据库名
public static final String DB_NAME = "bookstore.db";
//共享的表名
public static final String TABLE_NAME = "book";
//数据库版本
public static final int DB_VERSION = 1;

//匹配码:表示要访问book表中的所有数据
public static final int BOOK_ALL = 1;
//匹配码:表示要访问book表中某一项(id)的数据
public static final int BOOK_ITEM = 2;
//匹配的权限/地址,跟清单文件中注册的一致
public static final String AUTHORITY = "com.yu.contentprovidertest";

//uri匹配器
private static UriMatcher uriMatcher;
static {
//创建Uti匹配器,NO_MATCH表示默认没有匹配成功的返回码,值为-1
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
/**
* 添加匹配规则
* arg0:要匹配的权限/地址
* arg1:要匹配的路径
* arg2:匹配成功的返回码(匹配码)
*/

uriMatcher.addURI(AUTHORITY,"book",BOOK_ALL); // 匹配URI-> content://com.yu.contentprovidertest/book
uriMatcher.addURI(AUTHORITY,"book/#",BOOK_ITEM); // 匹配URI-> content://com.yu.contentprovidertest/book/1
}

@Override
public boolean onCreate() {
//创建SQLiteOpenHelper实例
helper = new MyDBHelper(getContext(),DB_NAME,null,DB_VERSION);
Log.d(TAG,"onCreate");
//返回true,表示ContentProvider初始化成功
return true;
}

//查
@Nullable
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
//获取数据库读写对象
SQLiteDatabase db = helper.getWritableDatabase();
Cursor cursor = null;
//匹配Uri
switch (uriMatcher.match(uri)) {
case BOOK_ALL://表示查询book表中的所有满足查询条件的数据
//执行book表查询操作,方法中的限制条件都是ContentProvider传递过来的
cursor = db.query(TABLE_NAME,projection,selection,selectionArgs,null,null,sortOrder);
Log.d(TAG,"query book all");
break;
case BOOK_ITEM://表示查询book表中id为 _id的某条数据
//两种方式,取出uri末尾携带的数字(id)
//long _id = ContentUris.parseId(uri);
String _id = uri.getPathSegments().get(1);
//执行book表查询操作,并指定查询的id
cursor = db.query(TABLE_NAME,projection,"id = ?",new String[]{_id},null,null,sortOrder);
Log.d(TAG,"query book item");
break;
}
//关闭数据库与游标
//db.close();
//cursor.close();
return cursor;
}

//插
@Nullable
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
//获取数据库读写对象
SQLiteDatabase db = helper.getWritableDatabase();
//执行插入数据的操作,值为ContentProvider传过来的values
long rowId = db.insert(TABLE_NAME,null,values);
db.close();
//返回可以表示插入的新数据的uri
Uri uriRet = Uri.parse("content://" + AUTHORITY + "/book/" + rowId);
Log.d(TAG,"insert book");
return uriRet;
}

//删
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
//获取数据库读写对象
SQLiteDatabase db = helper.getWritableDatabase();
//访问操作执行后受影响的行数
int effectRows=0;
//匹配Uri
switch (uriMatcher.match(uri)) {
case BOOK_ALL://删除book表中的所有满足条件的数据
effectRows = db.delete(TABLE_NAME,selection,selectionArgs);
Log.d(TAG,"delete book all");
break;
case BOOK_ITEM://删除book表中指定id的数据
String _id = uri.getPathSegments().get(1);
effectRows = db.delete(TABLE_NAME,"id = ?",new String[]{_id});
Log.d(TAG,"delete book item");
break;
}

db.close();
return effectRows;
}

//改
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
//获取数据库读写对象
SQLiteDatabase db = helper.getWritableDatabase();
int effectRows=0;
//匹配Uri
switch (uriMatcher.match(uri)) {
case BOOK_ALL://更新book表中的所有满足条件的数据
effectRows = db.update(TABLE_NAME,values,selection,selectionArgs);
Log.d(TAG,"update book all");
break;
case BOOK_ITEM://更新book表中指定id的数据
String _id = uri.getPathSegments().get(1);
effectRows = db.update(TABLE_NAME,values,"id = ?",new String[]{_id});
Log.d(TAG,"update book item");
break;
}

db.close();
return effectRows;
}

@Nullable
@Override
public String getType(@NonNull Uri uri) {
Log.d(TAG,"getType");
return null;
}
}

到这里,使用ContentProvider来共享数据的功能就已经实现了,那我们在另起一个项目,写访问ContentProvider的数据的代码。

ContentResolver

  开篇就已经提到过,Android中,想要访问ContentProvider的共享数据,就只能使用ContentResolver这个类。这个类也有query()、insert()、delete()、update()这些方法,这些方法中的参数,跟ContentProvider中的参数一模一样的(可以假认为是从ContentResolver直接传递到ContentProvider中的),因次,每个方法,也都有一个Uri参数,即用来确定,要访问哪个ContentProvider中什么样的共享数据,这里的Uri就是ContentProvider中要进行匹配的Uri对象。
  废话不多说了(已经说的够多了…),上代码…

package com.yu.contentresorver;

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.util.Log;
import android.view.View;

public class MainActivity extends AppCompatActivity {

public static final String TAG = "ContentResolver";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

//查询book数据
public void queryBook(View v) {
//创建ContentResolver对象,访问ContentProvider
ContentResolver resolver = getContentResolver();
//创建uri,查询book表中的所有数据
Uri uri = Uri.parse("content://com.yu.contentprovidertest/book");
//查询操作,返回游标
Cursor cursor = resolver.query(uri,null,null,null,null,null);
if (cursor != null) {
while(cursor.moveToNext()) {
//从游标中获取返回的数据
int id = cursor.getInt(cursor.getColumnIndex("id"));
String author =cursor.getString(cursor.getColumnIndex("author"));
float price = cursor.getFloat(cursor.getColumnIndex("price"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
String name = cursor.getString(cursor.getColumnIndex("name"));
//打印数据
Log.d(TAG,id + ";" + author + ";" + price + ";" + pages + ";" + name);
}
//关闭游标
cursor.close();
}
}

//插入数据到book表
public void insertBook(View v) {
ContentResolver resolver = getContentResolver();
//创建uri,向book表中插入数据
Uri uri = Uri.parse("content://com.yu.contentprovidertest/book");
//封装要插入的一条数据
ContentValues values = new ContentValues();
values.put("name", "冰与火之歌");
values.put("author", "乔治·R·R·马丁");
values.put("pages", 200202);
values.put("price", 22.22);
//插入操作
Uri uri1 = resolver.insert(uri,values);
Log.d(TAG,"uri1 = " + uri1.toString());

//另一条数据
values.clear();
values.put("name", "画尸人");
values.put("author", "偏离纬度");
values.put("pages", 100101);
values.put("price", 11.11);
//插入操作
Uri uri2 = resolver.insert(uri,values);
Log.d(TAG,"uri2 = " + uri2.toString());
}

//删除book表中的数据
public void deleteBook(View v) {
ContentResolver resolver = getContentResolver();
//创建uri,删除book表中id为1的某条数据
Uri uri = Uri.parse("content://com.yu.contentprovidertest/book/1");
//删除操作,返回删除的行数
int effectedRows = resolver.delete(uri,null,null);
Log.d(TAG,"删除了" + effectedRows + "条数据");
}

//更新book表数据
public void updateBook(View v) {
ContentResolver resolver = getContentResolver();
//创建uri,更新book表中所有数据
Uri uri = Uri.parse("content://com.yu.contentprovidertest/book");
//封装更新的数据
ContentValues values = new ContentValues();
//改变所有书的价格
values.put("price",33.33);
//更新操作,返回影响的行数
int effectedRows = resolver.update(uri,values,null,null);
Log.d(TAG,"更新了" + effectedRows + "条数据");

//更新book表中id为1的某条数据
uri = Uri.parse("content://com.yu.contentprovidertest/book/1");
values.clear();
values.put("price",22.66);
effectedRows = resolver.update(uri,values,null,null);
Log.d(TAG,"更新了" + effectedRows + "条数据");
}
}

好了,两个程序都已经写好了,那就开始进行测试吧。

部署运行程序

ContentProvider简单的界面:
Android — 之内容提供器(Content Provider)

操作步骤与对应的控制台日志:
步骤: 插入 -> 查询 -> 更新 -> 查询 -> 删除 -> 查询
ContentResolver日志:
Android — 之内容提供器(Content Provider)
ContentProvider日志:
Android — 之内容提供器(Content Provider)

卧槽!!终于写完了!!! 大脑已经开始抛异常了….我去重启一下….

注:如果有什么表述不正确的,请各位看官大婶们,多多指正,望不吝赐教