【Android】四大组件之 ContentProvider

前言

ContentProvider 是 Android 的四大组件之一,有时候我们需要操作其他应用程序的一些数据,就会用到 ContentProvider,ContentProvider 本质上是一个中间者,真正 存储和操作数据 的数据源还是原来存储数据的方式,如数据库、文件或网络等。

ContentProvider 以相对安全的方式封装了数据并提供简易的处理机制和统一的访问接口供其他程序调用。它的底层采用了 Binder 机制来实现,ContentProvider 为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给 其他应用进行增删改查,而不用担心因为直接开放数据库权限而带来的安全问题。当然,ContentProvider不仅可以实现跨进程通信,也可实现进程内的通信。

在 Android 中,为一些常见的数据提供了默认的 ContentProvider,如通讯录等。

ContentProvider 是一个抽象类,如果我们需要自定义内容提供者我们就需要继承 ContentProvider 类并复写它的方法,如下:

class MyContentProvider : ContentProvider() {
    
    
    
    override fun onCreate(): Boolean {
    
    
        // 在创建 ContentProvider 时使用,通常会在这里完成对数据库的创建和升级等操作,返回true表示初始化成功,返回false则表示失败。
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
    
    
        // 用于查询指定 uri 的数据,返回一个 Cursor
    }

    override fun getType(uri: Uri): String? {
    
    
        // 用于返回指定的 Uri 中的数据 MIME 类型
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
    
    
        // 用于向指定uri的 ContentProvider 中添加数据
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
    
    
        // 用于删除指定 uri 的数据
    }

    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
    
    
        // 用户更新指定 uri 的数据
    }
}

注意:访问数据的方法 insert,delete 和 update 可能被多个线程同时调用,操作数据时,务必要保证线程是安全的

在了解 ContentProvider 使用之前,我们需要对其涉及到的一些概念有一定的了解,如 URI、MIME 等。

URI

URI,Universal Resource Identifier,统一资源定位符。其它应用可以通过 ContentResolver 来访问 ContentProvider 提供的数据,而 ContentResolver 通过 uri 来定位自己要访问的数据。

URI 为系统中的每一个资源赋予一个名字,比方说通话记录。

每一个 ContentProvider 都拥有一个公共的 URI,用于表示 ContentProvider 所提供的数据。

URI 的格式如下:

  • scheme,标准前缀,一般就是 content://
  • host:port,URI 的标识,如:com.yang.provider.myprovider 。用于标识 ContentProvider,外部调用者可根据标识来找到它。标识必须是完整的小写的类名。一般是 包.类
  • path,表示要操作的数据库中表的名字,也可以自己定义,记得在使用的时候保持一致就可以了
  • query,表示要查询的表中的某条索引对应的数据。如果不写这个参数,就表示返回表中全部数据

URI 的示例如下:

