Android 进阶——Framework 核心四大组件之跨进程共享组件ContentProvider 核心知识总结(一)

文章大纲

  • 引言
  • 一、ContentProvider 概述
  • 二、ContentProvider 机制的核心元素
    • 1、ContentProvider
    • 2、ContentResolver
    • 3、URI和Uri
      • 3.1、< scheme: >[scheme-specific-part] [#fragment]
      • 3.2、< scheme: >[//authority][path][ ?query ] [ #fragment ]
      • 3.3、< scheme: > [//host:port] [path] [?query] [#fragment]
      • 3.4、UriMatcher和ContentUris
      • 3.5、自定义的Uri和schema
    • 4、ContentValues
    • 5、ContentProvider 权限控制
  • 三、使用系统自带的内容提供者
  • 四、自定义ContentProvider
    • 1、创建自定义的ContentProvider
      • 1.1、继承ContentProvider
      • 1.2、定义ContentProvider特有的Uri
      • 1.3、在static 静态代码块中使用UriMatcher为Uri和返回码建立映射
      • 1.4、根据自己的业务需求实现具体的方法
      • 1.5、实现getType方法
      • 1.6、在清单文件声明注册自定义ContentProvider
    • 2、使用自定义ContentProvider
  • 五、ContentObserver 机制
    • 1、ContentObserver
    • 2、通过ContentObserver 监听ContentProvider的变化

引言

Android为数据存储提供了多种方式,大致主要有五种:文件、SharedPreferences、SQLite、网络、内容提供者ContentProvider,其中ContentProvider作为四大组件之一,最适于跨进程(跨APP)之间的数据共享,这一篇好好总结下ContentProvider,系列文章链接:

  • Android 进阶——Framework 核心四大组件之跨进程共享组件ContentProvider 核心知识总结(一)
  • Android 进阶——Framework 核心四大组件之跨进程共享组件ContentProvider 创建和启动机制源码详细分析(二
  • Android 进阶——Framework 核心四大组件之跨进程共享组件ContentProvider 工作机制源码详细分析(三)

一、ContentProvider 概述

内容提供者(ContentProvider)主要用于在不同的应用程序(进程)之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据且能确保被访数据的安全性,是Android实现跨程序(进程)共享数据的标准方式。不同于文件存储和SharedPreferences存储这两种全局可读可写操作模式,内容提供者把数据封装起来,并通过ContentResolver 提供统一操作接口,可以选择只对哪一部分数据进行共享(通过提供的接口),从而保证我们程序中的隐私数据不会泄露的风险,ContentProvider 底层通信基于Binder机制。

Note: A content provider is only required if you need to share data between multiple applications.

一般说来ContentProvider 来说ContentProvider 默认是单例的,因为默认情况清单文件里ContentProvider的android:multiprocess属性为false,若为true时则意味每一个进程中都存在一个ContentProvider对象。

二、ContentProvider 机制的核心元素

1、ContentProvider

ContentProvider 是Android 提供的用于对跨进程共享数据的封装接口的顶层抽象,通过内部类的形式封装了Binder和管道IO的操作,同时和其他组件一样实现了ComponentCallbacks2接口。内容提供者就是相当于一个跨应用的数据共享机制,通过内容提供者我们可以把系统内部原本私有的或者某个应用私有的数据向其他应用程序暴露共享。

2、ContentResolver

ContentResolver 是Android提供给应用访问内容的一系列操作(CRUDQ)接口的顶层抽象,通过内部类形式封装了Resource和Cursor,负责把ContentProvider 封装后的数据提供给应用程序。想要访问内容提供者中共享的数据,就一定要通过Context的getContentResolver()方法获取ContentResolver实例,然后根据对应的Uri 调用对应的CRUDQ操作。ContentResolver的操作语法有点和原生的SQLite 语法相似,只不过ContentResolver 不直接接受表名等参数,而是把参数封装为另一种形态——Uri。 即通过 ContentResolver实例来进行CRUDQ操作

方法 参数 说明
final Cursor query(Uri uri,String[] projection,String selection,String[] selectionArgs,String sortOrder ) 表示内容提供者的Uri(下同)、相当于SQL语句中select和from 之间的部分即查询的子句(不包括select关键字,例"name,_id")、相当于where字句后面的查询条件部分即where子句、如果参数值包含了参数,即用通配符?,selectionArgs则表示为将替代通配符的数据,例new String[]{“cmo”,“1”}、order by字句后的部分,例按_id降序排列"_id desc" 通过ContenResolver执行查询操作,query方法返回一个Cursor游标对象,这个对象和SQLite返回的对象完全一样,可以通过next()直接访问Cursor对象中的数据,也可以和CursorAdaperter结合使用。
final Uri insert(Uri uri,ContentValues values) 内容对应的Uri (下同)、键值对对象 通过ContenResolver执行插入操作,返回对应的Uri
final int delete(Uri uri,String[] projection,String selection,String[] selectionArgs) Uri 、select子句、where子句 通过ContenResolver执行插入操作
final int update(Uri uri,ContentValues values,String where,String[] selectionArgs ) Uri 、键值对对象、 通过ContenResolver执行插入操作
final void registerContentObserver(Uri uri, boolean notifyForDescendants,ContentObserver observer) 提供该内容的Uri、内容观察者 注册内容观察者,用于监听指定uri的内容变化
final void unregisterContentObserver(ContentObserver observer) 内容观察者 反注册前面注册的内容观察者
public final Uri insert(Uri uri,ContentValues values)·
/**
*ContentValues其实是一个Map扩展(其内部封装了一个HashMap),也是一个键值对对象
*/
//使用ContentValues和insert
ContentValues values=new ContentValues();
values.put("key","value");
values.put("键","值");
getContentResolver().insert(Uri.parse("Content://contentProvider"),values);

public final int delete(Uri uri,String[] projection,String selection,String[] selectionArgs)

public final int update(Uri uri,ContentValues values,String where,selectionArgs )

/**
query方法返回一个Cursor游标对象,这个对象和SQLite返回的对象完全一样,可以通过next()直接访问Cursor对象中的数据,也可以和CursorAdaperter结合使用。
@parm uri:表示内容提供者的Uri,例如收件箱的Uri.parse("content://sms/inbox")
@parm projection:相当于SQL语句中select和from 之间的的部分,即查询的字段,但是不包括Select关键字,例"name,_id"
@parm selection:相当于where字句后面的查询条件部分
@parm selectionArgs:如果参数值包含了参数,即用通配符?,selectionArgs则表示为将替代通配符的数据,例new String[]{"cmo","1"}
@parm sortOrder :即order by字句后的部分,例按_id降序排列"_id desc"
*/
public final Cursor query(Uri uri,String[] projection,String selection,String[] selectionArgs,String sortOrder )

CRUDQ操作默认不是线程安全的,所以如果存在多线程并发时需要做好线程同步。

3、URI和Uri

URI(Uniform Resource Identifier )和Uri严格来说并不是同一对象,URI 是Java类库中的一个不可继承的类,用于标识互联网资源唯一名称的字符串(按照约定的语法组织), 通过标识用户可以通过特定的协议查找到对应的资源并进行交互操作;而Uri是Android 模拟URI概念专门针对Android 系统定义的一个类似的抽象标识(Android系统里图像、音视频、网页等资源都可以用Uri来表示),语法和URI 类似主要有以下语法结构:

3.1、< scheme: >[scheme-specific-part] [#fragment]

其中< scheme: > 代表模式,有http、file、content、tel等,< >代表必需,而[ ]代表可选。

Uri uri = Uri.parse("tel:10010");
Uri uri = Uri.parse("smsto:0861119");
Uri uri = Uri.parse("mailto:[email protected]");

3.2、< scheme: >[//authority][path][ ?query ] [ #fragment ]

Uri uri = Uri.parse("http://www.google.com");

3.3、< scheme: > [//host:port] [path] [?query] [#fragment]

为了追求高性能Uri 没有对输入进行验证,所以即使无效输入也不会抛出异常。
其中有下面几个规则

  1. path可以有多个,每个用/连接,如 scheme://authority/path1/path2/path3?query#fragment

  2. query参数可以带有对应的值,也可以不带,如果带对应的值用=表示,如scheme://authority/path1/path2/path3?id = 1#fragment。它有一个参数id,它的值是1

  3. query参数可以有多个,每个用&连接,如scheme://authority/path1/path2/path3?id=1&name= mingming&old#fragment。它有三个参数及对应的值:id=1、name=mingming、 old=null(没有对它赋值,所以它的值是null)

  4. 在android中,除了scheme、authority是必须要有的,其它的几个path、query、fragment,它们每一个可以选择性的要或不要,但顺序不能变,比如:

  • 其中"path"可不要,如scheme://authority?query#fragment

  • 其中"path"和"query"可都不要,如scheme://authority#fragment

  • 其中"query"和"fragment"可都不要,如scheme://authority/path

  • “path”,“query”,"fragment"都不要,scheme://authority

在ContentProvider 机制中,Uri给内容提供者中的数据建立了唯一的标识符(可以简单认为Uri 和其所代表的资源文件存在一种一对一的“映射”关系),它主要由两部分组成:authority和path,其中authority是用于对不同的应用程序做区分的(类似权限,需要和清单文件声明的一致),为了避免冲突可以采用程序包名的方式来进行命名(比如包名为com.example.app,则对应的authority就可以命名为com.example.app.xxprovider);而path则是用于对同一应用程序中不同的表做区分的(通常添加到authority的后面)。再获取内容Uri字符串之后,需要调用Uri.parse(string)将它解析成Uri对象才可以作为参数传入。

//和原生的SQLiteDatabase中的CRUD语法类似
Cursor cursor = getContentResolver().query(final Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal)

3.4、UriMatcher和ContentUris

ContentUris主要是通过withAppendId方法为path添加id和parse方法从Uri中提取到id,

Uri uri=Uri.parse("content://com.example.app.provider/table1");
uri=ContentUris.withAppendedId(uri,99);//得到的uri为content://com.example.app.provider/table1/99
long id=ContentUris.parseId(uri);//99

而UriMatcher主要是用于匹配Uri和对应的返回码,相当于是提供给开发者快速判断Uri,主要是通过addURI()方法把Uri的authority,path和一个final 整形的返回码建立映射。当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,得到对应的返回值,我们就可以判断出调用方期望访问的是那个Uri对应的内容了。

private static UriMatcher uriMatcher;
static{
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI("com.example.app.provider","table1",11);
}
uriMatcher.match(uri);//11

因为所有的 CRUDQ 操作都一定要匹配到相应的内容 URi 格式才能进行的,而我们当然不可能向UriMatcher中添加隐私数据的Uri,所以这部分数据根本无法被外部程序访问到,自然安全问题也就不存在了。

3.5、自定义的Uri和schema

4、ContentValues

封装的键值对对象,内部基于HashMap进行存储。

//增加数据
ContentValues values = new ContentValues();
values.put("Column1","text");
values.put("Column2","1");
getContextResolver().insert(uri,values);
//删除数据
getContextResolver().delete(uri,"column2 = ?",new String[]{ "1" });

//更新数据
ContentValues values = new ContentValues();
values.put("Column1","改数据");
getContextResolver().update(uri,values,"column1 =  ? and column2 = ?",new String[]{"text","1"});

5、ContentProvider 权限控制

ContentProvider 把权限分为三种:读写权限只读权限写权限,分别对应清单文件provider下的子节点android:permission、android:writePermission、android:readPermission。

  • 在清单文件下的permission节点下定义该权限
<permission android:name="cmo.permission.providers.wr"
       android:protectionLevel="normal"
       android:label="wr"
       />
  • 在清单文件下的provider节点下配置该权限(根据情况选择配置哪种类型权限)
<provider
     android:authorities="ss"
     android:name="android.app.slice.SliceProvider"
     android:permission="cmo.permission.providers.wr"
     android:writePermission="cmo.permission.providers.w"
     android:readPermission="cmo.permission.providers.or"
     />

若已经配置了permission则不需要再配置只读、只写权限了。

  • 在使用这个provider的清单文件里的uses-permission节点下声明申请该权限
<uses-permission android:name="cmo.permission.providers.wr"/>

adb shell pm list permissions ——获取Android系统所有权限
adb -d shell pm list permissions ——获取USB Android设备所有权限
adb -s xxxx shell pm list permissions ——获取某一设备中的所有权限

三、使用系统自带的内容提供者

  • 查询源码或者其他资料找到对应的Uri

  • 通过上下文获取ContentResolver实例

  • 利用ContentResolver实例进行CRUDQ操作

package cmo.test.unittest;

import java.util.ArrayList;

import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.test.AndroidTestCase;
import android.util.Log;
/*
 * 单元测试类
 *@author cmo
 */
public class ContactTest extends AndroidTestCase{
    private static final String contactUri="";
    private static final String Tag="CONTACTS";
    //获取所有通讯录里的联系人的姓名、邮件、电话
    public void testGetContacs() throws Exception{

        //Uri uri=Uri.parse("content://com.android.contacts");//通过uri找到了联系人的内容提供者
        /*那么怎么通过Uri得到了内容提供者,而需要访问的数据则是通过路径来确定的,即是在静态代码块中添加的,一般都是把业务路径添加到
         * UriMatcher对象中的,再进行URI匹配,
        */
        Uri uri=Uri.parse("content://com.android.contacts/contacts");//加上了路径,这个URI就是获取所有的联系人raw_contacts的表的数据,但是真正的数据是存在data表的,所以raw_contacts我们只需要获取id去关联到data表即可
        ContentResolver resolver=getContext().getContentResolver();
        Cursor cursor=resolver.query(uri, new String[]{"_id"}, null, null, null);
        if(cursor!=null){
            while(cursor.moveToNext()){
                int contactsId=cursor.getInt(cursor.getColumnIndex("_id"));
                StringBuilder sb=new StringBuilder("contactsId=");
                sb.append(contactsId);

                uri=Uri.parse("content://com.android.contacts/contacts/"+contactsId+"/data");
                Cursor dataCursor=resolver.query(uri,new String[]{"mimetype","data1","data2"}, null,null,null);
                while(dataCursor.moveToNext()){
                    String data=dataCursor.getString(dataCursor.getColumnIndex("data1"));
                    String datatype=dataCursor.getString(dataCursor.getColumnIndex("mimetype"));
                    //String data=dataCursor.getString(dataCursor.getColumnIndex(""));
                    if("vnd.android.cursor.item/name".equals(datatype)){
                        //姓名
                        sb.append("name="+data);
                    }else if("vnd.android.cursor.item/email_v2".equals(datatype)){
                        //邮件
                        sb.append("email="+data);
                    }else if("vnd.android.cursor.item/phone_v2".equals(datatype)){
                        //电话
                        sb.append("phone="+data);
                    }
                }
                Log.d(Tag,sb.toString());
            }
        }
    }

    //根据指定电话号码获取联系人的姓名
    public void testGetContactNameByNum() throws Exception{
        String number="15676964983";
        Uri uri=Uri.parse("content://com.android.contacts/data/phones/filter/"+number);
        ContentResolver resolver=getContext().getContentResolver();
        Cursor cursor=resolver.query(uri, new String[]{"display_name"}, null, null, null);
        if(cursor.moveToNext()){
            String name=cursor.getString(0);
            Log.d(Tag,name);
        }
    }

    public void testAddNewContact() throws Exception{
        //分两步,第一步把id添加到raw_contacts保存联系人的id,第二部插入到data

        Uri uri=Uri.parse("content://com.android.contacts/raw_contacts");
        ContentResolver resolver=getContext().getContentResolver();
        ContentValues values=new ContentValues();//由于在raw——Contacts表中只需要插入id即可其他字段不需要插入
        resolver.insert(uri, values);//返回的是新插入数据的Uri形如content://com.android.contacts/row_contacts/1
        long contactid=ContentUris.parseId(resolver.insert(uri, values));//从Uri中截取id部分的值

        //添加姓名
        uri =Uri.parse("content://com.android.contacts/data");
        values.put("raw_contact_id", contactid);//外键值
        values.put("mimetype", "vnd.android.cursor.item/name");//数据类型,是姓名、电话还是其他
        values.put("data2", "新联系人姓名");//data1不用添加,因为系统会自动把姓和名的值连接起来保存到一起data1
        resolver.insert(uri, values);
        //添加电话
        values.clear();
        uri =Uri.parse("content://com.android.contacts/data");
        values.put("raw_contact_id", contactid);//外键值
        values.put("mimetype", "vnd.android.cursor.item/phone_v2");//数据类型,是姓名、电话还是其他
        values.put("data2", "2");//用于辨识号码是家庭、工作等
        values.put("data1", "18787878317");//真正保存电话号码的字段
        resolver.insert(uri, values);

        //添加邮件
        values.clear();
        uri =Uri.parse("content://com.android.contacts/data");
        values.put("raw_contact_id", contactid);//外键值
        values.put("mimetype", "vnd.android.cursor.item/email_v2");//数据类型,是姓名、电话还是其他
        values.put("data2", "2");//
        values.put("data1", "[email protected]");//真正保存电话号码的字段
        resolver.insert(uri, values);
    }

    //在内容提供者中:批量操作,使用事务
    public void testAddContactBtTranstion() throws Exception{
        Uri uri=Uri.parse("content://com.android.contacts/raw_contacts");
        ContentResolver resolver=getContext().getContentResolver();

        ArrayList<ContentProviderOperation> operlations=new ArrayList<ContentProviderOperation>();
        ContentProviderOperation addid=ContentProviderOperation.newInsert(uri)
        .withValue("account_name", null)//为谷歌账户account_name字段赋值
        .build();//得到一个针对添加功能操作对象,但是还没有执行
        operlations.add(addid);

        //添加姓名
        uri =Uri.parse("content://com.android.contacts/data");
        ContentProviderOperation addname=ContentProviderOperation.newInsert(uri)
                .withValueBackReference("raw_contact_id", 0)//怎么获取外键id?获取ArrayList[0]所对应的操作返回的结果
                .withValue("mimetype", "vnd.android.cursor.item/name")
                .withValue("data2", "新联系")
                .build();//得到一个针对添加功能操作对象,但是还没有执行
                operlations.add(addname);

        //添加电话
        uri =Uri.parse("content://com.android.contacts/data");
        ContentProviderOperation addphone=ContentProviderOperation.newInsert(uri)
                .withValueBackReference("raw_contact_id", 0)//怎么获取外键id?获取ArrayList[0]所对应的操作返回的结果
                .withValue("mimetype", "vnd.android.cursor.item/phone_v2")
                .withValue("data2", "2")
                .withValue("data1", "18989788823")
                .build();//得到一个针对添加功能操作对象,但是还没有执行
                operlations.add(addphone);  

        //添加电话
        uri =Uri.parse("content://com.android.contacts/data");
        ContentProviderOperation addmail=ContentProviderOperation.newInsert(uri)
                .withValueBackReference("raw_contact_id", 0)//怎么获取外键id?获取ArrayList[0]所对应的操作返回的结果
                .withValue("mimetype", "vnd.android.cursor.item/email_v2")
                .withValue("data2", "2")
                .withValue("data1", "[email protected]")
                .build();//得到一个针对添加功能操作对象,但是还没有执行
                operlations.add(addmail);           

        resolver.applyBatch("com.android.contacts", operlations);//执行批量操作
    }
}

四、自定义ContentProvider

完整例子见自定义ContentProvider。

1、创建自定义的ContentProvider


package cmo.learn.db;

import cmo.learn.service.DBHelper;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;

public class StudentProvider extends ContentProvider {
    private DBHelper dbhelper;
    private static final UriMatcher MATCHER=new UriMatcher(UriMatcher.NO_MATCH);//仅当Uri不匹配时返回的UriMatcher.NO_MATCH
    private static final int STUDENTSCODE=2;//操作所有Student
    private static final int STUDENT=3;//操作指定Student

    //自定义Uri,作用是仅当传入的Uri匹配才进行后续操作,即只有在static静态代码块中add了的Uri才是有效Uri并返回对应的操作码
    static{
        MATCHER.addURI("cmo.learn.providers.student", "Student", STUDENTSCODE);//若匹配成功之后返回操作码2
        MATCHER.addURI("cmo.learn.providers.student", "Student/#", STUDENT);//若匹配成功之后返回4,其中#代表所有数字,*代表所有字符
    }
    @Override
    public int delete(Uri arg0, String arg1, String[] arg2) {
        //提供给外部应用程序往内容提供者删除数据
        SQLiteDatabase db=dbhelper.getWritableDatabase();
        int recs=0;
        switch(MATCHER.match(arg0)){
        case 2:
            recs=db.delete("Student", arg1, arg2);
            break; 
        case 3:
            long rowid=ContentUris.parseId(arg0);
            String where ="_id="+rowid;
            if(arg1!=null && !"".equals(arg1.trim())){
                where +=" and "+arg1;
            }
            recs= db.delete("Student", arg1, arg2);
            break;
        default:
            throw new IllegalArgumentException("不合法的Uri: "+arg0);
        }
        return recs;
    }

    @Override
    public String getType(Uri arg0) {
        //返回目前要操作的Uri的内容类型例如操作的是文本文件 类型是plain/txt
        return null;
    }

    @Override
    public Uri insert(Uri arg0, ContentValues arg1) {
        //提供给外部应用程序往内容提供者插入数据
        SQLiteDatabase db=dbhelper.getWritableDatabase();
        switch(MATCHER.match(arg0)){
        case 2:
            long rowid=db.insert("Student", null, arg1);//主键值
            //返回新插入的数据的uri content://cmo.leanr.providers.student/Student/1
            //Uri newRecord=Uri.parse("content://cmo.leanr.providers.student/Student/"+rowid);
            Uri newRecord=ContentUris.withAppendedId(arg0,rowid);
            return newRecord;
        default:
            throw new IllegalArgumentException("不合法的Uri"+arg0);
        }
    }

    @Override
    public boolean onCreate() {
        dbhelper=new DBHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3,
            String arg4) {
            //提供给外部程序查询的方法
        SQLiteDatabase db=dbhelper.getReadableDatabase();
        int recs=0;
        switch(MATCHER.match(arg0)){
        case 2:
            return db.query("Student", arg1, arg2, arg3, null, null,arg4);
        case 3:
            long rowid=ContentUris.parseId(arg0);
            String where ="_id="+rowid;
            if(arg2!=null && !"".equals(arg2.trim())){
                where +=" and "+arg2;
            }
            return db.query("Student", arg1, where, arg3, null, null,arg4);
        default:
            throw new IllegalArgumentException("不合法的Uri: "+arg0);
        }
    }

    @Override
    public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
        //提供给外部应用程序往内容提供者更新数据
        SQLiteDatabase db=dbhelper.getWritableDatabase();
        int recs=0;
        switch(MATCHER.match(arg0)){
        case 2:
            recs=db.update("Student", arg1, arg2, arg3);
            break; 
        case 3:
            long rowid=ContentUris.parseId(arg0);
            String where ="_id="+rowid;
            if(arg2!=null && !"".equals(arg2.trim())){
                where +=" and "+arg1;
            }
            recs= db.update("Student", arg1, arg2, arg3);
            break;
        default:
            throw new IllegalArgumentException("不合法的Uri: "+arg0);
        }
        return recs;
    }
}

1.1、继承ContentProvider

继承ContentProvider类的方式来创建一个自己的内容提供器,ContentProvider类有6个抽象方法(CRUDQ、onCreate和getType):

方法 说明
onCreate() 初始化内容提供器时触发,可在这完成对数据库的创建和升级等操作,返回true表示初始化成功,false则失败。当且仅当存在ContentResolver尝试访问我们的程序中的数据时,内容提供器才会被初始化,要早于Application的onCreate方法执行。
query() 从内容提供器中查询数据,projection参数用于确定查询的哪一列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
insert() 向内容提供器中新增一条数据,待添加的数据保存在values参数中,添加完成后,返回一个用于表示这条新纪录的Uri。
update() 更新内容提供器中已有的数据,新数据保存着values参数当中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。
delete() 从内容提供器中删除数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
getType() 根据传入的内容Uri来返回相应的MIME类型。

CRUDQ 操作都是通过ContentProvider提供的接口完成的(根据Uri 参数来确定操作的表名)。

1.2、定义ContentProvider特有的Uri

标准格式一般为content://authority/path/param,其中authority作为其他应用识别该内容提供者的唯一标识,path则是用于对同一应用程序中不同的表做区分的,param是整形的话代表id,Uri具体含义需要配合UriMatcher来定义。以路径结尾就表示期望访问该表中所有的数据,以参数结果就表示期望访问该表中符合参数条件的数据。例如:

content://com.example.app.provider/table1  

表示调用方期望访问的是com.example.app这个应用的table1表中的所有数据。而这个path后加上一个id:

content://com.example.app.provider/table1/1

则表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据,除了id还可以使用通配符的方式来分别匹配两种格式的内容Uri

  • *——表示匹配任意长度的任意字符,如 content://com.example.app.provider/ * 匹配任意表的内容Uri

  • #——表示匹配任意长度的任意数字,content://com.example.app.provider/table1/# 匹配table表中任意一行的数据

1.3、在static 静态代码块中使用UriMatcher为Uri和返回码建立映射

1.4、根据自己的业务需求实现具体的方法

这里就是当使用这个自定义ContentProvider 时,通过ContentResolver 去进行CRUDQ操作时,真正去执行任务的逻辑。

1.5、实现getType方法

所有的内容提供器都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容Uri所对应的MIME字符串主要由3部分组成,格式约定如下:

  • 必须以vnd开头
  • 如果内容Uri以路径结尾,则后接android.cursor.dir/;若以id结尾,则后接android.cursor.item/。
  • 最后接vnd.< authority >.< path >

例如 content://com.example.app.provider/table1对应的MIME类型:

vnd.android.cursor.dir/vnd.com.example.app.provider.table1

而content://com.example.app.provider/table1/1所对应的MIME类型:

vnd.android.cursor.item/vnd.com.example.app.provider.table1 

1.6、在清单文件声明注册自定义ContentProvider

在manifest清单文件中的provider节点注册ContentProvider,最基本的是要声明name和authorities,其中authorities值设置为定义ContentProvider时的,这样子就完成了自定义ContentProvider的开发。

  • 声明了唯一标识为cmo.learn.providers.student的ContentProvider
  • 声明了访问这个ContentProvider必须具备的权限——cmo.permission.providers
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- 其他应用是根据authorities 来识别到这个组件的 -->
        <provider android:name=".StudentProvider" android:authorities="cmo.learn.providers.student"
        android:permission="cmo.permission.providers"
></provider>
    </application>

完成以上工作之后,就已经定义了一个主机名为”cmo.learn.providers.student“的自定义内容提供者。

2、使用自定义ContentProvider

  • 通过Uri.parse方法把Uri标识字符串转为Uri对象

  • 把Uri传入ContentResolver

  • 通过ContentResolver实例进行CURDQ。

package cmo.learn.test;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.test.AndroidTestCase;

public class StudentProviderTest extends AndroidTestCase {
    public void testProviderInsert() throws Exception{
        Uri uri=Uri.parse("content://cmo.learn.providers.student/Student");
        ContentResolver resoler=getContext().getContentResolver();
        ContentValues values=new ContentValues();
        values.put("name", "cmo8");
        values.put("sex", "boy");
        values.put("amount", String.valueOf(520));
        resoler.insert(uri, values);
    }
    public void testDele() throws Exception{
        Uri uri=Uri.parse("content://cmo.learn.providers.student/Student/3");
        ContentResolver resoler=getContext().getContentResolver();
        resoler.delete(uri, null, null);
    }
    public void testUpdate()throws Exception{
        Uri uri=Uri.parse("content://cmo.learn.providers.student/Student/1");
        ContentResolver resoler=getContext().getContentResolver();
        ContentValues values=new ContentValues();
        values.put("name", "cmo8");
        values.put("sex", "boy");
        resoler.update(uri, values, null, null);
    }
    public void testQuery()throws Exception{
        Uri uri=Uri.parse("content://cmo.learn.providers.student/Student/1");
        ContentResolver resoler=getContext().getContentResolver();
        Cursor cursor=resoler.query(uri, null, null, null, "_id asc");
        cursor.moveToFirst();
    }
}

五、ContentObserver 机制

ContentObserver 机制本质上是基于发布/订阅模型的观察者模式,是Android 提供给APP程序便捷监听内容变化并及时作出响应的机制。原理上是通过Binder通信,通过getContentService()获取了一个注册到ServiceManager中的ContentService的系统服务,进而获取observer的一个binder对象,并注册给ContentService,ContentResolver注册ContentPrvider是将一个Binder的回调对象注册到了SystemServer的ContentService中了,当ContentProvider内容改变时只需要向ContentService发送一个内容改变的通知即可,再通过Uri告诉ContentService那部分内容发生来改变,ContentService再向所有的注册该Uri ContentObserver的进程发送通知。简而言之,可以通过使用ContentResolver来操作ContentProvider提供的数据,同时注册ContentObserver监听Uri数据的变化

1、ContentObserver

Receives call backs for changes to content. Must be implemented by objects which are added to a ContentObservable.

ContentObserver顾名思义内容观察者,目的是观察指定关联的Uri引起的内容的变化,继而做一些相应的处理,效果与传统数据库中的触发器机制类似(Trigger),当ContentObserver所观察的Uri对应的内容发生变化时(即使新值和旧值是一样的)就会触发对应的回调。其中根据不同个的Uri 中的MIME Type 可以把触发器分为ContentObserver也分为“表“ContentObserver、“行”ContentObserver(类比于表触发器、行触发器)。既可以监听本应用的,也可以监听其他应用的内容变化。使用时通过ContentResolver实例调用registerContentObserver(Uri uri,boolean notifyForDescendants, ContentObserver observer)方法给Uri对应的内容添加上观察者,当内容变化时触发对应的回调onChange方法。

ContentObserver 还可以用于监听指定cursor指向的内容变化,通过Cursor#registerContentObserver(ContentObserver observer)方法。

2、通过ContentObserver 监听ContentProvider的变化

  • 创建初始化ContentObserver实例
  • 获取ContentResolver实例调用registerContentObserver方法
  • 使用完毕之后ContentResolver实例调用unregisterContentObserver反注册
//1.1
ContentObserver m4GContentObserver = new ContentObserver(mWorkHandler) {
     @Override
     public void onChange(boolean selfChange, Uri uri) {
         super.onChange(selfChange, uri);
         //改变时就会触发这个回调
         Log.i(TAG, "m4GContentObserver -->onChange:"+Thread.currentThread().getName() + uri.toString());
         try {
             reConnectClient(true);
         }catch (Exception e){
            Log.i(TAG, "m4GContentObserver -->onChange: Exception"+Thread.currentThread().getName()  +e.getMessage());
         }
     }
 };
//1.2        
mContext.getContentResolver().registerContentObserver(uri, true, m4GContentObserver);
//1.3
mContext.getContentResolver().unregisterContentObserver(m4GContentObserver);

篇幅问题,ContentProvider的原理放到下一篇,未完待续…

你可能感兴趣的:(Android,进阶,Android系统组件使用)