欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android开发学习笔记——四大组件之ContentProvider

程序员文章站 2022-05-16 14:33:51
...

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(资源路径)
Android开发学习笔记——四大组件之ContentProvider
其中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()
    }
}

输出结果如下:
Android开发学习笔记——四大组件之ContentProvider
我们可以看到使用方法很简单,其实际上和数据库的操作方法差不多,只是我们是通过uri和ContentResolver来获取到数据的。注意:读取通讯录是需要权限的,因此要先申请权限。

创建ContentProvider提供外部访问接口

我们已经学习了如何在自己的程序中访问系统的通讯录,如果我们需要访问其它应用数据,思路也是一样的,不同就是Uri以及获取到数据后的操作。但是,我们发现实际上我们并没有用到ContentProvider类,因为ContentProvider是内容提供者,是提供数据访问接口的,也就是说在系统通讯录中实现了对应的ContentProvider,接下来,就让我们来学习下,如何使用ContentProvider为自己的应用提供外部访问接口。我们所需要做的就是继承ContentProvider,并实现其中的方法。
首先,和所有Android的四大组件一样,ContentProvider需要在AndroidManifest中注册才可以使用,当然如果我们使用AndroidStudio创建,AS会自动为我们进行注册,如下图:
Android开发学习笔记——四大组件之ContentProvider
Android开发学习笔记——四大组件之ContentProvider
在创建时,我们需要指定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中对数据进行对应操作。如下图:
Android开发学习笔记——四大组件之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)
            }
        }
    }

点击各个按钮,输出日志如下图:
Android开发学习笔记——四大组件之ContentProvider
我们可以看到,在另一个应用中使用ContentResolver访问本应用数据时,ContentProvider被创建,然后contentResolver调用的数据访问方法,也会对应的调用ContentProvider的方法,从而实现跨进程访问。而在ContentProvider中我们可以通过匹配uri的方式,来选择哪些数据能够被访问,哪些数据能被删除等,从而实现了数据访问的安全性。而且由于ContentProvider只提供了对外访问的接口,因此无论底层数据采用何种方式进行存储,外界访问方式都是统一的,这也使得访问变得更加简单高效。

总结

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