android ContentProvider使用详解

简介:

由于之前主要做手机游戏相关的开发,所以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:

1
2
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自动生成的,暂时先不动他们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
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和数据库、表、字段相关信息,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
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类型,下面看实现源码:

1
2
3
4
5
6
7
8
9
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中添加如下代码。

1
2
3
4
5
6
7
8
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,完成数据库表的创建、更新,这样可以通过它获得数据库对象,相关代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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来为查询设置投影映射以及设置相关查询条件,看源码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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字句来更新记录,返回更新的记录数,看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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方法,该方法返回删除的记录数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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时有用。

1
2
3
4
5
6
7
8
9
10
11
12
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,这样系统才找得到,当然你也可以设置相关的权限,这里就不设置了。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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方法

1
2
3
4
5
6
7
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方法

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

看logcat输出:

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

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

相关文章
|
5月前
|
数据库 Android开发 开发者
Android Studio入门之内容共享ContentProvider讲解以及实现共享数据实战(附源码 超详细必看)
Android Studio入门之内容共享ContentProvider讲解以及实现共享数据实战(附源码 超详细必看)
44 0
|
5月前
|
数据库 Android开发 Kotlin
android开发,使用kotlin学习ContentProvider
android开发,使用kotlin学习ContentProvider
49 0
|
9月前
|
Java Android开发
Android 四大组件之ContentProvider 访问通讯录进行增删改查操作
Android 四大组件之ContentProvider 访问通讯录进行增删改查操作
52 0
|
9月前
|
API 数据库 Android开发
Android ContentProvider内容提供者详解
Android ContentProvider内容提供者详解
38 2
|
11月前
|
存储 API 数据库
Android:四大组件之 ContentProvider(外共享数据)
数据库在 Android 当中是私有的,不能将数据库设为 WORLD_READABLE,每个数据库都只能允许创建它的包访问。这意味着只有创建这个数据库的应用程序才可访问它。也就是说不能跨越进程和包的边界,直接访问别的应用程序的数据库。那么如何在应用程序间交换数据呢? 如果需要在进程间传递数据,可以使用 ContentProvider 来实现。
222 0
Android:四大组件之 ContentProvider(外共享数据)
|
存储 数据库 Android开发
android中数据存储的contentprovider的使用方法
android中数据存储的contentprovider的使用方法
104 0
|
SQL 存储 自然语言处理
Android | ContentProvider 筑基篇 | 牛气冲天新年征文
Android | ContentProvider 筑基篇 | 牛气冲天新年征文
93 0
Android | ContentProvider 筑基篇 | 牛气冲天新年征文
|
API Android开发 对象存储
Android | 使用 ContentProvider 无侵入获取 Context
Android | 使用 ContentProvider 无侵入获取 Context
351 0
Android | 使用 ContentProvider 无侵入获取 Context
|
存储 缓存 数据库
Android ContentProvider支持跨进程数据共享与&quot;互斥、同步&quot;杂谈
Android ContentProvider支持跨进程数据共享与&quot;互斥、同步&quot;杂谈
503 0
Android ContentProvider支持跨进程数据共享与&quot;互斥、同步&quot;杂谈
|
存储 缓存 安全
Android 面试题之ContentProvider使用+实例
一、什么是ContentProvider 二、什么是Uri 三、什么是ContentResolver 四、创建ContentProvider 五、使用ContentProvider 六、跨应用使用ContentProvider 七、java.lang.SecurityException: Permission Denial: opening provider com.scc.cp.UserProvider from ProcessRecord 源码:ContentProviderDemo.zip
389 0
Android 面试题之ContentProvider使用+实例