Android 进阶11:进程通信之 ContentProvider 内容提供者

2024-06-02 07:18

本文主要是介绍Android 进阶11:进程通信之 ContentProvider 内容提供者,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

学习启舰大神,每篇文章写一句励志的话,与大家共勉。

  • When you are content to be simply yourself and don’t compare or compete, everyone will respect you.
  • 当你满足于做自己而不去比较或竞争时,每个人都会尊重你。

读完本文你将了解:

    • ContentProvider 简介
    • ContentProvider 与 URI
    • 权限
      • 先定义权限
      • 给 provider 中设置读权限
      • 在应用中注册这个权限
    • 支持的数据类型
    • ContentProvider 的使用
      • 设计数据存储
      • 创建 ContentProvider 子类
      • 定义 ContentProvider 的授权字符串authority内容 URI权限
      • 通过 ContentResolver 和 URI 进行增删改查
      • 运行结果
    • 源码浅析
    • 注意事项
      • 防止 SQL 注入
      • Cursor 搭配 ListView使用 SimpleCursorAdapter 更配
      • ContentProvider 的使用场景
    • 代码地址
    • Thanks

ContentProvider 简介

作为安卓 F4,ContentProvider 其实是比较低调的一个,日常开发中使用的频率也没那三位多。

它的诞生就是为了给不同应用提供内容访问,自然在我们研究的“多进程通信方式”之中。

ContentProvider 封装了数据的跨进程传输,我们可以直接使用 getContentResolver() 拿到 ContentResolver 进行增删改查即可。

ContentProvider 以一个或多个表(与在关系型数据库中的表类似)的形式将数据呈现给外部应用。 行表示提供程序收集的某种数据类型的实例,行中的每个列表示为实例收集的每条数据。

实现一个 ContentProvider 时需要实现以下几个方法:

  • onCreate():初始化 provider
  • query():查询数据
  • insert():插入数据到 provider
  • update():更新 provider 的数据
  • delete():删除 provider 中的数据
  • getType():返回 provider 中的数据的 MIME 类型

注意:
1. onCreate() 默认执行在主线程,别做耗时操作,query() 也最好异步执行
2. 上面的 4 个增删改查操作都可能会被多个线程并发访问,因此需要注意线程安全

ContentProvider 与 URI

ContentProvider 使用 URI 标识要操作的数据,这里的内容 URI 主要包括两部分:

  1. authority:整个提供程序的符号名称
  2. path:指向表的名称/路径

内容 URI 统一的形式就是:

content://authority/path

例如:

content://user_dictionary/words

当你调用 ContentResolver 方法来访问 ContentProvider 中的表时,需要传递要操作表的 URI。

在通过 ContentResolver 进行数据请求时(比如 contentResolver.insert(uri, contentValues);), 系统会检查指定 URI 的 authority 信息,然后将请求传递给注册监听这个 authority 的 ContentProvider 。这个 ContentProvider 可以监听 URI 想要操作的内容,Android 中为我们提供了 UriMatcher 来解析 URI。

权限

由于内容提供者要被不同应用访问,因此权限必不可少。我们可以给内容提供者设置 “读/写”权限。

设置自定义权限分三步:

  1. 向系统声明一个权限
  2. 给要设置权限的组件设置需要这个权限
  3. 在想要使用上述组件的应用中注册这个权限

先定义权限

<!--在系统中注册读内容提供者的权限-->
<permission
    android:name="top.shixinzhang.permission.READ_CONTENT"    //指定权限的名称android:label="Permission for read content provider"android:protectionLevel="normal"    />

其中 android:protectionLevel可选的值主要如下:

  • normal:低风险,任何应用都可以申请,在安装应用时,不会直接提示给用户
  • dangerous:高风险,系统可能要求用户输入相关信息才授予权限,任何应用都可以申请,在安装应用时,会直接提示给用户
  • signature:只有和定义了这个权限的 apk 用相同的私钥签名的应用才可以申请该权限
  • signatureOrSystem:有两种应用可以申请该权限
    • 和定义了这个权限的 apk 用相同的私钥签名的应用
    • 在 /system/app 目录下的应用

这里我们设置的值为 normal

给 provider 中设置读权限

这里设置的 readPermission 为上面声明的值:

<providerandroid:name=".provider.IPCPersonProvider"android:authorities="net.sxkeji.shixinandroiddemo2.provider.IPCPersonProvider"android:exported="true"    android:grantUriPermissions="true"android:process=":provider"android:readPermission="top.shixinzhang.permission.READ_CONTENT">

