一,写在前面
我们知道Android有四大组件,ContentProvider是其中之一,顾名思义:内容提供者。什么是内容提供者呢?一个抽象类,可以暴露应用的数据给其他应用。应用里的数据通常说的是数据库,事实上普通的文件,甚至是内存中的对象,也可以作为内容提供者暴露的数据形式。为什么要使用内容提供者呢?从上面定义就知道,内容提供者可以实现应用间的数据访问,一般是暴露表格形式的数据库中的数据。内容提供者的实现机制是什么呢?由于是实现应用间的数据通信,自然也是两个进程间的通信,其内部实现机制是Binder机制。那么,内容提供者也是实现进程间通信的一种方式。
事实上在开发中,很少需要自己写一个ContentProvider,一般都是去访问其他应用的ContentProvider。本篇文章之所以去研究如何自己写一个ContentProvider,也是为了更好的在开发中理解:如何访问其他应用的内容提供者。
二,实现一个ContentProvider
接下来介绍如何自己去实现一个内容提供者,大致分三步进行:
1,继承抽象类ContentProvider,重写onCreate,CUDR,getType六个方法;
2,注册可以访问内容提供者的uri
3,清单文件中配置provider
第一步,onCreate()方法中,获取SQLiteDatabase对象;CUDR方法通过对uri进行判断,做相应的增删改查数据的操作;getType方法是返回uri对应的MIME类型。
第二步,创建静态代码块,static{...code},在类加载的时候注册可以访问内容提供者的uri,使用类UriMatcher的addURI(...)完成。
第三步,注册内容提供者,加入authorities属性,对外暴露该应用的内容提供者。
直接上代码,应用B的MyContentProvider,如下:
public class MyContentProvider extends ContentProvider {
private DbOpenHelper helper;
private SQLiteDatabase db;
private static UriMatcher uriMatcher;
public static final String AUTHORITY = "com.example.mycontentprovider.wang";
public static final int CODE_PERSON = 0;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "person", CODE_PERSON);
}
@Override
public boolean onCreate() {
helper = DbOpenHelper.getInstance(getContext());
db = helper.getWritableDatabase();
//在数据库里添加一些数据
initData();
return true;
}
public void initData() {
for (int i = 0; i < 5; i++) {
ContentValues values = new ContentValues();
values.put("name", "kobe" + (i + 1));
values.put("age", 21 + i);
db.insert("person", null, values);
}
}
@Override
public String getType(Uri uri) {
return null;
}
public String getTableName(Uri uri) {
if (uriMatcher.match(uri) == CODE_PERSON) {
return "person";
} else {
//...
}
return null;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
String tableName = getTableName(uri);
if (tableName == null) {
throw new IllegalArgumentException("uri has not been added by urimatcher");
}
Cursor cursor = db.query(tableName, projection, selection, selectionArgs, null, null, null);
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
String tableName = getTableName(uri);
if (tableName == null) {
throw new IllegalArgumentException("uri has not been added by urimatcher");
}
db.insert(tableName, null, values);
//数据库中数据发生改变时,调用
getContext().getContentResolver().notifyChange(uri, null);
return uri;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
String tableName = getTableName(uri);
if (tableName == null) {
throw new IllegalArgumentException("uri has not been added by urimatcher");
}
int row = db.delete(tableName, selection, selectionArgs);
if (row > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return row;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
String tableName = getTableName(uri);
if (tableName == null) {
throw new IllegalArgumentException("uri has not been added by urimatcher");
}
int row = db.update(tableName, values, selection, selectionArgs);
if (row > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return row;
}
}
DbOpenHelper代码如下:
public class DbOpenHelper extends SQLiteOpenHelper {
public DbOpenHelper(Context context, String name, CursorFactory factory,
int version) {
super(context, name, factory, version);
}
private static DbOpenHelper helper;
public static synchronized DbOpenHelper getInstance(Context context) {
if (helper == null) {
//创建数据库
helper = new DbOpenHelper(context, "my_provider.db", null, 1);
}
return helper;
}
//创建表
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "create table person (_id integer primary key autoincrement, name Text, age integer)";
db.execSQL(sql);
}
//数据库升级时,回调该方法
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
在MyContentProvider o n C r e a t e 方 法 中 , 通 过 一 个 抽 象 帮 助 类 S Q L i t e O p e n H e l p e r 的 子 类 实 例 , 调 用 g e t W r i t a b l e D a t a b a s e ( ) 获 取 S Q L i t e D a t a b a s e 实 例 。 先 简 单 介 绍 下 S Q L i t e O p e n H e l p e r , D b O p e n H e l p e r 中 我 们 提 供 一 个 g e t I n s t a n c e 的 方 法 , 用 于 获 得 S Q L i t e O p e n H e l p e r 的 一 个 子 类 实 例 , 并 采 用 单 例 设 计 模 式 ; o n C r e a t e 方 法 : 创 建 数 据 库 的 表 , 且 可 以 创 建 多 个 表 ; o n U p g r a d e 方 法 : 在 数 据 库 版 本 发 生 改 变 时 , 该 方 法 被 回 调 , 可 以 加 入 修 改 表 的 操 作 的 代 码 。 在 M y C o n t e n t P r o v i d e r onCreate方法中,通过一个抽象帮助类SQLiteOpenHelper的子类实例,调用getWritableDatabase()获取SQLiteDatabase实例。先简单介绍下SQLiteOpenHelper,DbOpenHelper中我们提供一个getInstance的方法,用于获得SQLiteOpenHelper的一个子类实例,并采用单例设计模式;onCreate方法:创建数据库的表,且可以创建多个表;onUpgrade方法:在数据库版本发生改变时,该方法被回调,可以加入修改表的操作的代码。在MyContentProvider onCreate方法中,通过一个抽象帮助类SQLiteOpenHelper的子类实例,调用getWritableDatabase()获取SQLiteDatabase实例。先简单介绍下SQLiteOpenHelper,DbOpenHelper中我们提供一个getInstance的方法,用于获得SQLiteOpenHelper的一个子类实例,并采用单例设计模式;onCreate方法:创建数据库的表,且可以创建多个表;onUpgrade方法:在数据库版本发生改变时,该方法被回调,可以加入修改表的操作的代码。在MyContentProvideronCreate方法中获取了SQLiteDatabase实例就可以操作数据库,下面分析第二步的注册uri。
注册uri的目的就是确定哪些URI可以访问应用的数据,通常这些uri是由其他应用传递过来的,在后面访问uri的模块中会有所了解。UriMatcher可以用于注册uri,看起来就像一个容器,可以存储uri,还可以判断容器中是否有某一个uri。事实上,UriMatcher内部维护了一个ArrayList集合。查看UriMatcher的构造函数,代码如下:
public UriMatcher(int code)
{
mCode = code;
mWhich = -1;
mChildren = new ArrayList();
mText = null;
}
由此可见UriMatcher并不是一个什么陌生的东西,就是学习Java时接触到的ArrayList集合,只是将添加uri,判断uri的操作做了相应的封装。addURI(String authority,String path, int code),authority,path后面会讲到;code:与uri一一对应的int值,后面在判断uri是否添加到UriMatcher时,是先将该uri转化为code,再进行判断。
接下里分析CUDR操作,我们重写了这样四个方法:query,insert,delete,update,这个四个方法的参数都是想访问该应用的其他用户传递过来的,重点看uri。那么这个uri是如何构成的呢?uri = scheme + authorities + path。先看这样一个uri,
uri = “content://com.example.mycontentprovider.wang/a/b/c”,
scheme:"content://";
authorities:com.example.mycontentprovider.wang;authorities就是在清单文件中配置的authorities属性的值,唯一标识该应用的内容提供者。
path:/a/b/c;path里面常常放的是一些表名,字段信息,确定访问该数据库中哪个表的哪些数据,具体是访问哪些数据还要看CUDR对该uri做了怎样的操作。
在getTableName方法中,我们调用uriMatcher.match(uri)获取uri对应的code,如果该code没有注册过,则抛出异常IllegalArgumentException。也就是说,在其他应用访问本应用的内容提供者时,如果uri“不合法”,那么会抛出IllegalArgumentException异常。
然后调用SQLiteDatabase的query,insert,delete,update四个方法进行增删改查数据,值得一提的是,在增加,删除,修改数据后,需要调用内容解决者ContentResolver的notifyChange(uri,observer),通知数据发生改变。getType方法返回uri请求文件的MIME类型,这里返回null;
清单文件中注册provider代码如下:
authorities(也称,授权)属性必须指定相应的值,唯一标识该内容提供者,每个内容提供者的authorities的值都不同,它是访问的uri的一部分。
exported属性:若没有intent-filter,则默认false,不可访问;若有intent-filter,则默认true,可以访问。亦可手动设置
还可以添加权限属性,有兴趣的哥们可以自己去研究。以上就是自己写一个内容提供者的过程,分三步完成。下面展示另一个应用A,如何访问该应用的ContentProvider。
三,访问ContentProvider
应用A的代码,xml布局:
实体类Person代码如下:
package com.example.mcontentprovider.domain;
public class Person {
public int _id;
public String name;
public int age;
public Person() {
super();
}
public Person(int _id, String name, int age) {
super();
this._id = _id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
MainActivity代码如下:
public class MainActivity extends Activity implements OnClickListener {
private Button btn_add;
private Button btn_deleteAll;
private Button btn_query;
private Button btn_update;
private ContentResolver cr;
private static final String AUTHORITIES = "com.example.mycontentprovider.wang";
private MyContentObserver observer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
cr = getContentResolver();
observer = new MyContentObserver(new Handler());
cr.registerContentObserver(Uri.parse(uri), false, observer);
initView();
}
public void initView() {
btn_add = (Button) findViewById(R.id.btn_add);
btn_deleteAll = (Button) findViewById(R.id.btn_delete);
btn_query = (Button) findViewById(R.id.btn_query);
btn_update = (Button) findViewById(R.id.btn_update);
btn_add.setOnClickListener(this);
btn_deleteAll.setOnClickListener(this);
btn_query.setOnClickListener(this);
btn_update.setOnClickListener(this);
}
private String uri = "content://" + AUTHORITIES + "/person";
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_add:
new Thread(){
public void run() {
//休眠3秒,模拟异步任务
SystemClock.sleep(3000);
add();
};
}.start();
break;
case R.id.btn_delete:
Log.e("MainActivity", "删除名字为Tom的数据");
cr.delete(Uri.parse(uri), "name = ?", new String[]{"Tom"});
break;
case R.id.btn_query:
Cursor cursor = cr.query(Uri.parse(uri), null, null, null, null);
ArrayList persons = new ArrayList();
while (cursor.moveToNext()) {
int _id = cursor.getInt(0);
String name = cursor.getString(1);
int age = cursor.getInt(2);
persons.add(new Person(_id, name, age));
}
Log.e("MainActivity", persons.toString());
break;
case R.id.btn_update:
Log.e("MainActivity", "更改最后一条数据的name为paul");
ContentValues values2 = new ContentValues();
values2.put("name", "paul");
//获取数据库的行数
Cursor cursor2 = cr.query(Uri.parse(uri), null, null, null, null);
int count = cursor2.getCount();
cr.update(Uri.parse(uri), values2, "_id = ?", new String[]{count + ""});
break;
default:
break;
}
}
private void add() {
Log.e("MainActivity", "添加一条name为Tom,age为21的数据");
ContentValues values = new ContentValues();
values.put("name", "Tom");
values.put("age", 21);
cr.insert(Uri.parse(uri), values);
}
private class MyContentObserver extends ContentObserver {
public MyContentObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
Toast.makeText(getApplicationContext(), "数据改变啦!!!", 0).show();
super.onChange(selfChange);
}
}
}
在应用A中,我们设定uri = "content://" + AUTHORITIES + "/person",增删改查的操作对应都是该uri。事实上,只要内容提供者注册了的uri都可以访问,这里暂且让uri都相同。有兴趣的哥们可以尝试一下,若uri不合法,确实会抛出IllegalArgumentException异常。在实际开发中,最重要的是寻找到需要的uri,然后进行CUDR操作,如何进行CUDR操作不是本篇重点,不做讲解。
注意到代码里添加数据时,这里创建了一个线程,使线程休眠了3s,用于模拟添加大量数据时的异步操作。同时注册了一个内容观察者用于监听数据变化,cr.registerContentObserver(Uri.parse(uri), false, observer)。第一个参数:监听的uri。第二个参数:若为true,表示以该uri字串为开头的uri都可以监听;若为false,表示只能监听该uri。第三个参数:ContentObserver子类实例,数据发生改变时回调onChange方法。
执行点击操作,查看log。
查询;
添加->查询;(在点击添加按钮后,过了3秒左右,弹出toast,显示"数据改变啦!!!")
删除->查询;
更改->查询;
log如下:
这里解释下,在添加数据时,为何模拟异步操作。有这样一个场景:当数据添加进内容提供者的数据库中后,才可以执行某一个操作。那么onChange方法被回调时,就是一个很好的时机去执行某一个操作。
可能有的哥们要问:在应用A中调用了ContentResolver的CUDR方法,那么怎么应用B中数据库的数据为何能变化呢?表面上可以这样理解:应用A在调用ContentResolver的CUDR方法时,会使应用B中对应的CUDR方法被调用,而uri则是应用A传递给应用B的。而为何“会使应用B中对应的CUDR方法被调用”,但是是Binder机制实现的。包括被回调的onChange方法也是Binder机制才能实现的,试想数据增删改查操作是在应用B完成的,为何在应用B中调用notifyChange方法通知数据改变后,应用A的onChange方法能被回调。
侃了这么多,拿代码来点一下,查看ContentResolver$notifyChange源码如下:
public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork,
int userHandle) {
try {
getContentService().notifyChange(
uri, observer == null ? null : observer.getContentObserver(),
observer != null && observer.deliverSelfNotifications(), syncToNetwork,
userHandle);
} catch (RemoteException e) {
}
}
继续查看ContentResolver$getContentService方法:
public static IContentService getContentService() {
if (sContentService != null) {
return sContentService;
}
IBinder b = ServiceManager.getService(CONTENT_SERVICE_NAME);
if (false) Log.v(“ContentService”, "default service binder = " + b);
sContentService = IContentService.Stub.asInterface(b);
if (false) Log.v(“ContentService”, "default service = " + sContentService);
return sContentService;
}
sContentService不就是代理对象么,调用代理对象的notifyChange(…)方法:内部会调用transact方法向服务发起请求;然后onTransact(…)被调用,会调用IContentService接口的notifyChange方法完成通信。接口IContentService中方法的重写是在extends IContentService.Stub的类中,也就是ContentService。
四,另外
好了,上面只是简单点了一下,说明ContentProvider暴露数据给其他应用访问,内部就是Binder机制原理实现的。常用进程间通信方式有:AIDL,ContentProvider,Messenger等。