📚
handbook
  • Introduction
  • 1.前言
    • 前言
    • 鸣谢
  • 2.环境篇
    • 工具部署和使用
      • 团队协作工具
        • Confluence
      • 开发工具
        • Docker
          • 镜像导入导出
          • 安装
        • Docker Compose
      • 持续集成工具
        • Gerrit
        • Sonarqube
          • 分析参数设定
          • Prerequisite
          • 服务端设置
        • Ubuntu Ci Deploy
          • ubuntu使用docker部署jenkins+sonarqube
        • 持续集成部署
      • 文本编辑工具
        • Gitbook相关注意事项
        • Markdown快速入门
      • 版本控制
        • Git
          • 1.基础
            • Git基础(一)
            • Git基础(二)
            • Git基础(三)
            • Git基础(四)
            • Git基础(五)
          • 2.命令详解
            • 命令速查
          • 3.进阶技巧
            • git技巧
      • 自动化测试工具
        • Appium
          • capability参数配置
          • 安装
          • 简介
      • 项目管理工具
        • Jira
    • 开发环境配置
      • 通用
        • Homebrew安装与使用
        • Git服务器添加SSH Key
        • koroFileHeader使用
        • nodejs与npm的安装
        • npm更换国内源
        • pip使用相关
        • PostgreSQL安装与使用
        • proxychain安装与使用
        • shell配置环境变量
        • snapd安装与使用
        • terminal走代理
    • 快捷键速查
      • shell常用快捷键
  • 3.语言篇
    • C
      • 代码规范
      • 语言技巧
    • Cpp
      • 代码规范
      • 基础知识
        • 理解C++中的左值和右值
      • 语言技巧
        • 并发编程
          • 简单的线程池实现
    • Golang
      • 代码规范
        • 避免使用转义字符串
        • 避免参数语义不明确
        • 嵌套式结构体
        • 函数的分组与顺序
        • 函数命名
        • 声明一致性
        • 导入别名
        • 使用字段名初始化结构体
        • 本地变量声明
        • map初始化
        • nil用法
        • 包命名
        • 命名Printf样式的函数
        • 减少嵌套
        • 缩小变量作用域
        • struct引用初始化
        • 测试表声明
        • 顶层变量声明
        • 不必要的else
      • 环境配置
        • 代码检查格式化工具
          • Go Fmt
          • Goimports
          • Golint
          • Go Vet
        • go mod详解
        • golang安装
        • Golang开发环境
        • Troubleshooting
      • 语言技巧
        • 如何分包
    • Java
      • 代码规范
      • 语言技巧
        • 注解编程
        • 动态代理
    • Js
      • 语言技巧
        • Rollup
    • Kotlin
      • 基础知识
        • 写给开发者Kotlin指引(一)
        • 写给开发者Kotlin指引(二)
    • Python
      • 语言技巧
        • Best Practice Of Python S Project Structure
  • 4.规范篇
    • Git message规范
  • 5.技术篇
    • Android技术
      • Hook
        • EdXposed例子
        • Android 10 上安装Magisk和EdXposed
      • Tinker
        • 1.Tinker及其使用
      • 准备
        • ADB连接设备步骤及注意事项
        • adb连接设备
        • aosp编译
      • 基础
        • Binder接口调用的鉴权方法
        • Make 及 Android 编译系统介绍
        • 使用Content Provider为其他应用提供数据
      • 源码阅读
        • Framework源码分析 Looper Handler
        • Framework源码分析 启动流程 ServiceManager的初始化
        • Framework源码分析 启动流程 Zygote启动SystemServer
    • JS Bridge
      • JSBridge初探
    • Kernel技术
      • kallsyms子系统
    • Test技术
      • 软件测试
        • jnekin+sonar 部署 问题总结
        • 性能测试基础
        • 软件测试的背景
        • 测试基础
        • 测试人员的核心竞争力
    • 操作系统原理
      • 处理器如何实现原子操作
Powered by GitBook
On this page
  • 一、定义
  • 二、为应用创建Content Provider
  • 2.1 创建Content Provider
  • 2.2 Content Provider接口解释
  • 2.3 实现数据持久化部分
  • 2.4 完成Content Provider
  • 三、总结

Was this helpful?

  1. 5.技术篇
  2. Android技术
  3. 基础