这个权限无法在运行时请求,必须在清单文件中使用 <uses-permission> 元素和内容提供者定义的准确权限名称指明你的权限。

在应用中注册这个权限

<uses-permission android:name="top.shixinzhang.permission.READ_CONTENT"/>

在您的清单文件中指定此元素后,您将有效地为应用“请求”此权限。 用户安装您的应用时,会隐式授予允许此请求。

官方建议:
对于同一开发者提供的不同应用之间的 IPC 通信,最好将 android:protectionLevel 属性设置为 “signature” 保护级别。签名权限不需要用户确认,因此,这种方式不仅能提升用户体验,而且在相关应用使用相同的密钥进行签名来访问数据时,还能更好地控制对内容提供程序数据的访问。

支持的数据类型

Android 本身包括的内容提供程序可管理音频、视频、图像和个人联系信息等数据。

内容提供者可以提供多种不同的数据类型:

  • int
  • long
  • double
  • float
  • BLOB:作为 64KB 字节的数组的二进制大型对象

使用二进制大型对象 (BLOB) 数据类型存储大小或结构会发生变化的数据。
例如,您可以使用 BLOB 列来存储协议缓冲区或 JSON 结构。
之前反编译微信时,保存朋友圈的数据就是 BLOB 类型。

ContentProvider 还会维护其定义的每个内容 URI 的 MIME 数据类型信息。

你可以使用 MIME 类型信息确定应用是否可以处理 ContentProvider 提供的数据,或根据 MIME 类型选择处理类型。

在使用包含复杂数据结构或文件的提供程序时,通常需要 MIME 类型。

ContentProvider 的使用

ContentProvider 的使用分为以下 4 步:

  1. 设计数据存储
    • 选择文件还是数据库
    • 如果您想提供 Bitmap 或其他庞大的文件导向型数据,请将数据存储在一个文件中,然后间接提供这些数据,而不是直接将其存储在表中
    • 使用二进制大型对象 (BLOB) 数据类型存储大小或结构会发生变化的数据。 例如使用 BLOB 列来存储 JSON
  2. 创建 ContentProvider 子类,实现关键方法
    • ContentProvider 实例通过处理来自其他应用的请求来管理对结构化数据集的访问
    • 所有形式的访问最终都会调用 ContentResolver,后者接着调用 ContentProvider 的具体方法来获取访问权限
    • 注意文章开头提到的避免耗时操作和线程安全
    • 尽管必须实现这些方法,它们的返回值并不重要,只要返回符合要求的数据类型即可,即使不执行任何其他操作
  3. 定义提供程序的授权字符串(authority)、内容 URI 以及列名称
    • 对应前面设计的数据库表名和字段名
    • 如果想让内容提供者应用处理 Intent,则还要定义 Intent 操作、Extra 数据以及标志
    • 还要定义想要访问该数据的应用必须具备的权限
  4. 通过 ContentResolver 和 URI 进行增删改查

下面以一个例子实验一下。

设计数据存储

这里我们使用 SQLite 存储数据,创建一个数据库帮助类:

public class DbOpenHelper extends SQLiteOpenHelper {private final static String DB_NAME = "person_list.db";public final static String TABLE_NAME = "person";private final static int DB_VERSION = 1;private final String SQL_CREATE_TABLE = "create table if not exists " + TABLE_NAME + "(_id integer primary key, name TEXT, description TEXT)";public DbOpenHelper(final Context context) {super(context, DB_NAME, null, DB_VERSION);}@Overridepublic void onCreate(final SQLiteDatabase db) {db.execSQL(SQL_CREATE_TABLE);}@Overridepublic void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {}
}

上面的代码创建了数据库 person_listperson 表。

创建 ContentProvider 子类