// 规则
[scheme:][//host:port][path][?query]
// 示例
content://com.yang.provider.myprovider/tablename/id

MIME

MIME 是指定某个扩展名的文件用一种应用程序来打开,就像你用浏览器查看 PDF 格式的文件,浏览器会选择合适的应用来打开一样。

Android ContentProvider 会根据 URI 来返回 MIME 类型,ContentProvider 会返回一个包含两部分的字符串。

MIME 类型一般包含两部分,如:

text/html
text/css
text/xml
application/pdf

分为类型和子类型,Android 遵循类似的约定来定义MIME类型,每个内容类型的 Android MIME 类型有两种形式:多条记录(集合)和单条记录。

  • 集合记录(dir):vnd.android.cursor.dir/自定义
  • 单条记录(item):vnd.android.cursor.item/自定义

vnd 表示这些类型和子类型具有非标准的供应商特定的形式。Android中类型已经固定好了,不能更改,只能区别是集合还是单条具体记录,子类型可以按照格式自己填写。

在使用 Intent 时,也会用到 MIME,根据 Mimetype 打开符合条件的活动。

ContentProvider 中,我们可以根据 URI 返回MIME类型,代码如下:

val uri = Uri.parse("content://com.yang.provider.myprovider/tablename")
val type: String? = requireActivity().contentResolver.getType(uri)

UriMatcher and ContentUris

Uri 代表要操作的数据,在开发过程中对数据进行获取时需要解析 Uri 。

Android 提供了两个用于操作 Uri 的工具类,分别为 UriMatcher 和 ContentUris 。

UriMatcher

UriMatcher 类用于匹配 Uri,它的使用步骤如下:

**步骤一:**将需要匹配的Uri路径进行注册,代码如下:

// 常量 UriMatcher.NO_MATCH 表示不匹配任何路径的返回码
UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 如果 match() 方法匹配 content://com.wang.provider.myprovider/tablename 路径,返回匹配码为 1
sMatcher.addURI("content://com.yang.provider.myprovider", " tablename ", 1);
// 如果 match() 方法匹配 content://com.wang.provider.myprovider/tablename/11 路径,返回匹配码为 2
sMatcher.addURI("com.yang.provider.myprovider", "tablename/#", 2);

路径后面的 id 采用了通配符形式 # ,表示只要前面三个部分都匹配上就算是匹配成功。

**步骤二:**使用 sMatcher.match(Uri) 方法对输入的 Uri 进行匹配,如果匹配就返回对应的匹配码,即 addURI() 方法的第三个参数

int code = sMatcher.match(Uri.parse("content://com.yang.provider.myprovider/tablename/100"))
switch (code) {
    
    
    case 1:
        // match 1, todo something
        break;
    case 2
        // match 2, todo something
        break;
    default:
        // match nothing, todo something
        break;
}

ContentUris

ContentUris 类用于操作 Uri 路径后面的 ID 部分,它有两个比较实用的方法:

  • withAppendedId(Uri uri, long id) :用于为路径加上 ID 部分,如:
// 生成后的 Uri 为:content://com.yang.provider.myprovider/user/7
Uri uri = Uri.parse("content://com.yang.provider.myprovider/user")
Uri resultUri = ContentUris.withAppendedId(uri, 7); 
  • parseId(Uri uri):用于从路径中获取 ID 部分,如:
Uri uri = Uri.parse("com.yang.provider.myprovider/user/7")
// 获取的结果为 7
long personid = ContentUris.parseId(uri);

ContentResolver

其他 app 或进程想要操作 ContentProvider,需要先获取其相应的 ContentResolver,再利用 ContentResolver 类来完成对数据的增删改查操作。

为什么要使用通过 ContentResolver 类从而与 ContentProvider 类进行交互,而不直接访问ContentProvider类呢?ContentResolver 类是对所有的 ContentProvider 进行统一管理的,这样,调用者就不必了解每个不同的 ContentProvider 的实现,只需要关注如何通过 ContentResolver 操作 ContentProvider 就行了。

使用 ContentResolver 对 ContentProvider 中的数据进行操作的代码如下:

val uri = Uri.parse("content://com.yang.provider.myprovider/tablename")

// 构建 ContentResolver
val resolver: ContentResolver = requireActivity().contentResolver

// 添加一条记录
resolver.insert(uri, ContentValues().apply {
    
    
    put("name", "yang")
    put("age", 18)
})

// 获取 tablename 表中所有记录
val cursor = resolver.query(uri, null, null, null, "tablename data")
while (cursor!!.moveToNext()) {
    
    
    // todo use data..
}

// 把 id 为 1 的记录的 name 字段值更改新为 zhang
val updateIdUri = ContentUris.withAppendedId(uri, 1)
resolver.update(updateIdUri, ContentValues().apply {
    
    
    put("name", "zhang1")
}, null, null)

// 删除 id 为 2 的记录
val deleteIdUri = ContentUris.withAppendedId(uri, 2)
resolver.delete(deleteIdUri, null, null)

如果 ContentProvider 的访问者需要知道数据发生的变化,可以在 ContentProvider 发生数据变化时调用如下代码通知注册在此 URI 上的访问者:

val uri = Uri.parse("content://com.yang.provider.myprovider/tablename")
// notifyChange 方法的第二个参数为 observer对象,如果传 null 表示发送消息给任何人
requireActivity().contentResolver.notifyChange(uri, null)

同时,访问者需使用 ContentObserver 对数据进行监听:

private val observer = object : ContentObserver(null) {
    
    
    override fun onChange(selfChange: Boolean, uri: Uri?) {
    
    
        // todo
    }
}
private val uri = Uri.parse("content://com.yang.provider.myprovider/tablename")

// 注册监听
// 其中,第二个参数为 false 表示精确匹配,即只匹配该Uri;为 true 表示可以同时匹配其派生的 Uri
requireActivity().contentResolver.registerContentObserver(uri, true, observer)
// 解除注册
requireActivity().contentResolver.unregisterContentObserver(observer)

代码演示

进程内通信 Demo 演示

步骤一:创建数据库类,数据库中存在两个表,分别是 user 和 job 表。

class DBHelper(
    context: Context
) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    
    

    override fun onCreate(db: SQLiteDatabase) {
    
    
        // 创建两个表格:用户表 和 职业表
        db.execSQL("CREATE TABLE IF NOT EXISTS $USER_TABLE_NAME(_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)")
        db.execSQL("CREATE TABLE IF NOT EXISTS $JOB_TABLE_NAME(_id INTEGER PRIMARY KEY AUTOINCREMENT, job TEXT)")
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
    
    }

    companion object {
    
    
        // 数据库名
        private const val DATABASE_NAME = "finch.db"

        // 表名
        const val USER_TABLE_NAME = "user"
        const val JOB_TABLE_NAME = "job"

        //数据库版本号
        private const val DATABASE_VERSION = 1
    }
}

