Android ContentProvider 完全解析及DEMO(最具说服力)

转载来自:  http://blog.csdn.net/yhaolpz/article/details/51304345


一:为什么使用ContentProvider,它有什么作用?


1) ContentProvider为存储和读取数据提供了统一的接口

2) 使用ContentProvider,应用程序可以实现数据共享

3) android内置的许多数据都是使用ContentProvider形式,供开发者调用的(如视频,音频,图片,通讯录等)


多个程序打开数据库,读和写都必须先通过ConrtentProvide ,然后再修改的!
否则3个程序同时打开数据库是会出现问题的!


Android系统提供了像SharedPreferences这类简单的跨越程序边界的访问方法,但这些方法都存在一定的局限性。ContentProvider(数据提供者)是应用程序之间共享数据的一种接口机制,是一种更为高级的数据共享方法。

二:ContentProvider机理

调用关系

ContentProvider调用关系

在创建ContentProvider前,首先要实现底层的数据源,数据源包括数据库、文件系统或网络等,然后继承ContentProvider类中实现基本数据操作的接口函数。调用者不能直接调用ContentProvider的接口函数,需要通过ContentResolver对象,通过URI间接调用ContentProvider。 

 ContentProvider提供的数据形式

ContentProvider的数据集类似于数据库的数据表,每行是一条记录,每列具有相同的数据类型。每条记录都包含一个长整型的字段 _ID,用来唯一标识每条记录。ContentProvider可以提供多个数据集,调用者使用URI对不同数据集的数据进行操作。

通用资源标识符(Uniform Resource Identifier)

URI是一个用于标识某一互联网资源名称的字符串。 该种标识允许用户对任何(包括本地和互联网)的资源通过特定的协议进行交互操作。在ContentProvider机制中,使用ContentResolver对象通过URI定位ContentProvider提供的资源。 
ContentProvider使用的URI语法结构如下:

    content://<authority>/<data_path>/<id>
 
 
  • 1
  • 1
  • content:// 是通用前缀,表示该UIR用于ContentProvider定位资源。
  • < authority > 是授权者名称,用来确定具体由哪一个ContentProvider提供资源。因此一般< authority >都由类的小写全称组成,以保证唯一性。
  • 里面的<>代表的是泛型,不要写入进去,比如:content://com.example.peopleprovider/people
  • < data_path > 是数据路径,用来确定请求的是哪个数据集。如果ContentProvider近提供一个数据集,数据路径则可以省略;如果ContentProvider提供多个数据集,数据路径必须指明具体数据集。数据集的数据路径可以写成多段格式,例如people/girl和people/boy。
  • < id > 是数据编号,用来唯一确定数据集中的一条记录,匹配数据集中_ID字段的值。如果请求的数据不只一条,< id >可以省略。

如请求整个people数据集的URI为:

content://com.example.peopleprovider/people
 
 
  • 1
  • 1

而请求people数据集中第3条数据的URI则应写为:

content://com.example.peopleprovider/people/3

三:创建数据提供者

1. 创建一个类让其继承ContentProvider,并重载6个函数

  • onCreate() 
    一般用来初始化底层数据集和建立数据连接等工作

  • getType() 
    用来返回指定URI的MIME数据类型,若URI是单条数据,则返回的MIME数据类型以vnd.android.cursor.item开头;若URI是多条数据,则返回的MIME数据类型以vnd.android.cursor.dir/开头。

如果要处理的数据类型是一种比较新的类型

  • 你就必须先定义一个新的MIME类型,以供ContentProvider.geType(url)来返回。
  • MIME类型有两种形式: 
    1. 一种是为指定的单个记录的
    2. 另一种是为多条记录的。

这里给出一种常用的格式:


这里给出一种常用的格式:

vnd.android.cursor.item/vnd.yourcompanyname.contenttype // 单个记录的MIME类型

比如, 一个请求列车信息的URI
content://com.example.transportationprovider/trains/122 
可能就会返回
typevnd.android.cursor.item/vnd.example.rail
这样一个MIME类型
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
vnd.android.cursor.dir/vnd.yourcompanyname.contenttype // 多个记录的MIME类型
比如, 一个请求所有列车信息的URI
content://com.example.transportationprovider/trains 
可能就会返回
vnd.android.cursor.dir/vnd.example.rail
这样一个MIME 类型

  • insert()、delete()、update()、query() 
    用于对数据集的增删改查操作。

2. 声明CONTENT_URI,实现UriMatcher

private static final int MULTIPLE_PEOPLE = 1;//访问表的所有列
private static final int SINGLE_PEOPLE = 2;//访问单独的列
private static final UriMatcher uriMatcher ;

