概述
ContentProvider是Android中提供的专门用于不同应用间数据交互和共享的组件。ContentProvider实际上是对SQLiteOpenHelper的进一步封装,以一个或多个表的形式将数据呈现给外部应用,通过Uri映射来选择需要操作数据库中的哪个表,并对表中的数据进行增删改查处理。ContentProvider其底层使用了Binder来完成APP进程之间的通信,同时使用匿名共享内存来作为共享数据的载体。ContentProvider支持访问权限管理机制,以控制数据的访问者及访问方式,保证数据访问的安全性。
相关知识
在介绍ContentProvider之前,我们先简单介绍一下使用过程中涉及到的相关知识。
URI
URI(Uniform Resource Identifier)即统一资源标识符,是一个用于标识某一互联网资源名称的字符串。以联系人Contacts的Uri为例,其结构如下所示:
- schema: Android中固定为content://。
- authority: 用于唯一标识一个ContentProvider。
- path: ContentProvider中数据表的表名。
- id: 数据表中数据的标识,可选字段。
MIME类型
MIME(Multipurpose Internet Mail Extensions)即多用途互联网邮件扩展类型,是指定某种扩展名的文件用什么应用程序来打开的方式类型。当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。
类型/子类型(Content-Type/subtype ) | 扩展名 |
---|---|
application/vnd.android.package-archive | .apk |
text/plain | .txt |
image/jpeg | .jpeg |
text/html | .html |
audio/x-pn-realaudio | .rmvb |
audio/mpeg | .mp3 |
video/mp4 | .mp4 |
image/png | .png |
application/json | .json |
application/pdf |
关于MIME的类型可以参看我的Android进阶之旅------>MIME类型大全,里面列出了所有的类型。
UriMatcher类
UriMatcher类是一个工具类,帮助匹配ContentProvider中的Uri。只提供了两个方法——addURI和match方法。
private final static String AUTHORITY = "com.android.peter.provider";
private final static int STUDENT_URI_CODE = 0;
private final static UriMatcher sUriMatcher;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//把Uri和Uri_Code相关联
sUriMatcher.addURI(AUTHORITY,"student",STUDENT_URI_CODE);
}
//通过match方法能够根据传递的uri匹配到对应的Uri_Code
int uriType = sUriMatcher.match(uri);
ContentUris类
ContentUris类代码很短,只包含了withAppendedId、parseId、appendId三个静态方法。以接下来示例中要用到的uri("content://com.android.peter.provider/student")为例,依次执行withAppendedId和parseId方法。
Uri uri = Uri.parse("content://com.android.peter.provider/student");
Uri withAppendedIdUri = ContentUris.withAppendedId(uri, 1);
Log.d(TAG," withAppendedId ~ uri = " + withAppendedIdUri.toString());
long parseId = ContentUris.parseId(withAppendedIdUri);
Log.d(TAG," parseId ~ uri = " + parseId);
输出的log如下:
04-10 17:56:03.668 15652-15652/com.android.peter.contentproviderdemo D/MainActivity: withAppendedId ~ uri = content://com.android.peter.provider/student/1
04-10 17:56:03.669 15652-15652/com.android.peter.contentproviderdemo D/MainActivity: parseId ~ uri = 1
从log中可以看出,调用withAppendedId方法会在原始的uri后面添加了一个值为1的id,调用parseId方法可以取出这个id。
appendId方法用于通过Uri.Builder方式生成的Uri使用。
Uri.Builder ub = new Uri.Builder();
ub.authority("com.android.peter.provider")
.appendPath("student");
Log.d(TAG,"ub = " + ub.toString());
Uri.Builder appendIdUri = ContentUris.appendId(ub,1);
Log.d(TAG,"appendIdUri = " + appendIdUri.toString());
输出log如下:
04-10 18:10:48.283 19995-19995/com.android.peter.contentproviderdemo D/MainActivity: ub = //com.android.peter.provider/student
04-10 18:10:48.284 19995-19995/com.android.peter.contentproviderdemo D/MainActivity: appendIdUri = //com.android.peter.provider/student/1
ContentProvider的使用
ContentProvider 的使用可以简单的归纳为以下几步:
1、创建自己的数据列表;
2、自定义ContentProvider实现相关的抽象方法;
3、在AndroidManifest中声明provider以及定义相关访问权限;
4 、通过ContentResolver根据URI进行增删改查。
下面以创建一个student数据库列表,并通过自定义ContentProvider来访问数据为例,详细讲解一下每一步具体都做什么。
-
创建自己的数据列表
首先,你应该根据项目需要选择合适的数据类型设计你的数据库列表,并转化成对应的可执行的SQL语句。然后,派生抽象类SQLiteOpenHelper创建其子类DBOpenHelper 并实现构造方法以及重载onCreate和onUpgrade方法。最后,在onCreate方法中执行你设计好的SQL语句。
public class DBOpenHelper extends SQLiteOpenHelper {
private final static String TAG = "DBOpenHelper";
private final static String DATABASE_NAME = "com_android_peter_provider.db";
public final static String DATABASE_STUDENT_TABLE_NAME = "student";
private final static int DATABASE_VERSION = 1;
private Context mContext;
/**
* student table
* @id primary key
* @name student's name. e.g:peter.
* @gender student's gender. e.g: 0 male; 1 female.
* @number student's number. e.g: 201804081702.
* @score student's score. more than 0 and less than 100. e.g:90.
* */
private final static String CREATE_STUDENT_TABLE = "CREATE TABLE IF NOT EXISTS "
+ DATABASE_STUDENT_TABLE_NAME
+ "(id INTEGER PRIMARY KEY,"
+ "name TEXT VARCHAR(20) NOT NULL,"
+ "gender BIT DEFAULT(1),"
+ "number TEXT VARCHAR(12) NOT NULL,"
+ "score INTEGER CHECK(score >= 0 and score <= 100))";
public DBOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mContext = context;
}
public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, DATABASE_NAME, factory, DATABASE_VERSION);
mContext = context;
}
public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
super(context, DATABASE_NAME, factory, DATABASE_VERSION, errorHandler);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.d(TAG, "onCreate");
db.execSQL(CREATE_STUDENT_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.d(TAG, "onUpgrade o = " + oldVersion + " , n = " + newVersion);
}
}
创建的数据库位于"/data/data/包名/databases/"目录中。
-
自定义ContentProvider类实现相关的抽象方法
创建一个自定义的ContentProvider需要实现以下几个方法:
方法 | 功能 |
---|---|
onCreate() | 初始化 |
query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) | 查询数据 |
insert(@NonNull Uri uri, @Nullable ContentValues values) | 插入数据 |
update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) | 更新数据 |
delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) | 删除数据 |
getType(@NonNull Uri uri) | 获得数据的MIME类型 |
具体实现如下:
public class StudentContentProvider extends ContentProvider {
private final static String TAG = "StudentProvider";
private final static String AUTHORITY = "com.android.peter.provider";
private final static int STUDENT_URI_CODE = 0;
private Context mContext;
private SQLiteDatabase mDataBase;
private final static UriMatcher sUriMatcher;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY,"student",STUDENT_URI_CODE);
}
@Override
public boolean onCreate() {
mContext = getContext();
mDataBase = new DBOpenHelper(mContext).getWritableDatabase();
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
int uriType = sUriMatcher.match(uri);
Cursor cursor;
switch (uriType) {
case STUDENT_URI_CODE:
cursor = mDataBase.query(DBOpenHelper.DATABASE_STUDENT_TABLE_NAME,projection,selection,selectionArgs,null,null,sortOrder,null);
break;
default:
throw new IllegalArgumentException("UnSupport Uri : " + uri);
}
return cursor;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
int uriType = sUriMatcher.match(uri);
long row;
switch (uriType) {
case STUDENT_URI_CODE:
row = mDataBase.insert(DBOpenHelper.DATABASE_STUDENT_TABLE_NAME,null, values);
break;
default:
throw new IllegalArgumentException("UnSupport Uri : " + uri);
}
if(row > -1) {
mContext.getContentResolver().notifyChange(uri,null);
return ContentUris.withAppendedId(uri, row);
}
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
int uriType = sUriMatcher.match(uri);
int rowDelete;
switch (uriType) {
case STUDENT_URI_CODE:
rowDelete = mDataBase.delete(DBOpenHelper.DATABASE_STUDENT_TABLE_NAME,selection,selectionArgs);
break;
default:
throw new IllegalArgumentException("UnSupport Uri : " + uri);
}
if(rowDelete > 0) {
mContext.getContentResolver().notifyChange(uri,null);
}
return rowDelete;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
int uriType = sUriMatcher.match(uri);
int rowUpdate;
switch (uriType) {
case STUDENT_URI_CODE:
rowUpdate = mDataBase.update(DBOpenHelper.DATABASE_STUDENT_TABLE_NAME,values,selection,selectionArgs);
break;
default:
throw new IllegalArgumentException("UnSupport Uri : " + uri);
}
if(rowUpdate > 0) {
mContext.getContentResolver().notifyChange(uri,null);
}
return rowUpdate;
}
}
注意:
- onCreate()由系统回调并运行在主线程里,其他五个方法由外界回调并运行在Binder线程池中。所以,不要在onCreate方法中做耗时操作。
- 增删改查操作存在多线程并发访问问题,因此方法内部要做好线程同步。
- SQLiteDatabase的insert方法的返回值是插入数据所在的行号,update和delete方法的返回值代表此次操作影响到的行数。
-
在AndroidManifest中声明provider以及定义相关访问权限
在注册ContentProvider的时候通过android:process属性设置provider运行在单独的进程里,模拟进程间通信。
<!-- student provider 访问权限声明 -->
<permission
android:name="com.android.peter.provider.READ_PERMISSION"
android:label="Student provider read permission"
android:protectionLevel="normal"
/>
<permission
android:name="com.android.peter.provider.WRITE_PERMISSION"
android:label="Student provider read permission"
android:protectionLevel="normal"
/>
<!-- 声明ContentProvider -->
<application
...
<provider
android:name=".StudentContentProvider"
android:authorities="com.android.peter.provider"
android:readPermission="com.android.peter.provider.READ_PERMISSION"
android:writePermission="com.android.peter.provider.WRITE_PERMISSION"
android:process=":provider"
android:exported="true"/>
...
</application>
为了方便起见,权限声明时protectionLevel设置的是最低风险权限(normal),关于其他等级权限和说明如下:
权限等级 | 说明 |
---|---|
normal | 低风险权限,只要申请了就可以使用,安装时不需要用户确认。 |
dangerous | 高风险权限,安装时需要用户确认授权才可使用。 |
signature | 只有当申请权限应用与声明此权限应用的数字签名相同时才能将权限授给它。 |
signatureOrSystem | 签名相同或者申请权限的应用为系统应用才能将权限授给它。 |
-
通过ContentResolver根据URI进行增删改查
在Activity中分别定义insertValue、queryValue、updateValue、deleteValue对自定义的provider进行测试。其中,database中id=1的数据为预存数据,为了方便查看数据的变化;Student类为student数据的封装类。
public class ClientActivity extends AppCompatActivity {
private final static String TAG = "ClientActivity";
/**
* student table
* @id primary key
* @name student's name. e.g:peter.
* @gender student's gender. e.g: 0 male; 1 female.
* @number student's number. e.g: 201804081702.
* @score student's score. more than 0 and less than 100. e.g:90.
* */
private final static String AUTHORITY = "com.android.peter.provider";
private final static Uri STUDENT_URI = Uri.parse("content://" + AUTHORITY + "/student");
private Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client);
mContext = this;
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG,"------------insert---------");
insertValue();
queryValue();
Log.d(TAG,"------------update---------");
updateValue();
queryValue();
Log.d(TAG,"------------delete---------");
deleteValue();
queryValue();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
private void insertValue() {
ContentValues contentValues = new ContentValues();
contentValues.put("id",0);
contentValues.put("name","peter");
contentValues.put("gender",0);
contentValues.put("number","201804081705");
contentValues.put("score","100");
mContext.getContentResolver().insert(STUDENT_URI,contentValues);
}
private void queryValue() {
Cursor cursor = getContentResolver().query(STUDENT_URI, new String[]{"id", "name","gender","number","score"},null,null,null);
while (cursor.moveToNext()) {
Student student = new Student();
student.id = cursor.getInt(cursor.getColumnIndex("id"));
student.name = cursor.getString(cursor.getColumnIndex("name"));
student.gender = cursor.getInt(cursor.getColumnIndex("gender"));
student.number = cursor.getString(cursor.getColumnIndex("number"));
student.score = cursor.getInt(cursor.getColumnIndex("score"));
Log.d(TAG,"student = " + student.toString());
}
}
private void updateValue() {
ContentValues contentValues = new ContentValues();
contentValues.put("id",0);
contentValues.put("name","update");
contentValues.put("gender",1);
contentValues.put("number","201804111048");
contentValues.put("score","90");
getContentResolver().update(STUDENT_URI,contentValues,"id = ?",new String[] {"0"});
}
private void deleteValue() {
getContentResolver().delete(STUDENT_URI,"name = ?",new String[]{"update"});
}
}
//student数据封装类
public class Student {
private final static String TAG = "Student";
public Integer id;
public String name;
public Integer gender;
public String number;
public Integer score;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", gender=" + gender +
", number='" + number + '\'' +
", score=" + score +
'}';
}
}
为了方便查看数据,insert、update、delete操作之后都会调用query重新查询打印log。增加一条id=0的数据,然后更新这条数据,最后删除这条数据,log如下:
04-11 10:51:13.405 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: ------------insert---------
04-11 10:51:13.778 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: student = Student{id=0, name='peter', gender=0, number='201804081705', score=100}
04-11 10:51:13.779 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: student = Student{id=1, name='lemon', gender=1, number='201804091601', score=100}
04-11 10:51:13.779 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: ------------update---------
04-11 10:51:13.796 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: student = Student{id=0, name='update', gender=1, number='201804111048', score=90}
04-11 10:51:13.796 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: student = Student{id=1, name='lemon', gender=1, number='201804091601', score=100}
04-11 10:51:13.796 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: ------------delete---------
04-11 10:51:13.810 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: student = Student{id=1, name='lemon', gender=1, number='201804091601', score=100}
如果是访问其它应用定义的provider只需要在AndroidManifest中声明相应权限即可使用,使用方法跟上面Activity中的示例是一样的。
<uses-permission android:name="com.android.peter.provider.READ_PERMISSION"/>
<uses-permission android:name="com.android.peter.provider.WRITE_PERMISSION"/>
ContentObserver
Android中提供的用来监听ContentProvider变化的抽象类,可以通过ContentResolver的registerContentObserver和unregisterContentObserver方法来注册和注销ContentObserver监听器。当被监听的ContentProvider发生变化时,就会回调对应的ContentObserver的onChange方法。具体用法如下:
private final static int CONTENT_PROVIDER_CHANGED = 20180412;
private Handler mHandler;
private ContentObserver mContentObserver;
//更新UI线程
private class ObserverHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.d(TAG,"handleMessage msg = " + msg);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client);
mContext = this;
//实例化ContentObserver
mHandler = new ObserverHandler();
mContentObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
Log.d(TAG,"onChange selfChange = " + selfChange + " , uri = " + uri.toString());
mHandler.obtainMessage(CONTENT_PROVIDER_CHANGED).sendToTarget();
}
};
//注册监听
mContext.getContentResolver().registerContentObserver(STUDENT_URI,true,mContentObserver);
}
@Override
protected void onDestroy() {
super.onDestroy();
//注销监听
if(mContentObserver != null) {
mContext.getContentResolver().unregisterContentObserver(mContentObserver);
}
}
在使用完记得一定要注销ContentObserver以免引起内存泄漏。
小结
本文基于Android 8.0对ContentProvider使用过程中涉及到的一些知识点进行了简单的介绍和整理,归纳总结了ContentProvider的使用步骤,并通过一个示例详细的说明了每个步骤具体都做些什么。
参考文献
ContentProvider从入门到精通
Android 进阶11:进程通信之 ContentProvider 内容提供者
Android:关于ContentProvider的知识都在这里了!
Android SQLite数据库的详细使用
内容提供者ContentProvider的基本使用
ContentProvider数据库共享之——读写权限与数据监听
ContentObserver
网友评论