Android四大组件之ContentProvider 全面解析,ContentResolver源码解析如何调用其它APP的ContentProvider
今天来总结下Android中的ContentProvider(以下简称CP),具体代码请见https://github.com/Mangosir/ContentProviderReview/tree/master
概述
其实相对于Acivity,Service,BroadcastReciver这三个组件,CP的使用率还是相对比较少的,除非你开发的app是要采用手机本地资源或跟其它app共享资源的。从这个使用目的来看好像ContentProvider是用来进行进程间通信的,确实,android中IPC的六种方式之一就是使用CP,好吧,扯远了,那就走上正轨,先来看看官网介绍:
CP管理对结构化数据集的访问。它们封装数据,并提供用于定义数据安全性的机制。 CP是连接一个进程中的数据与另一个进程中运行的代码的标准界面。
如果您想要访问CP中的数据,可以将应用的 Context 中的 ContentResolver 对象用作客户端来与CP通信。ContentResolver 对象会与CP对象(即实现 ContentProvider 的类实例)通信。CP对象从客户端接收数据请求,执行请求的操作并返回结果。
如果您不打算与其他应用共享数据,则无需开发自己的CP。 不过,您需要通过自己的CP在您自己的应用中提供自定义搜索建议。 如果您想将复杂的数据或文件从您的应用复制并粘贴到其他应用中,也需要创建您自己的提供程序。
Android 本身包括的CP可管理音频、视频、图像和个人联系信息等数据。 android.provider 软件包参考文档中列出了部分提供程序。 任何Android 应用都可以访问这些提供程序,但会受到某些限制。
简介
CP是Android四大组件之一,为存储数据和获取数据提供统一的接口,可以在不同的应用程序间进行数据共享(IPC方式之一)。CP是以类似于数据库中的表的形式来组织数据的,无论数据来源是什么,CP都可以被认为是一张表,它把数据组装成一张表格来被外界使用;它对外提供了一下几个方法
onCreate() 实现这个方法在启动时初始化你的CP
insert(Uri uri,ContentValues values) 实现这个方法处理一个请求插入新的一行
query(Uri uri, String[]projection, String selection, String[] selectionArgs, String sortOrder)
实现这个方法去处理来自客户端的查询请求,支持取消操作
update(Uri uri,ContentValues values, String selection, String[] selectionArgs)
实现这个方法,处理更新一行或者多行的请求
delete(Uri uri, Stringselection, String[] selectionArgs)
实现这个方法,处理删除一行或者更多行的请求
getType(Uri uri) 实现这个方法处理指定uri的数据的MIME类型
从这些方法也可以看出对CP的操作与对数据库的操作类似。
使用原因
1.其中一个原因上面也说了,就是进程间通信的一种方式;
2.CP提供了对底层数据存储方式的抽象,不管底层使用Sqlite数据库还是MongoDB,被CP封装后给应用层开发者提供的数据接口不变
3.CP为应用程序间的数据共享提供了一个安全的环境,它允许你把自己想要开放的数据提供给其它应用进行增删改差,而不需要担心开放数据库权限所带来的安全问题
使用CP
在讲解之前先看下谷歌官方文档怎么介绍的(地址是在线API)
大致翻译下,可能不准确,英文有点渣
CP是Android应用程序的主要构建模块之一,为应用程序提供内容。CP封装数据并通过单个ContentResolver接口提供给应用程序。
当你需要在多个应用程序之间分享数据的时候就可以用到CP。比如,手机通讯录数据被多个应用程序使用并且只能存储在CP中。如果您不需要在多个应用程序间分享数据,你可以通过SqliteDatabase使用数据库。
当通过ContentResolver发出请求时,系统会检查给定的URI的权限,并将请求传递给注册该权限的CP。CP也可以解析其它的URI,UriMatcher类有助于解析URI。
数据访问方法(比如insert和update)可能被多个线程同时调用,所以必须是线程安全的。其它方法(比如onCreate)只能由应用程序主线程调用,同时避免执行冗长操作。参考方法描述以了解它们的线程特性。
对ContentResolver的请求会自动转发到合适的CP实例,所以子类不需要担心跨进程调用的细节。
在这里我们了解到有一个ContentResolver是作为客户端发请求到ContentProvider来访问其中的数据。那通过啥媒介来访问请求呢,没错就是URI:
Content URI是用于在CP中标识数据的URI,它包括整个CP的符号名称(授权)和一个指向表或者文件的名称(路径),后面追加可选ID表示表中具体哪一行。模式如下 content://<authority>/<path>/<id>。CP每一个数据访问方法都将URI作为传参,通过它来确定要访问的表,行或文件。
设计授权:CP通常具有单一授权,该授权是它在应用程序内的名称,为避免与其它应用程序的CP冲突,通常我们将CP所在应用程序的包名的扩展名来作为授权,比如content://com.mango.test.provider
设计路径结构:我们通常通过追加指向单个表的路径来创建URI,比如我有一个表table1,这URI是content://com.mango.test.provider/table1
处理URI可选ID:ContentProvider将ID值与表的_ID列进行匹配,并对匹配的行执行请求的访问,比如content://com.mango.test.provider/table1/6
讲到URI,Android提供了一个类UriMatcher,主要是在CP用来匹配Uri的,当我们通过CP插入一条数据的时候,先用UriMatcher进行匹配,假如是系统的ContentProvider,比如联系人,这些系统提供了相应的Uri,我们就可以根据系统提供的Uri来操作数据。
这个类提供了三个开放方法,还有两个匹配模式
在路径后添加# 比如content://com.mango.test.provider/table1/# 这是匹配由任意长度的数字字符组成的字符串(content://com.mango.test.provider/table1/11 就可以匹配成功)
在路径后面添加* 比如content://com.mango.test.provider/table1/* 这是匹配由任意长度的任何有效字符组成的字符串
(content://com.mango.test.provider/table1/k2 就可以匹配成功)
接下来我们就写个在一个APP里去对另一个APP的ContentProvider进行增删改查人员的操作的Demo。
新建一个类去继承ContentProvider
public class PeopleContentProvider extends ContentProvider { //AUTHORITY_APP要与xml注册文件里写的保持一致 private static final String AUTHORITY_APP = "com.mangoer.review"; //匹配成功返回的匹配码 private static final int MATCH_ALL_CODE = 100; private static final int MATCH_ONE_CODE = 101; //Uri匹配检查的类 private static UriMatcher uriMatcher; //数据库操作实例 private SQLiteDatabase db; //创建数据库的辅助类 private DBHelper openHelper; private Cursor cursor = null; //数据改变后指定通知的Uri private static final Uri NOTIFY_URI = Uri.parse("content://" + AUTHORITY_APP + "/student"); //在静态代码块中添加要匹配的 Uri static { //入参是匹配不成功时返回NO_MATCH(-1) uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); /** * uriMatcher.addURI(authority, path, code); 其中 * authority:主机名(用于唯一标示一个ContentProvider,这个需要和清单文件中的authorities属性相同) * path:路径路径(可以用来表示我们要操作的数据,路径的构建应根据业务而定) * code:返回值(用于匹配uri的时候,作为匹配成功的返回值) */ uriMatcher.addURI(AUTHORITY_APP, "student", MATCH_ALL_CODE);// 匹配记录集合 uriMatcher.addURI(AUTHORITY_APP, "student/#", MATCH_ONE_CODE);// 匹配单条记录 } @Override public boolean onCreate() { //创建数据库 openHelper = new DBHelper(getContext()); db = openHelper.getWritableDatabase(); return false; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { switch (uriMatcher.match(uri)) { /** * 这里如果匹配是uriMatcher.addURI(AUTHORITY_APP, "student", * MATCH_SUCCESS_CODE)中的Uri,则我们可以在这里对这个ContentProvider中的数据库 * 进行删除等操作。这里如果匹配成功,我们将删除所有的数据 */ case MATCH_ALL_CODE: int count=db.delete("personData", null, null); if(count>0){ notifyDataChanged(); return count; } break; /** * 这里如果匹配是uriMatcher.addURI(AUTHORITY_APP, * "student/#",MATCH_ONE_CODE);中的Uri,则说明我们要操作单条记录 */ case MATCH_ONE_CODE: // 这里可以做删除单条数据的操作。 break; default: throw new IllegalArgumentException("Unkwon Uri:" + uri.toString()); } return 0; } @Override public String getType(Uri uri) { return null; } /** * 插入 使用UriMatch的实例中的match方法对传过来的 Uri进行匹配。 这里通过ContentResolver传过来一个Uri, * 用这个传过来的Uri跟在ContentProvider中静态代码块中uriMatcher.addURI加入的Uri进行匹配 * 根据匹配的是否成功会返回相应的值,在上述静态代码块中调用uriMatcher.addURI(AUTHORITY_APP, * "student",MATCH_CODE)这里的MATCH_CODE * 就是匹配成功的返回值,也就是说假如返回了MATCH_CODE就表示这个Uri匹配成功了 * ,我们就可以按照我们的需求就行操作了,这里uriMatcher.addURI(AUTHORITY_APP, * "person/data",MATCH_CODE)加入的Uri为: * content://com.example.studentProvider/student * ,如果传过来的Uri跟这个Uri能够匹配成功,就会按照我们设定的步骤去执行相应的操作 */ @Override public Uri insert(Uri uri, ContentValues values) { int match=uriMatcher.match(uri); if(match!=MATCH_ALL_CODE){ throw new IllegalArgumentException("Unkwon Uri:" + uri.toString()); } long rawId = db.insert("personData", null, values); Uri insertUri = ContentUris.withAppendedId(uri, rawId); if(rawId>0){ notifyDataChanged(); return insertUri; } return null; } /** * 查询 如果uri为 * content://com.example.studentProvider/student则能匹配成功,然后我们可以按照需求执行匹配成功的操作 */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { switch (uriMatcher.match(uri)) { /** * 如果匹配成功,就根据条件查询数据并将查询出的cursor返回 */ case MATCH_ALL_CODE: cursor = db.query("personData", null, null, null, null, null, null); break; case MATCH_ONE_CODE: // 根据条件查询一条数据。。。。 break; default: throw new IllegalArgumentException("Unkwon Uri:" + uri.toString()); } return cursor; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { switch (uriMatcher.match(uri)) { case MATCH_ONE_CODE: long age = ContentUris.parseId(uri); selection = "age = ?"; selectionArgs = new String[] { String.valueOf(age) }; int count = db.update("personData", values, selection,selectionArgs); if(count>0){ notifyDataChanged(); } break; case MATCH_ALL_CODE: // 如果有需求的话,可以对整个表进行操作 break; default: throw new IllegalArgumentException("Unkwon Uri:" + uri.toString()); } return 0; } //通知指定URI数据已改变 private void notifyDataChanged() { getContext().getContentResolver().notifyChange(NOTIFY_URI, null); } }然后在AndroidManifest.xml中注册ContentProvider
<provider android:name=".provider.PeopleContentProvider" android:authorities="com.mangoer.review" android:exported="true"> </provider>
这里的authorities是这个ContentProvider唯一标识,这样别的应用就可以找到这个CP了
exported为true表示允许别的应用访问
内容提供者已经写好了,现在在另外一个APP里写一个内容访问者
public class MainActivity extends Activity implements OnClickListener { private ContentResolver contentResolver; private ListView lvShowInfo; private MyAdapter adapter; private Button btnInit; private Button btnInsert; private Button btnDelete; private Button btnUpdate; private Button btnQuery; private Cursor cursor; private static final String AUTHORITY = "com.mangoer.review"; private static final Uri STUDENT_ALL_URI = Uri.parse("content://" + AUTHORITY + "/student"); private Handler handler=new Handler(){ public void handleMessage(android.os.Message msg) { //当ContentProvider有数据更新推送过来的时候,我们可以在此做一些操作 // 比如Adapter.notifyDataSetChanged() cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null); adapter.changeCursor(cursor); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); lvShowInfo=findViewById(R.id.lv_show_info); initData(); } private void initData() { btnInit=findViewById(R.id.btn_init); btnInsert=findViewById(R.id.btn_insert); btnDelete=findViewById(R.id.btn_delete); btnUpdate=findViewById(R.id.btn_update); btnQuery=findViewById(R.id.btn_query); btnInit.setOnClickListener(this); btnInsert.setOnClickListener(this); btnDelete.setOnClickListener(this); btnUpdate.setOnClickListener(this); btnQuery.setOnClickListener(this); contentResolver = getContentResolver(); //注册内容观察者 contentResolver.registerContentObserver(STUDENT_ALL_URI,true,new PersonOberserver(handler)); adapter=new MyAdapter(MainActivity.this,cursor); lvShowInfo.setAdapter(adapter); } @Override public void onClick(View v) { switch (v.getId()) { //初始化 case R.id.btn_init: ArrayList<Student> students = new ArrayList<>(); Student student1 = new Student("苍老师",25,"好老师"); Student student2 = new Student("柳岩",26,"好球"); Student student3 = new Student("杨幂",27,"大幂幂"); Student student4 = new Student("陈冠希",28,"拍的一手好片"); students.add(student1); students.add(student2); students.add(student3); students.add(student4); for (Student Student : students) { ContentValues values = new ContentValues(); values.put("name", Student.getName()); values.put("age", Student.getAge()); values.put("msg", Student.getMsg()); contentResolver.insert(STUDENT_ALL_URI, values); } break; //增 case R.id.btn_insert: Student student = new Student("小明", 26, "帅气男人"); //实例化一个ContentValues对象 ContentValues insertContentValues = new ContentValues(); insertContentValues.put("name",student.getName()); insertContentValues.put("age",student.getAge()); insertContentValues.put("msg",student.getMsg()); //这里的uri和ContentValues对象经过一系列处理之后会传到ContentProvider中的insert方法中, //在我们自定义的ContentProvider中进行匹配操作 contentResolver.insert(STUDENT_ALL_URI,insertContentValues); break; //删 case R.id.btn_delete: //删除所有条目 contentResolver.delete(STUDENT_ALL_URI, null, null); //删除_id为1的记录 Uri delUri = ContentUris.withAppendedId(STUDENT_ALL_URI,1); contentResolver.delete(delUri, null, null); break; //改 case R.id.btn_update: ContentValues contentValues = new ContentValues(); contentValues.put("msg","性感"); //更新数据,将age=26的条目的msg更新为"性感",原来age=26的introduce为"大方". //生成的Uri为:content://com.mangoer.review/student/26 Uri updateUri = ContentUris.withAppendedId(STUDENT_ALL_URI,26); contentResolver.update(updateUri,contentValues, null, null); break; //查 case R.id.btn_query: //通过ContentResolver获得一个调用ContentProvider对象 Cursor cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null); //CursorAdapter的用法,参考此博客:http://blog.****.net/dmk877/article/details/44983491 adapter=new MyAdapter(MainActivity.this,cursor); lvShowInfo.setAdapter(adapter); cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null); adapter.changeCursor(cursor); break; } } }
在初始化的时候我们用ContentResolver注册了一个ContentOberServer:
直译也就是内容观察者,观察指定的Uri引起的数据库的变化,然后通知主线程,根据需求做我们想要做的处理。这样说大家可能理解的不是特别透彻,这样跟大家说它可以实现类似于Adapter的notifyDataSetChanged()这个方法的作用,比方说当观察到ContentProvider的数据变化时会自动调用谷歌工程师给我们提供的好的方法,可以在此方法中通知主线程数据改变等等。那么问题来了,应该怎样实现这样的功能呢?首先要做的就是注册这个观察者,这里的注册是在需要监测ContentProvider的应用中进行注册而不是在ContentProvider中,而在ContentProvider中要做的就是当数据变化时进行通知,这里的通知的方法谷歌已经帮我们写好,直接调用就行了,查看谷歌文档你会发现在ContentResolver中有这样的介绍:
public final void registerContentObserver(Uri uri, boolean notifyForDescendents, ContentObserver observer)
注册一个观察者实例,当指定的Uri发生改变时,这个实例会回调实例对象做相应处理。
参数:uri:需要观察的Uri
notifyForDescendents:如果为true表示以这个Uri为开头的所有Uri都会被匹配到,
如果为false表示精确匹配,即只会匹配这个给定的Uri。
举个例子,假如有这么几个Uri:
①content://com.mangoer.review/student
②content://com.mangoer.review/student/#
③content://com.mangoer.review/student/10
④content://com.mangoer.review/student/teacher
假如观察的Uri为content://com.example.studentProvider/student,当notifyForDescendents为true时则以这个Uri开头的Uri的数据变化时都会被捕捉到,在这里也就是①②③④的Uri的数据的变化都能被捕捉到,当notifyForDescendents为false时则只有①中Uri变化时才能被捕捉到。
看到registerContentObserver这个方法,根据语言基础我想大家能够想到ContentResolver中的另一个方法
public finalvoidunregisterContentObserver(ContentObserverobserver)
它的作用就是取消对注册的那个Uri的观察,这里传进去的就是在registerContentObserver中传递进去的ContentObserver对象。到这关于注册和解除注册的ContentObserver可能大家都比较清楚了,那么问题来了,怎么去写一个ContentObserver呢?其实它的实现很简单,直接创建一个类继承ContentObserver需要注意的是这里必须要实现它的构造方法
public ContentObserver(Handlerhandler)
这里传进去的是一个Handler对象,这个Handler对象的作用一般要依赖于ContentObserver的另一个方法即
public void onChange(boolean selfChange)
这个方法的作用就是当指定的Uri的数据发生变化时会回调该方法,此时可以借助构造方法中的Handler对象将这个变化的消息发送给主线程,当主线程接收到这个消息之后就可以按照我们的需求来完成相应的操作
调用过程源码解析
源码分析由于篇幅太长,就不在这里继续了,放到下一篇分析ContentResolver与ContentProvider的联系之源码解析