步骤二:创建一个 MyContentProvider ,继承自 ContentProvider 抽象类。其中,在 onCreate() 方法中,先对数据库初始化,并往数据库中的 user 表和 job 表分别添加两条数据。

class MyContentProvider : ContentProvider() {
    
    
    private lateinit var mDbHelper: DBHelper
    private lateinit var db: SQLiteDatabase

    private val mMatcher: UriMatcher by lazy {
    
    
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(AUTHORITY, "user", User_Code)
        matcher.addURI(AUTHORITY, "job", Job_Code)
        matcher
    }

    override fun onCreate(): Boolean {
    
    
        mDbHelper = DBHelper(context!!)
        db = mDbHelper.writableDatabase
        // 初始化两个表的数据 (先清空两个表,再各加入一个记录)
        db.execSQL("delete from user")
        db.execSQL("insert into user values(1,'yang');")
        db.execSQL("insert into user values(2,'zhang');")
        db.execSQL("delete from job")
        db.execSQL("insert into job values(1,'Android');")
        db.execSQL("insert into job values(2,'iOS');")
        return true
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri {
    
    
        val table = getTableName(uri)
        db.insert(table, null, values)
        // 通知外部调用者数据发生变化
        context?.contentResolver?.notifyChange(uri, null)
        return uri
    }

    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? {
    
    
        val table = getTableName(uri)
        return db.query(table, projection, selection, selectionArgs, null, null, sortOrder, null)
    }

    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ): Int {
    
    
        return 0
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
    
    
        return 0
    }

    override fun getType(uri: Uri): String? {
    
    
        return null
    }

    private fun getTableName(uri: Uri): String? {
    
    
        var tableName: String? = null
        when (mMatcher.match(uri)) {
    
    
            User_Code -> tableName = DBHelper.USER_TABLE_NAME
            Job_Code -> tableName = DBHelper.JOB_TABLE_NAME
        }
        return tableName
    }

    companion object {
    
    
        private const val AUTHORITY = "com.yang.provider.myprovider"
        const val User_Code = 1
        const val Job_Code = 2
    }
}

步骤三:在 androidManifest.xml 中注册 MyContentProvider

