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(资源路径)

c938968c941fc4a279c8280d57d3b50f.png

其中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()

}

}

输出结果如下:

3c932048c4d2e70280e85e90daa6c710.png

我们可以看到使用方法很简单,其实际上和数据库的操作方法差不多,只是我们是通过uri和ContentResolver来获取到数据的。注意:读取通讯录是需要权限的,因此要先申请权限。

创建ContentProvider提供外部访问接口

我们已经学习了如何在自己的程序中访问系统的通讯录,如果我们需要访问其它应用数据,思路也是一样的,不同就是Uri以及获取到数据后的操作。但是,我们发现实际上我们并没有用到ContentProvider类,因为ContentProvider是内容提供者,是提供数据访问接口的,也就是说在系统通讯录中实现了对应的ContentProvider,接下来,就让我们来学习下,如何使用ContentProvider为自己的应用提供外部访问接口。我们所需要做的就是继承ContentProvider,并实现其中的方法。

首先,和所有Android的四大组件一样,ContentProvider需要在AndroidManifest中注册才可以使用,当然如果我们使用AndroidStudio创建,AS会自动为我们进行注册,如下图:

7eb5eb3c2cccd56d3d2060fb2601b2b7.png

d852a8dc9f0991d2ce754348baa0974c.png

在创建时,我们需要指定ContentProvider的Uri的authorities,其中exported属性为是否暴露给外部使用,注册后AndroidManifest文件中代码如下:

android:name=".contentprovider.MyContentProvider"

android:authorities="com.example.learnproject.provider"

android:enabled="true"

android:exported="true">

具体需要实现的方法如下:

class MyContentProvider : ContentProvider() {

/**

* 删除数据

* @param uri 资源标识符,指定ContentProvider和表名

* @param selection 指定where的约束条件

* @param selectionArgs 为where中的占位符提供具体的值

*/

override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): 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?, selection: String?, selectionArgs: Array?, 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?

): 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中对数据进行对应操作。如下图:

4699ab77bea1b6cfd3d4f6f167e7923a.png

明白这些东西后,我们就可以对自定义的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?): 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?, selection: String?, selectionArgs: Array?, 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?

): 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)

}

}

}

点击各个按钮,输出日志如下图:

347bc79312a3bbf0a7b51ca8ba6c3d71.png

我们可以看到,在另一个应用中使用ContentResolver访问本应用数据时,ContentProvider被创建,然后contentResolver调用的数据访问方法,也会对应的调用ContentProvider的方法,从而实现跨进程访问。而在ContentProvider中我们可以通过匹配uri的方式,来选择哪些数据能够被访问,哪些数据能被删除等,从而实现了数据访问的安全性。而且由于ContentProvider只提供了对外访问的接口,因此无论底层数据采用何种方式进行存储,外界访问方式都是统一的,这也使得访问变得更加简单高效。

总结

ContentProvider在我们的日常开发中,如果不需要去开发与其它应用程序共享数据的APP,那么我们其实是很少使用到的,内容相对而言也较少,一般只用于获取系统应用的数据,如联系人信息、短信等。但是,作为Android四大组件之一,其重要性不言而喻,而其所包含的跨进程通信的Binder机制和思想,也是一个重要且有难度的知识点,需要认真学习一下。

本文地址:https://blog.csdn.net/qq_36425800/article/details/108177263

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值