由于之前主要做手机游戏相关的开发,所以ContentProvider了解的不多,今天就来学习一下。
1. 首先来了解一下ContentProvider是什么?它的作用是什么?
ContentProvider是Android的四大组件之一,可见它在Android中的作用非同小可。它主要的作用是:实现各个应用程序之间的(跨应用)数据共享,比如联系人应用中就使用了ContentProvider,你在自己的应用中可以读取和修改联系人的数据,不过需要获得相应的权限。其实它也只是一个中间人,真正的数据源是文件或者SQLite等。
2. 那ContentProvider是怎么实现数据共享的呢?
下面先来了解一下这几个东东:
(1) URI
URI:统一资源标识符,代表要操作的数据,可以用来标识每个ContentProvider,这样你就可以通过指定的URI找到想要的ContentProvider,从中获取或修改数据。在Android中URI的格式如下图所示(图片来自于网络):

主要分三个部分:scheme, authority and path。scheme表示上图中的content://,authority表示B部分,path表示C和D部分。
A部分:表示是一个Android内容URI,说明由ContentProvider控制数据,该部分是固定形式,不可更改的。
B部分:是URI的授权部分,是唯一标识符,用来定位ContentProvider。格式一般是自定义ContentProvider类的完全限定名称,注册时需要用到,如:com.alexzhou.provider.NoteProvider
C部分和D部分:是每个ContentProvider内部的路径部分,C和D部分称为路径片段,C部分指向一个对象集合,一般用表的名字,如:/notes表示一个笔记集合;D部分指向特定的记录,如:/notes/1表示id为1的笔记,如果没有指定D部分,则返回全部记录。
在开发中通常使用常量来定义URI,如下面的CONTENT_URI:

public static final String AUTHORITY = "com.alexzhou.provider.NoteProvider";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/notes");

(2) MIME
MIME是指定某个扩展名的文件用一种应用程序来打开,就像你用浏览器查看PDF格式的文件,浏览器会选择合适的应用来打开一样。Android中的工作方式跟HTTP类似,ContentProvider会根据URI来返回MIME类型,ContentProvider会返回一个包含两部分的字符串。MIME类型一般包含两部分,如:
text/html
text/css
text/xml
application/pdf
等,分为类型和子类型,Android遵循类似的约定来定义MIME类型,每个内容类型的Android MIME类型有两种形式:多条记录(集合)和单条记录。
多条记录
vnd.android.cursor.dir/vnd.alexzhou.note
单条记录
vnd.android.cursor.item/vnd.alexzhou.note
vnd表示这些类型和子类型具有非标准的、供应商特定的形式。Android中类型已经固定好了,不能更改,只能区别是集合还是单条具体记录,子类型vnd.之后的内容可以按照格式随便填写。在使用Intent时,会用到MIME这玩意,根据Mimetype打开符合条件的活动。

3. 接下来通过一个简单的demo,来学习怎么创建自定义的ContentProvider,这里数据源选用SQLite,最常用的也是这个。
(1) 创建一个类NoteContentProvider,继承ContentProvider,需要实现下面5个方法:
query
insert
update
delete
getType
这些方法是eclipse自动生成的,暂时先不动他们。

/**
author:alexzhou 
email :zhoujiangbohai@163.com
date  :2013-2-25
 **/

public class NoteContentProvider extends ContentProvider {

	@Override
	public boolean onCreate() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public String getType(Uri uri) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		// TODO Auto-generated method stub
		return 0;
	}

}

(2)先来设计一个数据库,用来存储笔记信息,主要包含_ID,title,content,create_date四个字段。创建NoteProviderMetaData类,封装URI和数据库、表、字段相关信息,源码如下:

/**
author:alexzhou 
email :zhoujiangbohai@163.com
date  :2013-2-25
 **/

public class NoteProviderMetaData {

	public static final String AUTHORITY = "com.alexzhou.provider.NoteProvider";

	public static final String DATABASE_NAME = "note.db";
	public static final int DATABASE_VERSION = 1;

	/**
	 * 
	 *数据库中表相关的元数据
	 */
	public static final class NoteTableMetaData implements BaseColumns {

		public static final String TABLE_NAME = "notes";
		public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/notes");
		public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.alexzhou.note";
		public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.alexzhou.note";

		public static final String NOTE_TITLE = "title";
		public static final String NOTE_CONTENT = "content";
		public static final String CREATE_DATE = "create_date";

		public static final String DEFAULT_ORDERBY = "create_date DESC";

		public static final String SQL_CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
										+ _ID + " INTEGER PRIMARY KEY,"
										+ NOTE_TITLE + " VARCHAR(50),"
										+ NOTE_CONTENT + " TEXT,"
										+ CREATE_DATE + " INTEGER"
										+ ");" ;
	}
}

AUTHORITY代表授权,该字符串和在Android描述文件AndroidManifest.xml中注册该ContentProvider时的android:authorities值一样,NoteTableMetaData继承BaseColumns,后者提供了标准的_id字段,表示行ID。
(3) ContentProvider是根据URI来获取数据的,那它怎么区分不同的URI呢,因为无论是获取笔记列表还是获取一条笔记都是调用query方法,现在来实现这个功能。需要用到类UriMatcher,该类可以帮助我们识别URI类型,下面看实现源码:

	private static final UriMatcher sUriMatcher;
	private static final int COLLECTION_INDICATOR = 1;
	private static final int SINGLE_INDICATOR = 2;

	static {
		sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
		sUriMatcher.addURI(NoteProviderMetaData.AUTHORITY, "notes", COLLECTION_INDICATOR);
		sUriMatcher.addURI(NoteProviderMetaData.AUTHORITY, "notes/#", SINGLE_INDICATOR);
	}

这段代码是NoteContentProvider类中的,UriMatcher的工作原理:首先需要在UriMatcher中注册URI模式,每一个模式跟一个唯一的编号关联,注册之后,在使用中就可以根据URI得到对应的编号,当模式不匹配时,UriMatcher将返回一个NO_MATCH常量,这样就可以区分了。
(4) 还需为查询设置一个投影映射,主要是将抽象字段映射到数据库中真实的字段名称,因为这些字段有时是不同的名称,既抽象字段的值可以不跟数据库中的字段名称一样。这里使用HashMap来完成,key是抽象字段名称,value对应数据库中的字段名称,不过这里我把两者的值设置是一样的,在NoteContentProvider.java中添加如下代码。

	private static HashMap<String, String> sNotesProjectionMap;
	static {
		sNotesProjectionMap = new HashMap<String, String>();
		sNotesProjectionMap.put(NoteProviderMetaData.NoteTableMetaData._ID, NoteProviderMetaData.NoteTableMetaData._ID);
		sNotesProjectionMap.put(NoteProviderMetaData.NoteTableMetaData.NOTE_CONTENT, NoteProviderMetaData.NoteTableMetaData.NOTE_CONTENT);
		sNotesProjectionMap.put(NoteProviderMetaData.NoteTableMetaData.NOTE_TITLE, NoteProviderMetaData.NoteTableMetaData.NOTE_TITLE);
		sNotesProjectionMap.put(NoteProviderMetaData.NoteTableMetaData.CREATE_DATE, NoteProviderMetaData.NoteTableMetaData.CREATE_DATE);
	}

(5) 在NoteContentProvider.java中创建一个内部类DatabaseHelper,继承自SQLiteOpenHelper,完成数据库表的创建、更新,这样可以通过它获得数据库对象,相关代码如下。

	private DatabaseHelper mDbHelper;

	private static class DatabaseHelper extends SQLiteOpenHelper {

		public DatabaseHelper(Context context) {
			super(context, NoteProviderMetaData.DATABASE_NAME, null, NoteProviderMetaData.DATABASE_VERSION);
		}

		@Override
		public void onCreate(SQLiteDatabase db) {
			Log.e("DatabaseHelper", "create table: " + NoteProviderMetaData.NoteTableMetaData.SQL_CREATE_TABLE);
			db.execSQL(NoteProviderMetaData.NoteTableMetaData.SQL_CREATE_TABLE);
		}

		@Override
		public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
			db.execSQL("DROP TABLE IF EXISTS " + NoteProviderMetaData.NoteTableMetaData.TABLE_NAME);
			onCreate(db);
		}

	}