使用Content Provider为其他应用提供数据

PreviousMake 及 Android 编译系统介绍Next源码阅读

Last updated 4 years ago

Was this helpful?

Content providers can help an application manage access to data stored by itself, stored by other apps, and provide a way to share data with other apps.

一、定义

Google的文档定义非常清晰,Content Provider就是用来与其他进程共享数据的标准方式。例如,社交应用需要获取你的联系人数据,为你迅速找到朋友的账号,联系人数据就是从系统应用联系人提供的Content Provider中获取的。它的具体结构可以用下图来展示。

具体来说,数据存储在Sqlite或者其他持久化方案中,Content Provider对外暴露对这些持久化数据的CRUD接口,而其他应用则可以通过这些接口完成对数据的读写。

二、为应用创建Content Provider

下面就基于SQLite3数据库,创建一个可以为其他应用提供CRUD接口的简单应用,一步步地学习Content Provider。该应用将为外界提供一组NBA球员的数据,其他应用可以通过接口对这些数据进行读写。

2.1 创建Content Provider

使用Android Studio(我使用的是最新的4.1.2版本)可以轻易创建一个Content Provider,先创建一个应用,因为是单纯的数据提供应用,所以选择No Activity的就行。在包名目录下右键new->other->Content Provider,如下图所示。

在新的窗口中为Content Provider命名,这里我使用了NBAPlayersContentProvider,URI Authority这一栏用于填写对外暴露的URI接口,其他应用将使用这个值作为URI的host部分(暂时认为这是NBAPlayersContentProvider的ID吧),一般的惯用写法是:

<package_name>.<provider_name>

这里我们由于只有一个provider,所以命名为com.rodneycheung.contentprovidersample.provider。Exported和Enabled都勾上,不勾也没事,反正在AndroidManifest.xml中也可以加上,然后点击Finish即可。

创建完成后,Android Studio会为我们自动生成代码如下:

class NBAPlayersContentProvider : ContentProvider() {

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        TODO("Implement this to handle requests to delete one or more rows")
    }

    override fun getType(uri: Uri): String? {
        TODO(
            "Implement this to handle requests for the MIME type of the data" +
                    "at the given URI"
        )
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        TODO("Implement this to handle requests to insert a new row.")
    }

    override fun onCreate(): Boolean {
        TODO("Implement this to initialize your content provider on startup.")
    }

    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.")
    }

    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.")
    }
}

2.2 Content Provider接口解释

Android Studio为我们自动生成的NBAPlayersContentProvider继承于ContentProvider,对于每个重写的函数都标注了函数的作用。我们一共需要实现一下几个,首先是onCreate方法,这个用于Content Provider创建的时候做的一些初始化工作;然后是getType,这个用于返回每个CRUD接口的MIME类型;最后就是对外暴露的CRUD接口,跟数据库中的很像,连参数都类似。query用于从查询数据,insert用于插入数据,update用于更新数据,delete用于删除数据。其实这些对于有数据库开发基础的开发者都很好理解。

2.3 实现数据持久化部分

前面的图解中说了,Content Provider中的数据来源于持久化方案中,所以我们先基于SQLite3把数据部分实现了。我们只需要创建一张表Players,表结构如下:

字段名称

类型

含义

name

text

球员姓名

score

real

场均得分

rebound

real

场均篮板

assists

real

场均助攻

block

real

场均盖帽

steals

real

场均抢断

下面创建数据库类NbaDbHelper

class NbaDbHelper (private val context: Context, name: String, version: Int) :
    SQLiteOpenHelper(context, name, null, version){
    override fun onCreate(db: SQLiteDatabase?) {
        TODO("Not yet implemented")
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        TODO("Not yet implemented")
    }
}

首先在onCreate方法中实现数据库的创建和升级逻辑:

class NbaDbHelper(private val context: Context, name: String, version: Int) :
    SQLiteOpenHelper(context, name, null, version) {

    private val TABLE_PLAYER = "player"

    private val COLUMN_NAME = "name"
    private val COLUMN_SCORE = "score"
    private val COLUMN_REBOUND = "rebound"
    private val COLUMN_ASSISTS = "assists"
    private val COLUMN_BLOCK = "block"
    private val COLUMN_STEALS = "steals"

    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL("create table $TABLE_PLAYER ($COLUMN_NAME text primary key, $COLUMN_SCORE real,$COLUMN_REBOUND real,$COLUMN_ASSISTS real,$COLUMN_BLOCK real,$COLUMN_STEALS real)")
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        db?.execSQL("drop table if exists $TABLE_PLAYER")
        onCreate(db)
    }
}

