实现ContentProvider

一,写在前面
我们知道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 onCreateSQLiteOpenHelpergetWritableDatabase()SQLiteDatabaseSQLiteOpenHelperDbOpenHelpergetInstanceSQLiteOpenHelperonCreateonUpgradeMyContentProvideronCreate方法中获取了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等。

你可能感兴趣的:(ContentProvide)