<provider
    android:name=".fragment.MyContentProvider"
    android:authorities="com.yang.provider.myprovider" />

步骤四:进程内使用 ContentResolver 操作 ContentProvider。

private fun operateData() {
    
    
    val userUri = Uri.parse("content://com.yang.provider.myprovider/user")
    val jobUri = Uri.parse("content://com.yang.provider.myprovider/job")

    // 向 user 表插入一条数据
    contentResolver.insert(userUri, ContentValues().apply {
    
    
        put("_id", 3)
        put("name", "chen")
    })

    // 查询 user 表数据
    val cursor = contentResolver.query(userUri, arrayOf("_id", "name"), null, null, null)
    while (cursor!!.moveToNext()) {
    
    
        println("query user:" + cursor.getInt(0) + " " + cursor.getString(1))
    }
    // 主动关闭游标
    cursor.close()

    // 向 job 表插入一条数据
    contentResolver.insert(jobUri, ContentValues().apply {
    
    
        put("_id", 3)
        put("job", "Web")
    })

    // 查询 job 表数据
    val cursor2 = contentResolver.query(jobUri, arrayOf("_id", "job"), null, null, null)
    while (cursor2!!.moveToNext()) {
    
    
        println("query job:" + cursor2.getInt(0) + " " + cursor2.getString(1))
    }
    cursor2.close()
}

// 执行 operateData() 方法,输出结果如下:
I/System.out: query user:1 yang
I/System.out: query user:2 zhang
I/System.out: query user:3 chen
I/System.out: query job:1 Android
I/System.out: query job:2 iOS
I/System.out: query job:3 Web

跨进程通信 Demo 演示

步骤一:创建数据库类,数据库中存在两个表,分别是 user 和 job 表。

class DBHelper(
    context: Context
) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    
    

    override fun onCreate(db: SQLiteDatabase) {
    
    
        // 创建两个表格:用户表 和 职业表
        db.execSQL("CREATE TABLE IF NOT EXISTS $USER_TABLE_NAME(_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)")
        db.execSQL("CREATE TABLE IF NOT EXISTS $JOB_TABLE_NAME(_id INTEGER PRIMARY KEY AUTOINCREMENT, job TEXT)")
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
    
    }

    companion object {
    
    
        // 数据库名
        private const val DATABASE_NAME = "finch.db"

        // 表名
        const val USER_TABLE_NAME = "user"
        const val JOB_TABLE_NAME = "job"

        //数据库版本号
        private const val DATABASE_VERSION = 1
    }
}

步骤二:创建一个 MyContentProvider ,继承自 ContentProvider 抽象类。其中,在 onCreate() 方法中,先对数据库初始化,并往数据库中的 user 表和 job 表分别添加两条数据。

class MyContentProvider : ContentProvider() {
    
    
    private lateinit var mDbHelper: DBHelper
    private lateinit var db: SQLiteDatabase

    private val mMatcher: UriMatcher by lazy {
    
    
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(AUTHORITY, "user", User_Code)
        matcher.addURI(AUTHORITY, "job", Job_Code)
        matcher
    }

    override fun onCreate(): Boolean {
    
    
        mDbHelper = DBHelper(context!!)
        db = mDbHelper.writableDatabase
        // 初始化两个表的数据 (先清空两个表,再各加入一个记录)
        db.execSQL("delete from user")
        db.execSQL("insert into user values(1,'yang');")
        db.execSQL("insert into user values(2,'zhang');")
        db.execSQL("delete from job")
        db.execSQL("insert into job values(1,'Android');")
        db.execSQL("insert into job values(2,'iOS');")
        return true
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri {
    
    
        val table = getTableName(uri)
        db.insert(table, null, values)
        // 通知外部调用者数据发生变化
        context?.contentResolver?.notifyChange(uri, null)
        return uri
    }

    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? {
    
    
        val table = getTableName(uri)
        return db.query(table, projection, selection, selectionArgs, null, null, sortOrder, null)
    }

    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ): Int {
    
    
        return 0
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
    
    
        return 0
    }

    override fun getType(uri: Uri): String? {
    
    
        return null
    }

    private fun getTableName(uri: Uri): String? {
    
    
        var tableName: String? = null
        when (mMatcher.match(uri)) {
    
    
            User_Code -> tableName = DBHelper.USER_TABLE_NAME
            Job_Code -> tableName = DBHelper.JOB_TABLE_NAME
        }
        return tableName
    }

    companion object {
    
    
        private const val AUTHORITY = "com.yang.provider.myprovider"
        const val User_Code = 1
        const val Job_Code = 2
    }
}

步骤三:在 androidManifest.xml 中注册 MyContentProvider

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="xxx.xxxx">
    <!--声明使用 MyContentProvider 的通信的权限 (开放所有权限) -->
    <permission
        android:name="com.yang.provider.myprovider.PROVIDER"
        android:protectionLevel="normal" />
    <!--声明使用 MyContentProvider 的通信的权限 (开放读权限) -->
    <permission
        android:name="com.yang.provider.myprovider.Read"
        android:protectionLevel="normal" />
    <!--声明使用 MyContentProvider 的通信的权限 (开放写权限) -->
    <permission
        android:name="com.yang.provider.myprovider.Write"
        android:protectionLevel="normal" />

    <application>
        <!--通过 android:permission、readPermission、writePermission 等属性定义 MyContentProvider 的通信权限-->
        <provider
            android:name=".MyContentProvider"
            android:authorities="com.yang.provider.myprovider"
            android:exported="true"
            android:permission="com.yang.provider.myprovider.PROVIDER"
            // android:readPermission="com.yang.provider.myprovider.Read"
            // android:writePermission="com.yang.provider.myprovider.Write"
            />

    </application>
</manifest>

至此,创建进程一的代码编写完毕。

接下来继续编写进程二的代码,步骤如下。

步骤一:声明访问进行 1 中的 ContentProvider 所需要的权限

<!--
声明本应用可允许通信的权限(全权限)
另外,还可以单独声明读或写权限,如:
<uses-permission android:name="com.yang.provider.myprovider.Read" />
<uses-permission android:name="com.yang.provider.myprovider.Write" />
注:声明的权限必须与进程1中设置的权限对应
-->
<uses-permission android:name="com.yang.provider.myprovider.PROVIDER" />

步骤二:使用 ContentResolver 操作 进程 1 的 ContentProvider。

private fun operateData() {
    
    
    val userUri = Uri.parse("content://com.yang.provider.myprovider/user")
    val jobUri = Uri.parse("content://com.yang.provider.myprovider/job")

    // 对 user 表进行操作
    contentResolver.insert(userUri, ContentValues().apply {
    
    
        put("_id", 4)
        put("name", "huang")
    })

    val cursor = contentResolver.query(userUri, arrayOf("_id", "name"), null, null, null)
    while (cursor!!.moveToNext()) {
    
    
        println("ipc query user:" + cursor.getInt(0) + " " + cursor.getString(1))
    }
    cursor.close()

    // 对 job 表进行操作
    contentResolver.insert(jobUri, ContentValues().apply {
    
    
        put("_id", 4)
        put("job", "algorithm")
    })

    val cursor2 = contentResolver.query(jobUri, arrayOf("_id", "job"), null, null, null)
    while (cursor2!!.moveToNext()) {
    
    
        println("ipc query job:" + cursor2.getInt(0) + " " + cursor2.getString(1))
    }
    cursor2.close()
}

// 执行 operateData() 方法,输出结果如下:
I/System.out: ipc query user:1 yang
I/System.out: ipc query user:2 zhang
I/System.out: ipc query user:4 huang
I/System.out: ipc query job:1 Android
I/System.out: ipc query job:2 iOS
I/System.out: ipc query job:4 algorithm

猜你喜欢

转载自blog.csdn.net/yang553566463/article/details/124869027