然后我们添加一些供NBAPlayersContentProvider访问的数据读写接口。

class NbaDbHelper(private val context: Context, name: String, version: Int) :
    SQLiteOpenHelper(context, name, null, version) {

    private val TABLE_PLAYER = "player"

    private val COLUMN_NAME = "name"
    private val COLUMN_SCORE = "score"
    private val COLUMN_REBOUND = "rebound"
    private val COLUMN_ASSISTS = "assists"
    private val COLUMN_BLOCK = "block"
    private val COLUMN_STEALS = "steals"

    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL("create table $TABLE_PLAYER ($COLUMN_NAME text primary key, $COLUMN_SCORE real,$COLUMN_REBOUND real,$COLUMN_ASSISTS real,$COLUMN_BLOCK real,$COLUMN_STEALS real)")
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        db?.execSQL("drop table if exists $TABLE_PLAYER")
        onCreate(db)
    }

    fun insertPlayer(values: ContentValues?): Long {
        return writableDatabase.insert(TABLE_PLAYER, null, values)
    }

    fun updatePlayer(
        values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        return writableDatabase.update(TABLE_PLAYER, values, selection, selectionArgs)
    }

    fun deletePlayer(selection: String?, selectionArgs: Array<String>?): Int {
        return writableDatabase.delete(TABLE_PLAYER, selection, selectionArgs)
    }

    fun queryPlayer(
        projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor {
        return readableDatabase.query(
            TABLE_PLAYER,
            projection,
            selection,
            selectionArgs,
            null,
            null,
            sortOrder
        )
    }
}

至此,数据库部分就是先完成了。

2.4 完成Content Provider

2.4.1 onCreate

该函数在Content Provider创建时调用,一般我们会在这里初始化数据库。

class NBAPlayersContentProvider : ContentProvider() {
    private lateinit var dbHelper: NbaDbHelper

    override fun onCreate(): Boolean = context?.let {
        dbHelper = NbaDbHelper(it, "nba.db", 1)
        true
    } ?: false
      ...
}

2.4.2 getType

该函数用于返回内容Uri的MIME类型。先解释一下什么叫内容Uri,之前的NBAPlayersContentProvider方法中很多都带一个叫Uri的参数,这个就是所谓的内容Uri。一个标准的内容Uri的格式是:

content://<authority>/<path>

authority就是前面创建NBAPlayersContentProvider时的值,path很好理解,用于定位NBAPlayersContentProvider中的资源。一般path的格式为:

tableName             在tableName表中进行读写
tableName/*           在tableName表中进行被命名为*的操作
tableName/#           读取tableName表中ID为#的行

*表示任意字符串,#表示任意数字。在这里,我们稍微简单一点,只用第一种格式,即可以在整张表中通过SQL约束来进行增删改查。对于tableName/*,一般用于对外暴露一个封装好的接口,即用户不需要了解数据存储的表结构,也可以获取到相应的数据。例如player/getAllTripleDouble,就是获取本赛季所有数据达到场均三双的球员。用户只需要使用contentResolver.query("com.rodneycheung.contentprovidersample.provider/player/getAllTripleDouble",...)就可以获取到所有场均三双球员了。

在了解了内容Uri的定义后,再介绍一个在ContentProvider中常用的Uri匹配的类UriMatcher,它可以帮助我们匹配各个方法中的参数Uri的值。下面直接上做法,非常简单易懂,就不多说了。

class NBAPlayersContentProvider : ContentProvider() {
    private lateinit var dbHelper: NbaDbHelper

    private val playerTable = 0
    private val authority = "com.rodneycheung.contentprovidersample.provider"

    private val uriMatcher by lazy {
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(authority, "player", playerTable)
        matcher
    }

      override fun onCreate(): Boolean = context?.let {
        dbHelper = NbaDbHelper(it, "nba.db", 1)
        true
    } ?: false
}

在定义好UriMatcher后,我们就不需要手动去解析uri参数并和我们预设的路径进行匹配了。

Android对于这里的MIME类型作了三点格式的规定:

  1. 以vnd开头

  2. 如果内容Uri是以路径结尾,vnd后面接android.cursor.dir/,否则接android.cursor.item

  3. 最后接上vnd.<authority>.<path>

由于我们只定义了一个针对整个player表的路径,那么我们的MIME字符串就是vnd.android.cursor.dir/vnd.com.rodneycheung.contentprovidersample.provider.player,下面来实现getType方法。

class NBAPlayersContentProvider : ContentProvider() {
    private lateinit var dbHelper: NbaDbHelper

    private val playerTable = 0
    private val authority = "com.rodneycheung.contentprovidersample.provider"

    private val uriMatcher by lazy {
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(authority, "player", playerTable)
        matcher
    }

    override fun onCreate(): Boolean = context?.let {
        dbHelper = NbaDbHelper(it, "nba.db", 1)
        true
    } ?: false

    override fun getType(uri: Uri): String? {
        return when (uriMatcher.match(uri)) {
            playerTable -> "vnd.android.cursor.dir/vnd.com.rodneycheung.contentprovidersample.provider.player"
            else -> null
        }
    }
}

2.4.3 CRUD接口

接下来就是实现CRUD接口,由于我们直接对于player表进行操作,所以非常简单,直接把参数填到dbHelper对应的接口里就可以了。

class NBAPlayersContentProvider : ContentProvider() {
    private lateinit var dbHelper: NbaDbHelper

    private val playerTable = 0
    private val authority = "com.rodneycheung.contentprovidersample.provider"

    private val uriMatcher by lazy {
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(authority, "player", playerTable)
        matcher
    }

    override fun onCreate(): Boolean = context?.let {
        dbHelper = NbaDbHelper(it, "nba.db", 1)
        true
    } ?: false

    override fun getType(uri: Uri): String? {
        return when (uriMatcher.match(uri)) {
            playerTable -> "vnd.android.cursor.dir/vnd.com.rodneycheung.contentprovidersample.provider.player"
            else -> null
        }
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        return when (uriMatcher.match(uri)) {
            playerTable -> dbHelper.deletePlayer(selection, selectionArgs)
            else -> 0
        }
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        return when (uriMatcher.match(uri)) {
            playerTable -> {
                Uri.parse("content://$authority/player/${dbHelper.insertPlayer(values)}")
            }
            else -> null
        }
    }

    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? {
        return when (uriMatcher.match(uri)) {
            playerTable -> dbHelper.queryPlayer(projection, selection, selectionArgs, sortOrder)
            else -> null
        }
    }

    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        return when (uriMatcher.match(uri)) {
            playerTable -> dbHelper.updatePlayer(values, selection, selectionArgs)
            else -> 0
        }
    }
}

2.4.4 权限申请

在Android 11中,对于Content Provider的权限又收紧了,需要在AndroidManifest.xml中进行读写权限的申请,具体做法如下。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.rodneycheung.contentprovidersample">

    <!--    声明读写两种权限-->
    <permission
        android:name="com.rodneycheung.contentprovidersample.provider.READ_PERMISSION"
        android:label="contentprovidersample provider read pomission"
        android:protectionLevel="normal" />
    <permission
        android:name="com.rodneycheung.contentprovidersample.provider.WRITE_PERMISSION"
        android:label="contentprovidersample provider write pomission"
        android:protectionLevel="normal" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ContentProviderSample">
        <!--            引用读写权限-->
        <provider
            android:name=".NBAPlayersContentProvider"
            android:authorities="com.rodneycheung.contentprovidersample.provider"
            android:enabled="true"
            android:exported="true"
            android:readPermission="com.rodneycheung.contentprovidersample.provider.READ_PERMISSION"
            android:writePermission="com.rodneycheung.contentprovidersample.provider.WRITE_PERMISSION" />
    </application>

</manifest>

此外,用户为了能找到这个provider,还需要在自己的AndroidManifest.xml里面加上:

<queries>
  <package android:name="com.rodneycheung.contentprovidersample" />
</queries>

三、总结

结束了,非常的简单😄,完整的代码在。

这里
Content Provider Diagram
Content Provider Diagram
Content Provider Diagram