public class IPCPersonProvider extends ContentProvider {private final String TAG = this.getClass().getSimpleName();private static final UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);public static final String AUTHORITY = "net.sxkeji.shixinandroiddemo2.provider.IPCPersonProvider";  //授权public static final Uri PERSON_CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/person");private SQLiteDatabase mDatabase;private Context mContext;private String mTable;private static final int TABLE_CODE_PERSON = 2;static {//关联不同的 URI 和 code,便于后续 getTypemUriMatcher.addURI(AUTHORITY, "person", TABLE_CODE_PERSON);}@Overridepublic boolean onCreate() {initProvider();return false;}/*** 初始化时清楚旧数据,插入一条数据*/private void initProvider() {mTable = DbOpenHelper.TABLE_NAME;mContext = getContext();mDatabase = new DbOpenHelper(mContext).getWritableDatabase();new Thread(new Runnable() {@Overridepublic void run() {mDatabase.execSQL("delete from " + mTable);mDatabase.execSQL("insert into " + mTable + " values(1,'shixinzhang','handsome boy')");}}).start();}@Nullable@Overridepublic Cursor query(final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder) {String tableName = getTableName(uri);showLog(tableName + " 查询数据" );return mDatabase.query(tableName, projection, selection, selectionArgs, null, sortOrder, null);}@Nullable@Overridepublic Uri insert(final Uri uri, final ContentValues values) {String tableName = getTableName(uri);showLog(tableName + " 插入数据");mDatabase.insert(tableName, null, values);mContext.getContentResolver().notifyChange(uri, null);return null;}@Overridepublic int delete(final Uri uri, final String selection, final String[] selectionArgs) {String tableName = getTableName(uri);showLog(tableName + " 删除数据");int deleteCount = mDatabase.delete(tableName, selection, selectionArgs);if (deleteCount > 0) {mContext.getContentResolver().notifyChange(uri, null);}return deleteCount;}@Overridepublic int update(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) {String tableName = getTableName(uri);showLog(tableName + " 更新数据");int updateCount = mDatabase.update(tableName, values, selection, selectionArgs);if (updateCount > 0) {mContext.getContentResolver().notifyChange(uri, null);}return updateCount;}/*** CRUD 的参数是 Uri,根据 Uri 获取对应的表名** @param uri* @return*/private String getTableName(final Uri uri) {String tableName = "";int match = mUriMatcher.match(uri);switch (match){case TABLE_CODE_PERSON:tableName = DbOpenHelper.TABLE_NAME;}showLog("UriMatcher " + uri.toString() + ", result: " + match);return tableName;}@Nullable@Overridepublic String getType(final Uri uri) {return null;}private void showLog(final String s) {LogUtils.d(TAG, s + "***** @ " + Thread.currentThread().getName());}
}

定义 ContentProvider 的授权字符串(authority)、内容 URI、权限

①ContentProvider 可以关联多个授权字符串(authority),如上述代码所示,我们使用这个类的完整路径名为一个authority:


public static final String AUTHORITY = "net.sxkeji.shixinandroiddemo2.provider.IPCPersonProvider";  //授权

②内容 URI 用于在 ContentProvider 中标识数据的 URI,可以使用 content:// + authority 作为 ContentProvider 的 URI,这里就是:

content://net.sxkeji.shixinandroiddemo2.provider.IPCPersonProvider

如果该数据库中有多个表,可以继续增加 path:

content://net.sxkeji.shixinandroiddemo2.provider.IPCPersonProvider/table1
content://net.sxkeji.shixinandroiddemo2.provider.IPCPersonProvider/table2

这里我们的 URI 为:

public static final Uri PERSON_CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/person");

在 ContentProvider 中可以通过 UriMatcher 来为不同的 URI 关联不同的 code,便于后续根据 URI 找到对应的表。

③AndroidManifest 中声明权限

<uses-permission android:name="top.shixinzhang.permission.READ_CONTENT"/><!--读内容提供者的权限-->
<permission
    android:name="top.shixinzhang.permission.READ_CONTENT"android:label="Permission for read content provider"android:protectionLevel="normal"/>
<provider
    android:name=".provider.IPCPersonProvider"android:authorities="net.sxkeji.shixinandroiddemo2.provider.IPCPersonProvider"android:exported="true"android:grantUriPermissions="true"android:process=":provider"android:readPermission="top.shixinzhang.permission.READ_CONTENT">

因为我们要测试跨进程通信,因此这里将 provider 声明为另外一个进程 android:process=":provider"

通过 ContentResolver 和 URI 进行增删改查

在 Activity 中调用 ContentResolver 进行增加和查询操作:


private void getContentFromContentProvider() {Uri uri = IPCPersonProvider.PERSON_CONTENT_URI;    //ContentProvider 中注册的 URIContentValues contentValues = new ContentValues();contentValues.put("_id", id++);contentValues.put("name", "rourou" + DateUtils.getCurrentTime());contentValues.put("description", "beautiful girl");ContentResolver contentResolver = getContentResolver();    //获取内容处理器contentResolver.insert(uri, contentValues);    //插入一条数据//再查询一次Cursor cursor = contentResolver.query(uri, new String[]{"name", "description"}, null, null, null, null);if (cursor == null) {return;}StringBuilder cursorResult = new StringBuilder("DB 查询结果:");while (cursor.moveToNext()) {String result = cursor.getString(0) + ", " + cursor.getString(1);LogUtils.d(TAG, "DB 查询结果:" + result);cursorResult.append("\n").append(result);}mTvCpResult.setText(cursorResult.toString());cursor.close();
}@OnClick(R.id.btn_add_person_to_db)
public void addPersonToDB() {getContentFromContentProvider();
}