static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(People.AUTHORITY, People.PATH_MULTIPLE, MULTIPLE_PEOPLE);
    uriMatcher.addURI(People.AUTHORITY, People.PATH_SINGLE, SINGLE_PEOPLE);
}

其中UriMatcher类引用官方文档中的解释:

Utility class to aid in matching URIs in content providers.

可见UriMatcher本质上是一个文本过滤器,用在contentProvider中帮助我们过滤,分辨出查询者想要查询哪个数据表。 
UriMatcher的构造函数中,UriMatcher.NO_MATCH是URI无匹配时的返回代码,值为-1。 addURI() 方法用来添加新的匹配项,语法为:

public void addURI(String authority, String path, int code)
 
 
  • 1
  • 1

其中authority表示匹配的授权者名称,path表示数据路径(#代表任何数字),code表示返回代码。

@Override
public String getType(Uri uri) {
    switch (uriMatcher.match(uri)){
        case MULTIPLE_PEOPLE://多條數據的處理
            return People.MIME_TYPE_MULTIPLE;
        case SINGLE_PEOPLE://單條數據的處理
            return People.MIME_TYPE_SINGLE;
        default:
            throw new IllegalArgumentException("Unkown uro:"+uri);
    }
}

3. 注册ContentProvider

在AndroidManifest.xml文件中的 application节点下使用< provider >标签注册。示例:

<provider
    android:authorities="contenprovide.peng.cx.com.mycontentprovidedemo"
    android:name=".PeopleProvider" />
上例中注册了一个授权者名称为contenprovide.peng.cx.com.mycontentprovidedemo,其实现类为 Peopleprovider 。

四:使用数据提供者

每个Android组件都有一个ContentResolver对象,通过调用getContentResolver() 方法可得到ContentResolver对象。

demo实例:



public class People {

    public static final String MIME_DIR_PREFIX = "vnd.android.cursor.dir";
    public static final String MIME_ITEM_PREFIX = "vnd.android.cursor.item";
    public static final String MIME_ITEM = "vnd.example.people";

    public static final String MIME_TYPE_SINGLE = MIME_ITEM_PREFIX + "/" + MIME_ITEM ;
    public static final String MIME_TYPE_MULTIPLE = MIME_DIR_PREFIX + "/" + MIME_ITEM ;

    public static final String AUTHORITY = "com.example.peopleprovider";
    public static final String PATH_SINGLE = "people/#";
    public static final String PATH_MULTIPLE = "people";


    /**
     * 封裝標準的形式:content://<authority>/<data_path>/<id>
     */
    public static final String CONTENT_URI_STRING = "content://" + AUTHORITY + "/" + PATH_MULTIPLE;


    /**
     * 暴露和共享的URL     */
    public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);


    /**
     * 數據庫表的字段
     */
    public static final String KEY_ID = "_id";
    public static final String KEY_NAME = "name";
    public static final String KEY_AGE = "age";
    public static final String KEY_HEIGHT = "height";

}

/**
 * 继承ContentProvider ,实现他的所有方法  on 2017/5/3.
 */
public class PeopleProvider extends ContentProvider{


    private static final String DB_NAME="people.db";
    private static final String DB_TABLE="peopleinfo";
    private static final int DB_VERSION = 1;

    private SQLiteDatabase db;
    private DBOpenHelper dbOpenHelper;