(6) 现在来分别实现第一步中未实现的5个方法,先来实现query方法,这里借助SQLiteQueryBuilder来为查询设置投影映射以及设置相关查询条件,看源码实现:

	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
		SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
		switch(sUriMatcher.match(uri)) {
		case COLLECTION_INDICATOR:
			// 设置查询的表
			queryBuilder.setTables(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME);
			// 设置投影映射
			queryBuilder.setProjectionMap(sNotesProjectionMap);
			break;

		case SINGLE_INDICATOR:
			queryBuilder.setTables(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME);
			queryBuilder.setProjectionMap(sNotesProjectionMap);
			queryBuilder.appendWhere(NoteProviderMetaData.NoteTableMetaData._ID + "=" + uri.getPathSegments().get(1));
			break;

		default:
			throw new IllegalArgumentException("Unknow URI: " + uri);
		}

		String orderBy;
		if(TextUtils.isEmpty(sortOrder))
		{
			orderBy = NoteProviderMetaData.NoteTableMetaData.DEFAULT_ORDERBY;
		} else {
			orderBy = sortOrder;
		}
		SQLiteDatabase db = mDbHelper.getReadableDatabase();
		Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, orderBy);

		return cursor;
	}

返回的是一个Cursor对象,它是一个行集合,包含0和多个记录,类似于JDBC中的ResultSet,可以前后移动游标,得到每行每列中的数据。注意的是,使用它需要调用moveToFirst(),因为游标默认是在第一行之前。
(7)实现insert方法,实现把记录插入到基础数据库中,然后返回新创建的记录的URI。

	public Uri insert(Uri uri, ContentValues values) {        
		if (sUriMatcher.match(uri) != COLLECTION_INDICATOR) {
            throw new IllegalArgumentException("Unknown URI " + uri);
        }

		SQLiteDatabase db = mDbHelper.getWritableDatabase();
		long rowID = db.insert(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME, null, values);

		if(rowID > 0) {
			Uri retUri = ContentUris.withAppendedId(NoteProviderMetaData.NoteTableMetaData.CONTENT_URI, rowID);
			return retUri;
		}

		return null;
	}

(8) 实现update方法,根据传入的列值和where字句来更新记录,返回更新的记录数,看源码:

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		SQLiteDatabase db = mDbHelper.getWritableDatabase();
		int count = -1;
		switch(sUriMatcher.match(uri)) {
		case COLLECTION_INDICATOR:
			count = db.update(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME, values, null, null);
			break;

		case SINGLE_INDICATOR:
			String rowID = uri.getPathSegments().get(1);
			count = db.update(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME, values, NoteProviderMetaData.NoteTableMetaData._ID + "=" + rowID, null);
			break;

		default:
			throw new IllegalArgumentException("Unknow URI : " + uri);

		}
		this.getContext().getContentResolver().notifyChange(uri, null);
		return count;
	}

notifyChange函数是在更新数据时,通知其他监听对象。
(9)实现delete方法,该方法返回删除的记录数。

	public int delete(Uri uri, String selection, String[] selectionArgs) {
		SQLiteDatabase db = mDbHelper.getWritableDatabase();
		int count = -1;
		switch(sUriMatcher.match(uri)) {
		case COLLECTION_INDICATOR:
			count = db.delete(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME, selection, selectionArgs);
			break;

		case SINGLE_INDICATOR:
			String rowID = uri.getPathSegments().get(1);
			count = db.delete(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME, NoteProviderMetaData.NoteTableMetaData._ID + "=" + rowID, null);
			break;

		default:
			throw new IllegalArgumentException("Unknow URI :" + uri);

		}
		// 更新数据时,通知其他ContentObserver
		this.getContext().getContentResolver().notifyChange(uri, null);
		return count;
	}

(10) 实现getType方法,根据URI返回MIME类型,这里主要用来区分URI是获取集合还是单条记录,这个方法在这里暂时没啥用处,在使用Intent时有用。

	public String getType(Uri uri) {
		switch(sUriMatcher.match(uri)) {
			case COLLECTION_INDICATOR:
				return NoteProviderMetaData.NoteTableMetaData.CONTENT_TYPE;

			case SINGLE_INDICATOR:
				return NoteProviderMetaData.NoteTableMetaData.CONTENT_ITEM_TYPE;

			default:
				throw new IllegalArgumentException("Unknow URI: " + uri);
		}
	}