运行结果

调用 ContentProvider 的 Activity:

这里写图片描述

我们在另外一个进程的 provider 中打了些 Log,可以看到被调用了:

这里写图片描述

源码浅析

在上面打印 ContentProvider 增删改查所在线程时,看到显示的是 “Binder”,难不成也是使用 Binder 实现的么,我们去看看源码。

先看 Activity 直接调用的 ContentResolver.insert() 方法:

public final @Nullable Uri insert(@RequiresPermission.Write @NonNull Uri url,@Nullable ContentValues values) {Preconditions.checkNotNull(url, "url");IContentProvider provider = acquireProvider(url);if (provider == null) {throw new IllegalArgumentException("Unknown URL " + url);}try {long startTime = SystemClock.uptimeMillis();Uri createdRow = provider.insert(mPackageName, url, values);long durationMillis = SystemClock.uptimeMillis() - startTime;maybeLogUpdateToEventLog(durationMillis, url, "insert", null /* where */);return createdRow;} catch (RemoteException e) {...}
}

可以看到它调用了 IContentProvider.insert() 方法,直觉告诉我,这个类应该不简单!

点开源码一看,果然!


/*** The ipc interface to talk to a content provider.* @hide*/
public interface IContentProvider extends IInterface {...}

IContentProvider 也是个 IInterface,跟我们前面看的 AIDL、Binder 一模一样嘛!

在下水平时间有限,就不深入研究了,这里借用 gityuan 的 理解ContentProvider原理 的一张图大概了解一下:

这里写图片描述

注意事项

防止 SQL 注入

如果 ContentProvider 管理的数据位于 SQL 数据库中,在保存数据时,有可能会遇到恶意语句导致 SQL 注入。

这部分翻译理解自官方文档,有不合适的地方求指出 0.0

比如 ContentProvider.query():

public Cursor query(final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder) {String tableName = getTableName(uri);return mDatabase.query(tableName, projection, selection, selectionArgs, null, sortOrder, null);
}

这时如果输入的 selection 为恶意 SQL,就可能被执行,造成意外的损失。

例如,传入的 selectionname = nothing; DROP TABLE *;,这会生成查询子句 name = nothing; DROP TABLE *;

由于这个查询子句被作为 SQL 语句处理,因此这可能会导致 ContentProvider 擦除数据库中的所有表。

要避免此问题,可使用一个用于将 ? 作为可替换参数的查询子句以及一个单独的选择参数数组。

也就是将查询的 “字段名 = ?” 和具体值分别传入到在上述代码的 selectionselectionArgs

这样执行查询操作时,用户的输入直接受查询约束,而不会被作为 SQL 语句的一部分,因此无法注入恶意 SQL。

将 ? 用作可替换参数的条件语句和一个选择参数数组是指定查询语句的首选方式,即使 ContentProvider 管理的数据类型不是 SQL 数据库。

Cursor 搭配 ListView,使用 SimpleCursorAdapter 更配

ContentProvider.query() 会返回 Cursor,如果要结合 ListView 展示,可以使用 SimpleCursorAdapter

