Android开发学习笔记——四大组件之ContentProvider
Android开发学习笔记——四大组件之ContentProvider
ContentProvider
简介
ContentProvider作为Android开发四大组件之一,其主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能选择只对哪一部分数据 进行访问,保证被访问数据的安全性。
ContentProvider实际上是对SQLiteOpenHelper的进一步封装,以一个或多个表的形式将数据呈现给外部应用,通过Uri映射来选择需要操作数据库中的哪个表,并对表中的数据进行增删改查处理。其底层使用了Binder来完成跨进程通信,同时使用匿名共享内存来作为共享数据的载体。ContentProvider支持访问权限管理机制,以控制数据的访问者及访问方式,保证数据访问的安全性。
跨进程通信概述
在学习使用ContentProvider之前,首先,我们需要了解为什么我们需要使用ContentProvider呢?为什么不同应用之间不能够直接进行数据共享和通信,而要通过ContentProvider呢?这就需要我们了解下跨进程通信的机制了。
首先,我们需要了解什么是进程?进程是系统分配资源的最小单位,在Android系统中,系统为每个进程分配了一个独立的虚拟机,所以在内存分配上不同进程是有不同的地址空间的,也就是说不同进程是无法相互访问内存中的数据的,这也就造成了不同进程之间无法直接进行通信的。此时就需要跨进程通信机制。
跨进程通信的方法有AIDL、Messager、Socket等,其中就包括了ContentProvider.q其实从上述描述中,我们知道,ContentProvider能够实现跨应用的数据共享,而Android系统中,每个应用都拥有这单独的进程,也就是说,ContentProvider能够实现跨进程的数据共享。
基本使用
我们知道ContentProvider是用于跨应用共享数据,那么我们可以将其用法分为两种,一种是使用ContentProvider来读取和操作相应程序中的数据,另一种则是使用ContentProvider将应用本身的数据暴露给外部其它应用使用。
相关知识
为了学习ContentProvider首先我们需要学习一些相关的内容。
ContentResolver
对于每一个应用程序来说,如果想要访问ContentProvider*享的数据,就一定要借助ContentResolver类。
ContentResolver类主要是用于统一管理不同的ContentProvider间的操作,即通过ContentResolver类可以操作不同的ContentProvider中的数据。那么为什么要使用ContentResolver而不是直接使用ContentProvider呢?这是因为,如果一款应用 要使用多个ContentProvider,如需要了解每个ContentProviderr从而来完成数据交互,操作成本高且难度大,而加上一个ContentResolver来对所有的ContentProvider统一管理无疑就方便了许多。
通过Context中的getContentResolver方法,我们可以获取到ContentResolver的实例,ContentResolver中提供了一系列的方法用于对数据进行增删改查操作。具体方法如下:
方法名 | 说明 |
---|---|
insert | 外部进程向contentProvider中添加数据 |
delete | 外部进程向contentProvider中删除数据 |
update | 外部进程向contentProvider中修改数据 |
query | 外部进程向contentProvider中查询数据 |
具体使用方式大致如下:
//设置ContentProvider的Uri
val uri = Uri.parse("xxx")
//获取contentResolver实例,并根据Uri对对应的ContentProvider进行数据操作
contentResolver.insert(uri, null)
URI
我们现在已经知道了ContentResolver是用于管理不同的ContentProvider的,那么它是如何进行区分的呢?其实,从上述代码和方法的参数,我们可以发现,其是使用Uri进行区分的。
URI就是Uniform Resource Identifier,即统一资源标识符,其作用就是唯一标识数据资源。在ContentProvider中,外界进程就是通过URI找到对应的ContentProvider和其中的数据,从而进行数据操作的。URI主要被分为三个部分,包括:schema(协议声明)、authority(授权信息)、path(资源路径)
其中content为Android规定的ContentProvider前缀,authority是用于对不同的应用程序做区分的,一般使用应用包名进行命名,而path则是用于说明资源路径,区分应用程序中不同的表,其后还能够表中具体记录id。获取到uri字符串后,我们即可通过Uri.parse方法解析成uri对象。
//名为"com.example.learnproject.provider"ContentProvider的user表
val uri = Uri.parse("content://com.example.learnproject.provider/user")
同时,Uri支持通配符,其中"*“表示匹配任意长度的任意字符,”#"表示匹配任意长度的数字。
MIME数据类型
MIME主要用于指定某个扩展名文件用某种应用程序来打开,每个MIME类型都是由2部分组成的字符串,MIME=类型/子类型,如"text/html"等。对于一个URI对应的MIME,Android做了以下规定:
- 对于单条记录的uri,即以path以id结尾的uri,MIME的类型为vnd.android.cursor.item
- 对于多条记录的uri,即不以id结尾的uri,MIME的类型为vnd.android.cursor.dir
- MIME的子类型为vnd..
如下实例:
//uri = content://com.example.learnproject.provider/user
vnd.android.cursor.dir/vnd.com.example.learnproject.provider.user
//uri = content://com.example.learnproject.provider/user/1
vnd.android.cursor.item/vnd.com.example.learnproject.provider.user
使用ContentProvider访问其它应用
我们知道ContentProvider能够实现跨应用的数据访问,在Android系统中,许多系统应用提供了内置的默认ContentProvider,那么接下来,就让我们来简单学习下通过ContentProvider来访问系统的通讯录。
其实,更加前面的介绍,我们可以知道,如果需要访问外部应用的ContentProvider数据,我们只通过Uri即可使用ContentResolver获取到对应的数据并进行操作。如下代码:
//读取通讯录
private fun readConttacts(){
//获取系统通讯录提供的Uri
val uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI
//通过ContentResolver获取数据
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.apply {
while (moveToNext()){
//获取联系人名字
val name = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
//获取联系人电话
val phone = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
Log.e("test_bug", "contact:----name:$name, phone:$phone")
}
close()
}
}
输出结果如下:
我们可以看到使用方法很简单,其实际上和数据库的操作方法差不多,只是我们是通过uri和ContentResolver来获取到数据的。注意:读取通讯录是需要权限的,因此要先申请权限。
创建ContentProvider提供外部访问接口
我们已经学习了如何在自己的程序中访问系统的通讯录,如果我们需要访问其它应用数据,思路也是一样的,不同就是Uri以及获取到数据后的操作。但是,我们发现实际上我们并没有用到ContentProvider类,因为ContentProvider是内容提供者,是提供数据访问接口的,也就是说在系统通讯录中实现了对应的ContentProvider,接下来,就让我们来学习下,如何使用ContentProvider为自己的应用提供外部访问接口。我们所需要做的就是继承ContentProvider,并实现其中的方法。
首先,和所有Android的四大组件一样,ContentProvider需要在AndroidManifest中注册才可以使用,当然如果我们使用AndroidStudio创建,AS会自动为我们进行注册,如下图:
在创建时,我们需要指定ContentProvider的Uri的authorities,其中exported属性为是否暴露给外部使用,注册后AndroidManifest文件中代码如下:
<provider
android:name=".contentprovider.MyContentProvider"
android:authorities="com.example.learnproject.provider"
android:enabled="true"
android:exported="true"></provider>
具体需要实现的方法如下:
class MyContentProvider : ContentProvider() {
/**
* 删除数据
* @param uri 资源标识符,指定ContentProvider和表名
* @param selection 指定where的约束条件
* @param selectionArgs 为where中的占位符提供具体的值
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
TODO("Implement this to handle requests to delete one or more rows")
}
/**
* 插入数据
* @param uri 资源标识符,指定ContentProvider和表名
* @param values 插入的数据内容
*/
override fun insert(uri: Uri, values: ContentValues?): Uri? {
TODO("Implement this to handle requests to insert a new row.")
}
/**
* 获取数据
* @param uri 资源标识符,指定ContentProvider和表名
* @param projection 指定查询的列名
* @param selection 指定where的约束条件
* @param selectionArgs 为where中的占位符提供具体的值
* @param sortOrder 指定返回结果的排序方式
*/
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
TODO("Implement this to handle query requests from clients.")
}
/**
* 更新数据
* @param uri 资源标识符,指定ContentProvider和表名
* @param values 指定更新后的数据
* @param selection 指定where的约束条件
* @param selectionArgs 为where中的占位符提供具体的值
*/
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?
): Int {
TODO("Implement this to handle requests to update one or more rows.")
}
/**
* ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvider时 由系统进行调用
* 通常会在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,返回false则表示失败。
* 注:运行在主线程,不能做耗时操作
*/
override fun onCreate(): Boolean {
TODO("Implement this to initialize your content provider on startup.")
}
/**
* 得到数据类型,即返回当前 Url 所代表数据的MIME类型
*/
override fun getType(uri: Uri): String? {
TODO(
"Implement this to handle requests for the MIME type of the data" +
"at the given URI"
)
}
}
我们可以看到,实际上其主要是实现的就是增删改查,这样我们就可以很容易理解了,在自定义中的ContentProvider类中,我们对数据库中的数据进行增删改查操作,然后外部应用通过ContentProvider的uri以及ContentResolver来调用ContentProvider对应的方法,从而就实现了对我们应用的数据的访问。也就是说,我们需要在ContentProvider中对数据进行对应操作。如下图:
明白这些东西后,我们就可以对自定义的Content Provider进行代码实现了,如下:
class MyContentProvider : ContentProvider() {
//uri匹配
private var uriMatcher : UriMatcher ?= null
companion object{
const val USER_ITEM = 0//user表单条数据类型
const val USER_DIR = 1//user表多条数据类型
const val BOOK_ITEM = 2//book表单条数据类型
const val BOOK_DIR = 3//book表多条数据类型
//该ContentProvider的authority
const val AUTHORITY = "com.example.learnproject.provider"
}
init {
uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
uriMatcher?.apply {
//匹配对应的uri
addURI(AUTHORITY, "user", USER_DIR)
addURI(AUTHORITY, "user/#", USER_ITEM)
addURI(AUTHORITY, "book", BOOK_DIR)
addURI(AUTHORITY, "book/#", BOOK_ITEM)
}
}
/**
* 删除数据
* @param uri 资源标识符,指定ContentProvider和表名
* @param selection 指定where的约束条件
* @param selectionArgs 为where中的占位符提供具体的值
* @return 返回删除行数
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
Log.e("test_bug", "myContentProvider--delete")
when(uriMatcher?.match(uri)){
USER_DIR -> {
Log.e("test_bug", "myContentProvider--删除user表多条数据")
}
USER_ITEM -> {
Log.e("test_bug", "myContentProvider--删除user一条数据")
}
BOOK_ITEM -> {
Log.e("test_bug", "myContentProvider--删除book表一条数据")
}
BOOK_DIR -> {
Log.e("test_bug", "myContentProvider--删除book表多条数据")
}
}
return 0
}
/**
* 插入数据
* @param uri 资源标识符,指定ContentProvider和表名
* @param values 插入的数据内容
* @return 返回一个用于表示这条新记录的URI
*/
override fun insert(uri: Uri, values: ContentValues?): Uri? {
Log.e("test_bug", "myContentProvider--insert")
when(uriMatcher?.match(uri)){
USER_DIR -> {
Log.e("test_bug", "myContentProvider--向user表插入多条数据")
}
USER_ITEM -> {
Log.e("test_bug", "myContentProvider--向user表插入一条数据")
}
BOOK_ITEM -> {
Log.e("test_bug", "myContentProvider--向book表插入一条数据")
}
BOOK_DIR -> {
Log.e("test_bug", "myContentProvider--向book表插入多条数据")
}
}
return null
}
/**
* 获取数据
* @param uri 资源标识符,指定ContentProvider和表名
* @param projection 指定查询的列名
* @param selection 指定where的约束条件
* @param selectionArgs 为where中的占位符提供具体的值
* @param sortOrder 指定返回结果的排序方式
* @return 返回查询结果cursor对象
*/
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
Log.e("test_bug", "myContentProvider--query")
when(uriMatcher?.match(uri)){
USER_DIR -> {
Log.e("test_bug", "myContentProvider--向user表查询多条数据")
}
USER_ITEM -> {
Log.e("test_bug", "myContentProvider--向user表查询一条数据")
}
BOOK_ITEM -> {
Log.e("test_bug", "myContentProvider--向book表查询一条数据")
}
BOOK_DIR -> {
Log.e("test_bug", "myContentProvider--向book表查询多条数据")
}
}
return null
}
/**
* 更新数据
* @param uri 资源标识符,指定ContentProvider和表名
* @param values 指定更新后的数据
* @param selection 指定where的约束条件
* @param selectionArgs 为where中的占位符提供具体的值
* @return 返回更新数据行数
*/
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?
): Int {
Log.e("test_bug", "myContentProvider--update")
when(uriMatcher?.match(uri)){
USER_DIR -> {
Log.e("test_bug", "myContentProvider--向user表更新多条数据")
}
USER_ITEM -> {
Log.e("test_bug", "myContentProvider--向user表更新一条数据")
}
BOOK_ITEM -> {
Log.e("test_bug", "myContentProvider--向book表更新一条数据")
}
BOOK_DIR -> {
Log.e("test_bug", "myContentProvider--向book表更新多条数据")
}
}
return 0
}
/**
* ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvider时 由系统进行调用
* 通常会在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,返回false则表示失败。
* 注:运行在主线程,不能做耗时操作
*/
override fun onCreate(): Boolean {
Log.e("test_bug", "myContentProvider--onCreate")
Log.e("test_bug", "myContentProvider--获取数据库实例")
return true
}
/**
* 得到数据类型,即返回当前 Url 所代表数据的MIME类型
*/
override fun getType(uri: Uri): String? {
Log.e("test_bug", "myContentProvider--getType")
return when(uriMatcher?.match(uri)){
USER_DIR -> {
"vnd.android.cursor.dir/vnd.com.example.learnproject.provider.user"
}
USER_ITEM -> {
"vnd.android.cursor.item/vnd.com.example.learnproject.provider.user"
}
BOOK_ITEM -> {
"vnd.android.cursor.item/vnd.com.example.learnproject.provider.book"
}
BOOK_DIR -> {
"vnd.android.cursor.dir/vnd.com.example.learnproject.provider.book"
}
else -> null
}
}
}
这里我们并没有真正进行数据操作,但实际上,我们只需要在update、insert、query和delete方法中对数据库中或者是SP中的数据进行相应操作即可,这里我们只是输出了对应的log,我们主要明白使用方法即可,数据操作的方法完全和我们对数据库进行的一般操作相同。
然后,我们即可在另一个应用中对该ContentProvider进行数据访问,我们创建四个按钮,设置点击事件分别进行增删改查,如下:
override fun onClick(p0: View?) {
when(p0?.id){
R.id.insert -> {
val uri = Uri.parse("content://com.example.learnproject.provider/user/1")
contentResolver.insert(uri, ContentValues())
}
R.id.delete -> {
val uri = Uri.parse("content://com.example.learnproject.provider/user")
contentResolver.delete(uri, null, null)
}
R.id.update -> {
val uri = Uri.parse("content://com.example.learnproject.provider/book")
contentResolver.update(uri, ContentValues(), null, null)
}
R.id.query -> {
val uri = Uri.parse("content://com.example.learnproject.provider/book/2")
contentResolver.query(uri, null, null, null, null)
}
}
}
点击各个按钮,输出日志如下图:
我们可以看到,在另一个应用中使用ContentResolver访问本应用数据时,ContentProvider被创建,然后contentResolver调用的数据访问方法,也会对应的调用ContentProvider的方法,从而实现跨进程访问。而在ContentProvider中我们可以通过匹配uri的方式,来选择哪些数据能够被访问,哪些数据能被删除等,从而实现了数据访问的安全性。而且由于ContentProvider只提供了对外访问的接口,因此无论底层数据采用何种方式进行存储,外界访问方式都是统一的,这也使得访问变得更加简单高效。
总结
ContentProvider在我们的日常开发中,如果不需要去开发与其它应用程序共享数据的APP,那么我们其实是很少使用到的,内容相对而言也较少,一般只用于获取系统应用的数据,如联系人信息、短信等。但是,作为Android四大组件之一,其重要性不言而喻,而其所包含的跨进程通信的Binder机制和思想,也是一个重要且有难度的知识点,需要认真学习一下。
推荐阅读
-
Android开发学习笔记之通过API接口将LaTex数学函数表达式转化为图片形式
-
laravel框架学习笔记之组件化开发实现方法
-
Android学习笔记之——UI组件/RelativeLayout(相对布局)
-
Android开发四大组件之Service如何使用(与Activity通信)
-
Android学习笔记之——UI组件/LinearLayout(线性布局)
-
Android开发学习笔记——四大组件之ContentProvider
-
Android学习笔记之——UI组件/Button
-
学习笔记之——基于ROS的Android开发
-
Android学习笔记之——UI组件/TextView
-
Android开发学习笔记——Jetpack之WorkManager