(11) 在AndroidManifest.xml中注册该ContentProvider,这样系统才找得到,当然你也可以设置相关的权限,这里就不设置了。

<provider android:name="NoteContentProvider" android:authorities="com.alexzhou.provider.NoteProvider"/>

到现在为止,自定义ContentProvider的全部代码已经完成,下面创建一个简单的应用来测试一下。
主要测试insert、update、delete、query这四个函数。
1. 现在数据库中没有数据,所以先测试insert,插入一条数据。主要代码如下:

public static final String AUTHORITY = "com.alexzhou.provider.NoteProvider";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/notes");

private void insert() {
	 ContentValues values = new ContentValues();
	 values.put("title", "hello");
	 values.put("content", "my name is alex zhou");
	 long time = System.currentTimeMillis();
	 values.put("create_date", time);
	 Uri uri = this.getContentResolver().insert(CONTENT_URI, values);
	 Log.e("test ", uri.toString());
	}

这里需要获得CONTENT_URI值,其他的应用可以通过ContentResolver接口访问ContentProvider提供的数据。logcat的输出如下:
因为现在数据库和表还不存在,所以首先会创建表,返回的URI的值是第一条记录的URI。
2. 测试query方法

	private void query() {
		Cursor cursor = this.getContentResolver().query(CONTENT_URI, null, null, null, null);
		Log.e("test ", "count=" + cursor.getCount());
		cursor.moveToFirst();
		while(!cursor.isAfterLast()) {
			String title = cursor.getString(cursor.getColumnIndex("title"));
			String content = cursor.getString(cursor.getColumnIndex("content"));
			long createDate = cursor.getLong(cursor.getColumnIndex("create_date"));
			Log.e("test ", "title: " + title);
			Log.e("test ", "content: " + content);
			Log.e("test ", "date: " + createDate);

			cursor.moveToNext();
		}
		cursor.close();
	}

logcat输入如下,正是刚才插入的值。

3. 测试update方法

	private void update() {
		ContentValues values = new ContentValues();
		values.put("content", "my name is alex zhou !!!!!!!!!!!!!!!!!!!!!!!!!!");
		int count = this.getContentResolver().update(CONTENT_URI, values, "_id=1", null);
		Log.e("test ", "count="+count);
		query();
	}

logcat输出:

刚才插入的值已被更新了。
(4) 测试delete方法

	private void delete() {
		int count = this.getContentResolver().delete(CONTENT_URI, "_id=1", null);
		Log.e("test ", "count="+count);
		query();
	}

看logcat输出:

再次查询时,已经没有了数据。

ok,到此为止,应该对ContentProvider的作用和创建一个自定义的ContentProvider基本了解了吧。

android使用tcpdump抓包

最近游戏在接qq opensdk的时候调用一个cgi一直不成功,文档描述太简单,我们调用的又是互娱这边msdk的api,由msdk调用opensdk相关api,中间跨了两部门,为了...

阅读全文

Android.mk文件解读

我们在Android平台写c/c++程序的时候需要用到Android.mk(Makefile),一般用来编译c/c++源码、引用第三方头文件和库,生成程序所需的so文件。下面是一个cocos2...

阅读全文

Android性能优化案例研究(下)

去掉冗余的图层 为 了去掉重绘我们必须首先理解它从哪里产生的。这就轮到Hierarchy Viewer和Tracer for OpenGL大显身手的时候了。Hierarchy Viewer是ADT工具...

阅读全文

7 条评论

  1. I often visit your website and have noticed that you don’t update it often.
    More frequent updates will give your page higher authority & rank in google.
    I know that writing posts takes a lot of time, but you can always help yourself with miftolo’s tools which will
    shorten the time of creating an article to a couple of seconds.

  2. I see you don’t monetize your site, don’t waste your traffic,
    you can earn additional bucks every month because you’ve got
    high quality content. If you want to know how to make extra
    money, search for: Mertiso’s tips best adsense alternative

欢迎留言