    private static final int MULTIPLE_PEOPLE = 1;//访问表的所有列
    private static final int SINGLE_PEOPLE = 2;//访问单独的列
    private static final UriMatcher uriMatcher ;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(People.AUTHORITY, People.PATH_MULTIPLE, MULTIPLE_PEOPLE);
        uriMatcher.addURI(People.AUTHORITY, People.PATH_SINGLE, SINGLE_PEOPLE);
    }



    @Override
    public boolean onCreate() {
        Context context = getContext();
        dbOpenHelper = new DBOpenHelper(context, DB_NAME, null, DB_VERSION);
        db = dbOpenHelper.getWritableDatabase();
        if(db == null){
            return false;
        }else{
            return true;
        }
    }

    @Nullable
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();//API
        qb.setTables(DB_TABLE);
        switch (uriMatcher.match(uri)){
            case SINGLE_PEOPLE:
                qb.appendWhere(People.KEY_ID+"="+uri.getPathSegments().get(1));
                break;
            default:
                break;
        }
        Cursor cursor = qb.query(db,
                projection,
                selection,
                selectionArgs,
                null,
                null,
                sortOrder);
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }

    @Nullable
    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)){
            case MULTIPLE_PEOPLE://多條數據的處理
                return People.MIME_TYPE_MULTIPLE;
            case SINGLE_PEOPLE://單條數據的處理
                return People.MIME_TYPE_SINGLE;
            default:
                throw new IllegalArgumentException("Unkown uro:"+uri);
        }
    }

    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        //如果添加成功,利用新添加的Id
        long id =db.insert(DB_TABLE, null, values);
        if(id>0){

            //content://contacts/people/45 这个URI就可以写成如下形式:
           // Uri person = ContentUris.withAppendedId(People.CONTENT_URI,  45);

            Uri newUri = ContentUris.withAppendedId(People.CONTENT_URI, id);
            //通知监听器,数据已经改变
            getContext().getContentResolver().notifyChange(newUri, null);
            return newUri;
        }
        throw new SQLException("failed to insert row into " + uri);
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int count = 0;
        switch (uriMatcher.match(uri)){
            case MULTIPLE_PEOPLE:
                count = db.delete(DB_TABLE, selection, selectionArgs);
                break;
            case SINGLE_PEOPLE:
                String segment = uri.getPathSegments().get(1);
                count = db.delete(DB_TABLE, People.KEY_ID + "=" + segment, selectionArgs);
                break;
            default:
                throw new IllegalArgumentException("Unsupported URI:" + uri);
        }
        getContext().getContentResolver().notifyChange(uri,null);
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        int count;
        switch (uriMatcher.match(uri)){
            case MULTIPLE_PEOPLE:
                count = db.update(DB_TABLE, values, selection, selectionArgs);
                break;
            case SINGLE_PEOPLE:
                String segment = uri.getPathSegments().get(1);
                count = db.update(DB_TABLE, values, People.KEY_ID + "=" + segment, selectionArgs);
                break;
            default:
                throw new IllegalArgumentException("Unknow URI: " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }


    private static class DBOpenHelper extends SQLiteOpenHelper {

        private static final  String DB_CREATE = "create table "+
                DB_TABLE+"("+People.KEY_ID+" integer primary key autoincrement, "+
                People.KEY_NAME+" text not null, "+People.KEY_AGE+" integer, "+
                People.KEY_HEIGHT+" float);";

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

        @Override
        public void onCreate(SQLiteDatabase db) {

            db.execSQL(DB_CREATE);

        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

            db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
            onCreate(db);

        }
    }

/**
 * 只需要People.CONTENT_URI
 * @param name
 * @param age
 * @param heigth
 */
public void add(String name,int age,float heigth){
    ContentValues values = new ContentValues();
    values.put(People.KEY_NAME, name);
    values.put(People.KEY_AGE, age);
    values.put(People.KEY_HEIGHT, heigth);
    Uri newUri = resolver.insert(People.CONTENT_URI, values);
    tv_show.setText("添加成功,URI:" + newUri);
}

/**
 * 同樣只需要URL
 */
public void delete(){
    resolver.delete(People.CONTENT_URI, null, null);
    String msg = "数据全部删除";
    tv_show.setText(msg);
}


public void update(String name,int age,float height,String id){
    ContentValues values = new ContentValues();
    values.put(People.KEY_NAME, name);
    values.put(People.KEY_AGE, age);
    values.put(People.KEY_HEIGHT, height);
    Uri uri = Uri.parse(People.CONTENT_URI_STRING + "/" + id);
    int result = resolver.update(uri, values, null, null);
    String msg = "更新ID" + id + "的数据" + (result > 0 ? "成功" : "失败");
    tv_show.setText(msg);
}

public void query(){
    Cursor cursor = resolver.query(People.CONTENT_URI,
            new String[]{People.KEY_ID, People.KEY_NAME, People.KEY_AGE, People.KEY_HEIGHT},
            null, null, null);
    if (cursor == null) {
        tv_show.setText("数据库中没有数据");
        return;
    }
    tv_show.setText("数据库:" + String.valueOf(cursor.getCount()) + "条记录");
    String msg= "";
    if (cursor.moveToFirst()) {
        do {
            msg += "ID: " + cursor.getString(cursor.getColumnIndex(People.KEY_ID)) + ",";
            msg += "姓名: " + cursor.getString(cursor.getColumnIndex(People.KEY_NAME)) + ",";
            msg += "年龄: " + cursor.getInt(cursor.getColumnIndex(People.KEY_AGE)) + ",";
            msg += "身高: " + cursor.getFloat(cursor.getColumnIndex(People.KEY_HEIGHT)) + ",";
        } while (cursor.moveToNext());
    }
    tv_display.setText(msg);
}

补充讲解:


//content://contacts/people/45 这个URI就可以写成如下形式:
// Uri person = ContentUris.withAppendedId(People.CONTENT_URI,  45);

 Uri newUri = ContentUris.withAppendedId(People.CONTENT_URI, id);

Uri newUri = ContentUris.withAppendedId(People.CONTENT_URI, id)
我们使用SQLiteQueryBuilder来辅助数据库查询操作,使用这个类的好处是我们可以不把数据库表的字段暴露出来,而是提供别名给第三方应用程序使用,这样就可以把数据库表内部设计隐藏起来,方便后续扩展和维护。
  • SQLiteQueryBuilder

public class 
SQLiteQueryBuilder 
extends Object

This is a convience class that helps build SQL queries to be sent to 
SQLiteDatabase objects.

  • uri.getPathSegments()

public abstract List getPathSegments () 
Added in API level 1 
Gets the decoded path segments. 
Returns 
decoded path segments, each without a leading or trailing ‘/’

  • uri.getPathSegments().get(position)

public abstract E get (int location) 
Added in API level 1 
Returns the element at the specified location in this List. 
Parameters 
location 
the index of the element to return. 
Returns 
the element at the specified location. 
Throws 

问题:

1.PeopleProvider:什么时候创建数据库?怎么使用ContentProvider?

MyProvider是由ActivityThread负责启动的,ActivityThread对应应用进程的主线程,即在应用进程启动时,会将ContentProvider启动起来。

所以程序要先运行一遍,然后生成了数据库就可以,注册了ContentProvider,会自动执行ContentProvider的OnCreate方法

只有一个表,怎么会有2个URL,对的,用于提供不同的查询条件!

多个记录和单个记录的操作

  content://contacts/people/       这个URI将返回设备上的所有联系人信息

content://contacts/people/45     这个URI返回单个结果(联系人信息中ID为45的联系人记录)

2.怎么得到系统电话本,图片的URL和字段呢?

通过看源码:在源码/Provide里面的

总结:

1.增删改查都必须用到给的URL

2.contentprovider的用户都不可能直接访问到contentprovider实例,只能通过ContentResolver在中间代理。


3.说说 ContentProvider、ContentResolver、ContentObserver 之间的关系

a. ContentProvider 内容提供者,用于对外提供数据 
b. ContentResolver.notifyChange(uri)发出消息 ,内容解析者,用于获取内容提供者提供的数据 
c. ContentResolver 内容解析者,用于获取内容提供者提供的数据 
d. ContentObserver 内容监听器,可以监听数据的改变状态 
e. ContentResolver.registerContentObserver()监听消息。


面试题:多个进程同时调用一个ContentProvider的query获取数据,ContentPrvoider是如何反应的呢?
标准答案:一个content provider可以接受来自另外一个进程的数据请求。尽管ContentResolver与ContentProvider类隐藏了实现细节,但是ContentProvider所提供的query(),insert(),delete(),update()都是在ContentProvider进程的线程池中被调用执行的,而不是进程的主线程中。这个线程池是有Binder创建和维护的,其实使用的就是每个应用进程中的Binder线程池。

面试题:你觉得Android设计ContentProvider的目的是什么呢?
标准答案:1. 隐藏数据的实现方式,对外提供统一的数据访问接口;
2.更好的数据访问权限管理。ContentProvider可以对开发的数据进行权限设置,不同的URI可以对应不同的权限,只有符合权限要求的组件才能访问到ContentProvider的具体操作。
3.ContentProvider封装了跨进程共享的逻辑,我们只需要Uri即可访问数据。由系统来管理ContentProvider的创建、生命周期及访问的线程分配,简化我们在应用间共享数据(进程间通信)的方式。我们只管通过ContentResolver访问ContentProvider所提示的数据接口,而不需要担心它所在进程是启动还是未启动。

面试题:运行在主线程的ContentProvider为什么不会影响主线程的UI操作?
标准答案:
ContentProvider的onCreate()是运行在UI线程的,而query(),insert(),delete(),update()是运行在线程池中的工作线程的,所以调用这向个方法并不会阻塞ContentProvider所在进程的主线程,但可能会阻塞调用者所在的进程的UI线程!

所以,调用ContentProvider的操作仍然要放在子线程中去做。虽然直接的CRUD的操作是在工作线程的,但系统会让你的调用线程等待这个异步的操作完成,你才可以继续线程之前的工作。





参考博客:




源码地址:不知道为什么上传不了


  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值