// Cursor 中要获取的数据列名称
String[] mWordListColumns = {UserDictionary.Words.WORD,   UserDictionary.Words.LOCALE  
};// ListView 的 item 布局中要展示上面两个数据对于的 id
int[] mWordListItems = { R.id.dictWord, R.id.locale};mCursorAdapter = new SimpleCursorAdapter(getApplicationContext(),               // The application's Context objectR.layout.wordlistrow,                  // A layout in XML for one row in the ListViewmCursor,                               // The result from the querymWordListColumns,                      // A string array of column names in the cursormWordListItems,                        // An integer array of view IDs in the row layout0);                                    // Flags (usually none are needed)mWordList.setAdapter(mCursorAdapter);

注意:要通过 Cursor 显示 ListView,游标必需包含名为 _ID 的列。

ContentProvider 的使用场景

只有在多个应用间分享数据时才需要使用 ContentProvider ,比如:

  • 您想为其他应用提供复杂的数据或文件
  • 您想允许用户将复杂的数据从您的应用复制到其他应用中
  • 您想使用搜索框架提供自定义搜索建议

否则直接使用应用内常用的数据存储方式(sp, db, file)即可。

代码地址

Thanks

《Android 开发艺术探索》
https://developer.android.com/guide/topics/providers/content-providers.html
https://developer.android.com/guide/topics/providers/content-provider-basics.html
https://developer.android.com/guide/topics/providers/content-provider-creating.html
http://blog.csdn.net/harvic880925/article/details/44651967
http://blog.csdn.net/harvic880925/article/details/38683625

这篇关于Android 进阶11:进程通信之 ContentProvider 内容提供者的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1023305

相关文章

Android Paging 分页加载库使用实践

《AndroidPaging分页加载库使用实践》AndroidPaging库是Jetpack组件的一部分,它提供了一套完整的解决方案来处理大型数据集的分页加载,本文将深入探讨Paging库... 目录前言一、Paging 库概述二、Paging 3 核心组件1. PagingSource2. Pager3.

C语言进阶(预处理命令详解)

《C语言进阶(预处理命令详解)》文章讲解了宏定义规范、头文件包含方式及条件编译应用,强调带参宏需加括号避免计算错误,头文件应声明函数原型以便主函数调用,条件编译通过宏定义控制代码编译,适用于测试与模块... 目录1.宏定义1.1不带参宏1.2带参宏2.头文件的包含2.1头文件中的内容2.2工程结构3.条件编

linux批量替换文件内容的实现方式

《linux批量替换文件内容的实现方式》本文总结了Linux中批量替换文件内容的几种方法,包括使用sed替换文件夹内所有文件、单个文件内容及逐行字符串,强调使用反引号和绝对路径,并分享个人经验供参考... 目录一、linux批量替换文件内容 二、替换文件内所有匹配的字符串 三、替换每一行中全部str1为st

从入门到精通详解LangChain加载HTML内容的全攻略

《从入门到精通详解LangChain加载HTML内容的全攻略》这篇文章主要为大家详细介绍了如何用LangChain优雅地处理HTML内容,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录引言:当大语言模型遇见html一、HTML加载器为什么需要专门的HTML加载器核心加载器对比表二

一文解密Python进行监控进程的黑科技

《一文解密Python进行监控进程的黑科技》在计算机系统管理和应用性能优化中,监控进程的CPU、内存和IO使用率是非常重要的任务,下面我们就来讲讲如何Python写一个简单使用的监控进程的工具吧... 目录准备工作监控CPU使用率监控内存使用率监控IO使用率小工具代码整合在计算机系统管理和应用性能优化中,监

Python实现MQTT通信的示例代码

《Python实现MQTT通信的示例代码》本文主要介绍了Python实现MQTT通信的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 安装paho-mqtt库‌2. 搭建MQTT代理服务器(Broker)‌‌3. pytho

从入门到进阶讲解Python自动化Playwright实战指南

《从入门到进阶讲解Python自动化Playwright实战指南》Playwright是针对Python语言的纯自动化工具,它可以通过单个API自动执行Chromium,Firefox和WebKit... 目录Playwright 简介核心优势安装步骤观点与案例结合Playwright 核心功能从零开始学习

Linux进程CPU绑定优化与实践过程

《Linux进程CPU绑定优化与实践过程》Linux支持进程绑定至特定CPU核心,通过sched_setaffinity系统调用和taskset工具实现,优化缓存效率与上下文切换,提升多核计算性能,适... 目录1. 多核处理器及并行计算概念1.1 多核处理器架构概述1.2 并行计算的含义及重要性1.3 并

Linux下进程的CPU配置与线程绑定过程

《Linux下进程的CPU配置与线程绑定过程》本文介绍Linux系统中基于进程和线程的CPU配置方法,通过taskset命令和pthread库调整亲和力,将进程/线程绑定到特定CPU核心以优化资源分配... 目录1 基于进程的CPU配置1.1 对CPU亲和力的配置1.2 绑定进程到指定CPU核上运行2 基于

Android kotlin中 Channel 和 Flow 的区别和选择使用场景分析

《Androidkotlin中Channel和Flow的区别和选择使用场景分析》Kotlin协程中,Flow是冷数据流,按需触发,适合响应式数据处理;Channel是热数据流,持续发送,支持... 目录一、基本概念界定FlowChannel二、核心特性对比数据生产触发条件生产与消费的关系背压处理机制生命周期