ContentProvider数据访问详解
Android官方指出的数据存储方式总共有五种:Shared Preferences、网络存储、文件存储、外储存储、SQLite,这些存储方式一般都只是在一个单独的应用程序中实现数据的共享,而对于需要操作其他应用程序中的数据时(如媒体库、通讯录等),可能就需要借助ContentProvider了。
1、ContentProvider
ContentProvider为存储和获取数据提供了统一的接口,使用表的形式来对数据进行封装,使得开发者在后续的开发过程中不用关心数据存储的细节。使用ContentProvider可以在不同的应用程序之间共享数据,Android为常见的数据类型提供了默认的ContentProvider(包括音频、视频、图片和通讯录等)。
总的来说,利用ContentProvider来实现共享数据的好处是统一了数据的访问方式。
2、URI(Uniform Resource Identifier)
URI为系统中的每一个资源赋予一个名字,比方说通话记录。每一个ContentProvider都拥有一个公共的URI,用于表示ContentProvider所提供的数据。 Android所提供的ContentProvider都位于android.provider包中, 可以将URI分为A、B、C、D 4个部分来理解。如对于content://com.wang.provider.myprovider/tablename/id:
a、标准前缀——content://,用来说明一个Content Provider控制这些数据;
b、URI的标识——com.wang.provider.myprovider,用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。对于第三方应用程序,为了保证URI标识的唯一性,它必须是一个完整的、小写的类名。这个标识在元素的authorities属性中说明,一般是定义该ContentProvider的包.类的名称;
c、路径——tablename,通俗的讲就是你要操作的数据库中表的名字,或者你也可以自己定义,记得在使用的时候保持一致就可以了;
d、记录ID——id,如果URI中包含表示需要获取的记录的ID,则返回该id对应的数据,如果没有ID,就表示返回全部;
对于第三部分路径(path)做进一步的解释,用来表示要操作的数据,构建时应根据实际项目需求而定。如:
a、操作tablename表中id为11的记录,构建路径:/tablename/11;
b、操作tablename表中id为11的记录的name字段:tablename/11/name;
c、操作tablename表中的所有记录:/tablename;
d、操作来自文件、xml或网络等其他存储方式的数据,如要操作xml文件中tablename节点下name字段:/ tablename/name;
e、若需要将一个字符串转换成Uri,可以使用Uri类中的parse()方法,如:
1 Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename");
3、UriMatcher
Uri代表要操作的数据,在开发过程中对数据进行获取时需要解析Uri,Android提供了两个用于操作Uri的工具类,分别为UriMatcher和ContentUris 。掌握它们的基本概念和使用方法,对一个Android开发者来说是一项必要的技能。
UriMatcher类用于匹配Uri,它的使用步骤如下:
a、将需要匹配的Uri路径进行注册,代码如下:
1 //常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码 2 UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH); 3 //如果match()方法匹配“content://com.wang.provider.myprovider/tablename”路径,返回匹配码为1 4 sMatcher.addURI("content://com.wang.provider.myprovider", " tablename ", 1); 5 //如果match()方法匹配content://com.wang.provider.myprovider/tablename/11路径,返回匹配码为2 6 sMatcher.addURI("com.wang.provider.myprovider", "tablename/#", 2);
注意,添加第二个URI时,路径后面的id采用了通配符形式“#”,表示只要前面三个部分都匹配上了就OK。
b、注册完需要匹配的Uri后,可以使用sMatcher.match(Uri)方法对输入的Uri进行匹配,如果匹配就返回对应的匹配码,匹配码为调用addURI()方法时传入的第三个参数。
1 switch (sMatcher.match(Uri.parse("content://com.zhang.provider.yourprovider/tablename/100"))) { 2 case 1: 3 //match 1, todo something 4 break; 5 case 2 6 //match 2, todo something 7 break; 8 default: 9 //match nothing, todo something 10 break; 11 }
4、ContentUris
ContentUris类用于操作Uri路径后面的ID部分,它有两个比较实用的方法:withAppendedId(Uri uri, long id)和parseId(Uri uri)。
withAppendedId(Uri uri, long id)用于为路径加上ID部分:
1 Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename"); 2 Uri resultUri = ContentUris.withAppendedId(uri, 10);
parseId(Uri uri)则从路径中获取ID部分:
1 Uri uri = Uri.parse("content://com.zhang.provider.myprovider/tablename/10") 2 long personid = ContentUris.parseId(uri);
5、ContentProvider数据共享
ContentProvider类主要方法的介绍:
public boolean onCreate(),在ContentProvider创建后就会被调用,而ContentProvider是在其它应用第一次访问它时被创建;
public Uri insert(Uri uri, ContentValues values),供外部应用向ContentProvider添加数据;
public int delete(Uri uri, String selection, String[] selectionArgs),供外部应用从ContentProvider删除数据;
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs),供外部应用更新ContentProvider中的数据;
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder),供外部应用从ContentProvider中获取数据;
public String getType(Uri uri),返回当前Uri所代表数据的MIME类型;
如果操作的数据属于集合类型,那么MIME类型字符串应该以vnd.android.cursor.dir/开头,如要得到所有tablename记录的Uri为content://com.wang.provider.myprovider/tablename,那么返回的MIME类型字符串应该为:vnd.android.cursor.dir/table。
如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以vnd.android.cursor.item/开头,如得到id为10的tablename记录,Uri为content://com.wang.provider.myprovider/tablename/10,那么返回的MIME类型字符串为:vnd.android.cursor.item/tablename 。
6、ContentResolver操作数据
当外部应用需要对ContentProvider中的数据进行添加、删除、修改及查询操作时,可以使用ContentResolver 类来完成。而要获取ContentResolver 对象,可以使用Activity提供的getContentResolver()方法。
ContentResolver 类提供了与ContentProvider类相同签名的四个方法:
public Uri insert(Uri uri, ContentValues values),往ContentProvider添加数据;
public int delete(Uri uri, String selection, String[] selectionArgs),从ContentProvider删除数据;
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs),更新ContentProvider中的数据;
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder),从ContentProvider中获取数据;
这些方法的第一个参数为Uri,代表要操作的ContentProvider和对其中的什么数据进行操作,其实和ContentProvider里面的方法是一样的。他们所对应的数据,最终会被传到我们在之前程序里面定义的那个ContentProvider类的方法,假设给定的是:Uri.parse("content://com.wang.provider.myprovider/tablename/10"),那么将会对主机名为com.wang.provider.myprovider的ContentProvider进行操作,操作的数据为tablename表中id为10的记录。
使用ContentResolver对ContentProvider中的数据进行操作的代码如下:
1 ContentResolver resolver = getContentResolver(); 2 Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename"); 3 //添加一条记录 4 ContentValues values = new ContentValues(); 5 values.put("name", "wang1"); 6 values.put("age", 28); 7 resolver.insert(uri, values); 8 //获取tablename表中所有记录 9 Cursor cursor = resolver.query(uri, null, null, null, "tablename data"); 10 while(cursor.moveToNext()){ 11 Log.i("ContentTest", "tablename_id="+ cursor.getInt(0)+ ", name="+ cursor.getString(1)); 12 } 13 //把id为1的记录的name字段值更改新为zhang1 14 ContentValues updateValues = new ContentValues(); 15 updateValues.put("name", "zhang1"); 16 Uri updateIdUri = ContentUris.withAppendedId(uri, 2); 17 resolver.update(updateIdUri, updateValues, null, null); 18 //删除id为2的记录,即字段age 19 Uri deleteIdUri = ContentUris.withAppendedId(uri, 2); 20 resolver.delete(deleteIdUri, null, null);
7、监听数据变化
如果ContentProvider的访问者需要知道数据发生的变化,可以在ContentProvider发生数据变化时调用getContentResolver().notifyChange(uri, null)来通知注册在此URI上的访问者。只给出类中监听部分的代码:
1 public class MyProvider extends ContentProvider { 2 public Uri insert(Uri uri, ContentValues values) { 3 db.insert("tablename", "tablenameid", values); 4 getContext().getContentResolver().notifyChange(uri, null); 5 } 6 }
而访问者必须使用ContentObserver对数据(数据采用uri描述)进行监听,当监听到数据变化通知时,系统就会调用ContentObserver的onChange()方法:
1 getContentResolver().registerContentObserver(Uri.parse("content://com.ljq.providers.personprovider/person"), 2 true, new PersonObserver(new Handler())); 3 public class PersonObserver extends ContentObserver{ 4 public PersonObserver(Handler handler) { 5 super(handler); 6 } 7 public void onChange(boolean selfChange) { 8 //to do something 9 } 10 }
8、完整项目代码
a、对于实际测试中的Uri——com.wang.provider.myprovider,需要在AndroidManifest.xml文件中进行说明:
1 <provider android:name="MyProvider" android:authorities="com.wang.provider.myprovider" />
b、定义Profile类,其主要负责定义Uri各组成部分名称对应的String常量:
1 package com.wang.testcontentprovider; 2 3 import android.net.Uri; 4 5 public class Profile { 6 7 /** 8 * 表格名称 9 */ 10 public static final String TABLE_NAME = "tablename"; 11 12 /** 13 * 列表一,_ID,自动增加 14 */ 15 public static final String COLUMN_ID = "_id"; 16 17 /** 18 * 列表二,名称 19 */ 20 public static final String COLUMN_NAME = "name"; 21 22 public static final String AUTOHORITY = "com.wang.provider.myprovider"; 23 public static final int ITEM = 1; 24 public static final int ITEM_ID = 2; 25 26 public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.wang.tablename"; 27 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.wang.tablename"; 28 29 public static final Uri CONTENT_URI = Uri.parse("content://" + AUTOHORITY + "/tablename"); 30 }
c、实现存放数据的DBHelper类,继承自SQLiteOpenHelper类,完成数据库的一些常规操作:
1 package com.wang.testcontentprovider; 2 3 import android.content.Context; 4 import android.database.SQLException; 5 import android.database.sqlite.SQLiteDatabase; 6 import android.database.sqlite.SQLiteOpenHelper; 7 8 public class DBHelper extends SQLiteOpenHelper { 9 10 /** 11 * 数据库名称 12 */ 13 private static final String DATABASE_NAME = "test.db"; 14 15 /** 16 * 数据库版本 17 */ 18 private static final int DATABASE_VERSION = 1; 19 20 public DBHelper(Context context) { 21 super(context, DATABASE_NAME, null, DATABASE_VERSION); 22 } 23 24 @Override 25 public void onCreate(SQLiteDatabase db) throws SQLException { 26 //创建表格 27 db.execSQL("CREATE TABLE IF NOT EXISTS "+ Profile.TABLE_NAME + "("+ Profile.COLUMN_ID +" INTEGER PRIMARY KEY AUTOINCREMENT," + Profile.COLUMN_NAME +" VARCHAR NOT NULL);"); 28 } 29 30 @Override 31 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLException { 32 //删除并创建表格 33 db.execSQL("DROP TABLE IF EXISTS "+ Profile.TABLE_NAME+";"); 34 onCreate(db); 35 } 36 }
d、MyProvider类的实现,主要负责向ContentProvider中添加数据
1 package com.wang.testcontentprovider; 2 3 import android.content.ContentProvider; 4 import android.content.ContentUris; 5 import android.content.ContentValues; 6 import android.content.UriMatcher; 7 import android.database.Cursor; 8 import android.database.SQLException; 9 import android.database.sqlite.SQLiteDatabase; 10 import android.net.Uri; 11 12 public class MyProvider extends ContentProvider { 13 14 DBHelper mDbHelper = null; 15 SQLiteDatabase db = null; 16 17 private static final UriMatcher mMatcher; 18 static{ 19 mMatcher = new UriMatcher(UriMatcher.NO_MATCH); 20 mMatcher.addURI(Profile.AUTOHORITY,Profile.TABLE_NAME, Profile.ITEM); 21 mMatcher.addURI(Profile.AUTOHORITY, Profile.TABLE_NAME+"/#", Profile.ITEM_ID); 22 } 23 24 @Override 25 public int delete(Uri uri, String selection, String[] selectionArgs) { 26 // TODO Auto-generated method stub 27 return 0; 28 } 29 30 @Override 31 public String getType(Uri uri) { 32 switch (mMatcher.match(uri)) { 33 case Profile.ITEM: 34 return Profile.CONTENT_TYPE; 35 case Profile.ITEM_ID: 36 return Profile.CONTENT_ITEM_TYPE; 37 default: 38 throw new IllegalArgumentException("Unknown URI"+uri); 39 } 40 } 41 42 @Override 43 public Uri insert(Uri uri, ContentValues values) { 44 // TODO Auto-generated method stub 45 long rowId; 46 if(mMatcher.match(uri)!=Profile.ITEM){ 47 throw new IllegalArgumentException("Unknown URI"+uri); 48 } 49 rowId = db.insert(Profile.TABLE_NAME,null,values); 50 if(rowId>0){ 51 Uri noteUri=ContentUris.withAppendedId(Profile.CONTENT_URI, rowId); 52 getContext().getContentResolver().notifyChange(noteUri, null); 53 return noteUri; 54 } 55 56 throw new SQLException("Failed to insert row into " + uri); 57 } 58 59 @Override 60 public boolean onCreate() { 61 // TODO Auto-generated method stub 62 mDbHelper = new DBHelper(getContext()); 63 64 db = mDbHelper.getReadableDatabase(); 65 66 return true; 67 } 68 69 @Override 70 public Cursor query(Uri uri, String[] projection, String selection, 71 String[] selectionArgs, String sortOrder) { 72 // TODO Auto-generated method stub 73 Cursor c = null; 74 switch (mMatcher.match(uri)) { 75 case Profile.ITEM: 76 c = db.query(Profile.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder); 77 break; 78 case Profile.ITEM_ID: 79 c = db.query(Profile.TABLE_NAME, projection,Profile.COLUMN_ID + "="+uri.getLastPathSegment(), selectionArgs, null, null, sortOrder); 80 break; 81 default: 82 throw new IllegalArgumentException("Unknown URI"+uri); 83 } 84 85 c.setNotificationUri(getContext().getContentResolver(), uri); 86 return c; 87 } 88 89 @Override 90 public int update(Uri uri, ContentValues values, String selection, 91 String[] selectionArgs) { 92 // TODO Auto-generated method stub 93 return 0; 94 } 95 96 }
e、作为主程序的MainActivity类,对ContentProvider中的数据进行获取,并在界面上进行简单的显示:
1 package com.wang.testcontentprovider; 2 3 import android.support.v7.app.ActionBarActivity; 4 import android.os.Bundle; 5 import android.view.Menu; 6 import android.view.MenuItem; 7 8 import android.app.ListActivity; 9 import android.content.ContentResolver; 10 import android.content.ContentUris; 11 import android.content.ContentValues; 12 import android.database.Cursor; 13 import android.database.SQLException; 14 import android.database.sqlite.SQLiteDatabase; 15 import android.net.Uri; 16 import android.widget.SimpleCursorAdapter; 17 18 public class MainActivity extends ListActivity { 19 private SimpleCursorAdapter adapter= null; 20 private Cursor mCursor = null; 21 private ContentResolver mContentResolver = null; 22 23 @Override 24 public void onCreate(Bundle savedInstanceState) { 25 super.onCreate(savedInstanceState); 26 initData(); 27 initAdapter(); 28 } 29 30 public void initData(){ 31 mContentResolver = getContentResolver(); 32 for (int i = 0; i < 100; i++) { 33 ContentValues values = new ContentValues(); 34 values.put(Profile.COLUMN_NAME, "Wang "+i); 35 mContentResolver.insert(Profile.CONTENT_URI, values); 36 } 37 } 38 39 public void initAdapter(){ 40 mCursor = mContentResolver.query(Profile.CONTENT_URI, new String[]{Profile.COLUMN_ID,Profile.COLUMN_NAME}, null, null, null); 41 42 startManagingCursor(mCursor); 43 44 //设置adapter 45 adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2, mCursor, new String[]{Profile.COLUMN_ID,Profile.COLUMN_NAME}, new int[]{android.R.id.text1,android.R.id.text2}); 46 setListAdapter(adapter); 47 } 48 49 @Override 50 public boolean onCreateOptionsMenu(Menu menu) { 51 getMenuInflater().inflate(R.menu.main, menu); 52 return true; 53 } 54 55 }
9、结果分析
先贴上一张不太美观的结果图: