ContentProvider 真的是徒有虚名吗?

143 阅读4分钟

我们知道,Android 四大组件为 Activity,Service,BroadcastReceiver 和 ContentProvider,ContentProvider 作为四大组件之一,似乎有些名不副实了,因为在一般应用开发中,使用的次数很少,但它真的是徒有虚名吗?

什么是 ContentProvider

ContentProvider 主要用于在不同的应用程序之间实现数据共享的功能,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。

什么是 URI

URI 全称是通用资源标识符,URI 给 ContentProvider 中的数据建立唯一标识符,主要由两部分组成:

  • authority:用于对不同的应用程序做区分的,一般为了避免冲突,会采用应用包名的方式进行命名,比如某个应用的包名是 com.example.myapplication,那么该应用对应的 authority 就可以命名为 com.example.myapplication.provider。
  • path:用于对同一应用程序中不同的表做区分的,添加到 authority 的后面。比如某个应用的数据库里存在一张表 table,这时就可以将 path 分别命名为 /table。

然后把 authority 和 path 进行组合,标准的内容 URI 如下:

content://com.example.myapplication.provider/table

内容 URI 的格式主要有两种,以路径结尾表示访问该表中所有的数据,如上所示,以 id 结尾表示访问该表中拥有相应 id 的数据,如下所示:

content://com.example.myapplication.provider/table/1

我们可以使用通配符分别匹配这两种格式的内容 URI,* 表示匹配任意长度的任意字符。# 表示匹配任意长度的数字。

一个匹配任意表的内容 URI 可以写成

content://com.example.myapplication.provider/*

一个能够匹配 table 表中任意一行数据的内容 URI 可以写成

content://com.example.myapplication.provider/table/#

由此可见,内容 URI 可以清楚地表达我们想要访问哪个程序中哪张表里的数据。但是,有内容 URI 字符串还不行,还需要把它解析成 Uri 对象才可以作为参数传入。

val uri = Uri.parse("content://com.example.myapplication.provider/table1")

什么是 ContentResolver

如果想要访问 ContentProvider 中共享的数据,就一定要借助 ContentResolver 类,ContentResolver 中提供了一系列的方法用于对数据进行增删改查的操作。

ContentResolver 的使用

这里以读取系统联系人为例,先需要声明权限,这里使用 PermissionX

implementation 'com.guolindev.permissionx:permissionx:1.7.1'
<uses-permission android:name="android.permission.READ_CONTACTS" />
PermissionX.init(this)
    .permissions(Manifest.permission.READ_CONTACTS)
    .request { allGranted, _, deniedList ->
        if (allGranted) {
            Toast.makeText(this, "All permissions are granted", Toast.LENGTH_LONG).show()
        } else {
            Toast.makeText(
                this,
                "These permissions are denied: $deniedList",
                Toast.LENGTH_LONG
            ).show()
        }
    }

权限授予之后,我们就可以读取联系人数据了。

contentResolver.query(
    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    null,
    null,
    null
)?.run {
    while (moveToNext()) {
        val name =
            getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
        val phoneNumber =
            getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
        Log.i(tag, "name: $name,phoneNumber: $phoneNumber")
    }
    close()
}

创建 ContentProvider

在此之前,我们需要先创建一个数据库,这里使用 Jetpack Room 来操作 SQLite。 用到的依赖如下:

plugins {
    ...
    id 'kotlin-kapt'
}
def room_version = "2.4.3"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation "androidx.activity:activity-ktx:1.5.1"

这里创建一个名为 Project 的表

@Entity
data class Project(
    @PrimaryKey var id: String,
    var technology: String,
    var charge: String
)
@Database(entities = [Project::class], version = 1)
abstract class AppDatabase : RoomDatabase() {

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            val instance = INSTANCE
            instance?.let { return it }
            synchronized(this) {
                val db = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "database_name"
                ).build()
                INSTANCE = db
                return db
            }
        }
    }
}

数据库创建好之后,我们就可以创建 ContentProvider 将该数据库操作提供出去了。在此过程中,我们需要使用 UriMatcher 来匹配内容 URI。UriMatcher 中有一个 addURI 的方法,这个方法接收三个参数,可以分别把authority,path 和 code 传进去即可。

11.png

Android Studio 创建 ContentProvider 之后,便可以往里面加入我们的逻辑了,如下所示:

<provider
    android:name=".MyContentProvider"
    android:authorities="com.example.myapplication.provider"
    android:enabled="true"
    android:exported="true"/>
class MyContentProvider : ContentProvider() {

    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
    private val tableName = "project"
    private val dirCode = 1
    private val itemCode = 2

    //删除数据,uri 表示要删除数据的表,selection 和 selectionArgs 约束删除哪些行,返回被删除的行数。
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) =
        when (uriMatcher.match(uri)) {
            dirCode -> AppDatabase.getDatabase(context!!).openHelper.writableDatabase.delete(
                tableName,
                selection,
                selectionArgs
            )
            itemCode -> {
                val id = uri.pathSegments[1]
                AppDatabase.getDatabase(context!!).openHelper.writableDatabase.delete(
                    tableName,
                    "id = ?",
                    arrayOf(id)
                )
            }
            else -> 0
        }


    ////根据传入的内容 URI 返回相应的 MIME 类型
    override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
        dirCode -> "vnd.android.cursor.dir/vnd.com.example.myapplication.provider.project"
        itemCode -> "vnd.android.cursor.item/vnd.com.example.myapplication.provider.project"
        else -> null
    }

    //添加数据,uri 确定要添加的表,values 表示要添加的数据,返回一个用于表示这条新记录的 Uri。
    override fun insert(uri: Uri, values: ContentValues?) = when (uriMatcher.match(uri)) {
        dirCode, itemCode -> {
            val rowId = AppDatabase.getDatabase(context!!).openHelper.writableDatabase.insert(
                tableName,
                SQLiteDatabase.CONFLICT_REPLACE,
                values
            )
            ContentUris.withAppendedId(uri, rowId)
        }
        else -> null
    }

    /* 初始化 ContentProvider 的时候调用。返回 true 表示初始化成功,返回 false 表示失败。初始化在 
       Application 的 attachBaseContext 和 onCreate 之间。*/
    override fun onCreate(): Boolean {
        uriMatcher.addURI("com.example.myapplication.provider", tableName, dirCode)
        uriMatcher.addURI("com.example.myapplication.provider", "$tableName/#", itemCode)
        return true
    }

    /* 查询数据:参数 uri 确定查询哪张表,projection 确定查询哪些列,selection 和 selectionArgs 
      约束查询哪些行,sortOrder 结果排序,查询结果存在 Cursor 对象中返回。*/
    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? = when (uriMatcher.match(uri)) {
        dirCode -> AppDatabase.getDatabase(context!!).openHelper.readableDatabase.query(
            SupportSQLiteQueryBuilder.builder(tableName)
                .selection(selection, selectionArgs)
                .columns(projection)
                .orderBy(sortOrder)
                .create()
        )

        itemCode -> {
            val id = uri.pathSegments[1]
            AppDatabase.getDatabase(context!!).openHelper.readableDatabase.query(
                SupportSQLiteQueryBuilder.builder(tableName)
                    .selection("id = ?", arrayOf(id))
                    .columns(projection)
                    .orderBy(sortOrder)
                    .create()
            )
        }
        else -> null
    }

    /* 更新已有数据,uri 表示要更新数据的表,values 表示要更新的数据,selection 和 selectionArgs 
       约束更新哪些行,返回更新的行数。*/
    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ) = when (uriMatcher.match(uri)) {
        dirCode -> AppDatabase.getDatabase(context!!).openHelper.writableDatabase.update(
            tableName,
            SQLiteDatabase.CONFLICT_REPLACE,
            values,
            selection,
            selectionArgs
        )

        itemCode -> {
            val id = uri.pathSegments[1]
            AppDatabase.getDatabase(context!!).openHelper.writableDatabase.update(
                tableName,
                SQLiteDatabase.CONFLICT_REPLACE,
                values,
                "id = ?",
                arrayOf(id)
            )
        }
        else -> 0
    }

}

此时,该应用已用于跨进程共享数据的能力了,我们再新建一个 Project 来作为调用方,需要注意的是,调用方需要在 AndroidManifest 中 application 同级的位置添加如下代码才可操作。

<queries>
    <provider android:authorities="com.example.myapplication.provider" />
</queries>

调用方添加数据

val uri = Uri.parse("content://com.example.myapplication.provider/project")
val value = contentValuesOf("id" to "1", "technology" to "web", "charge" to "Taylor")
contentResolver.insert(uri, value)

调用方删除数据

val uri = Uri.parse("content://com.example.myapplication.provider/project/1")
contentResolver.delete(uri, null, null)
val uri = Uri.parse("content://com.example.myapplication.provider/project")
contentResolver.delete(uri, "id = ?", arrayOf("1"))

调用方更新数据

val uri = Uri.parse("content://com.example.myapplication.provider/project")
val value = contentValuesOf("id" to "520", "technology" to "ios", "charge" to "Justin")
contentResolver.update(uri, value, "id = ?", arrayOf("1"))

调用方查询数据

    // 查询所有
    val uri = Uri.parse("content://com.example.myapplication.provider/project")
    contentResolver.query(uri, null, null, null, null)?.apply {
        while (moveToNext()) {
            val id = getString(getColumnIndex("id"))
            val technology = getString(getColumnIndex("technology"))
            val charge = getString(getColumnIndex("charge"))
            Log.i(tag, "id: $id, technology: $technology, charge: $charge")
        }
        close()
    }
}
//查询的列为 id 和 charge,查询条件为 id < 5,结果根据 id 排序
val uri = Uri.parse("content://com.example.myapplication.provider/project")
contentResolver.query(uri, arrayOf("id", "charge"), "id < ?", arrayOf("5"), "id")
    ?.apply {
        while (moveToNext()) {
            val id = getString(getColumnIndex("id"))
            val charge = getString(getColumnIndex("charge"))
            Log.i(tag, "id: $id, charge: $charge")
        }
        close()
    }