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

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

  • ContentProvider
    • 简介
    • 跨进程通信概述
    • 基本使用
      • 相关知识
        • ContentResolver
        • URI
        • MIME数据类型
      • 使用ContentProvider访问其它应用
      • 创建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(资源路径)
Android开发学习笔记——四大组件之ContentProvider_第1张图片
其中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会自动为我们进行注册,如下图:
Android开发学习笔记——四大组件之ContentProvider_第2张图片
Android开发学习笔记——四大组件之ContentProvider_第3张图片
在创建时,我们需要指定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_第4张图片
明白这些东西后,我们就可以对自定义的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_第5张图片
我们可以看到,在另一个应用中使用ContentResolver访问本应用数据时,ContentProvider被创建,然后contentResolver调用的数据访问方法,也会对应的调用ContentProvider的方法,从而实现跨进程访问。而在ContentProvider中我们可以通过匹配uri的方式,来选择哪些数据能够被访问,哪些数据能被删除等,从而实现了数据访问的安全性。而且由于ContentProvider只提供了对外访问的接口,因此无论底层数据采用何种方式进行存储,外界访问方式都是统一的,这也使得访问变得更加简单高效。

总结

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

你可能感兴趣的:(Android开发学习笔记,Android,android)