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协程高级用法大全

《Android协程高级用法大全》这篇文章给大家介绍Android协程高级用法大全,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友跟随小编一起学习吧... 目录1️⃣ 协程作用域(CoroutineScope)与生命周期绑定Activity/Fragment 中手

从基础到进阶详解Python条件判断的实用指南

《从基础到进阶详解Python条件判断的实用指南》本文将通过15个实战案例,带你大家掌握条件判断的核心技巧,并从基础语法到高级应用一网打尽,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录​引言:条件判断为何如此重要一、基础语法:三行代码构建决策系统二、多条件分支:elif的魔法三、

Linux系统管理与进程任务管理方式

《Linux系统管理与进程任务管理方式》本文系统讲解Linux管理核心技能,涵盖引导流程、服务控制(Systemd与GRUB2)、进程管理(前台/后台运行、工具使用)、计划任务(at/cron)及常用... 目录引言一、linux系统引导过程与服务控制1.1 系统引导的五个关键阶段1.2 GRUB2的进化优

Java使用正则提取字符串中的内容的详细步骤

《Java使用正则提取字符串中的内容的详细步骤》:本文主要介绍Java中使用正则表达式提取字符串内容的方法,通过Pattern和Matcher类实现,涵盖编译正则、查找匹配、分组捕获、数字与邮箱提... 目录1. 基础流程2. 关键方法说明3. 常见场景示例场景1:提取所有数字场景2:提取邮箱地址4. 高级

Python进阶之列表推导式的10个核心技巧

《Python进阶之列表推导式的10个核心技巧》在Python编程中,列表推导式(ListComprehension)是提升代码效率的瑞士军刀,本文将通过真实场景案例,揭示列表推导式的进阶用法,希望对... 目录一、基础语法重构:理解推导式的底层逻辑二、嵌套循环:破解多维数据处理难题三、条件表达式:实现分支

C#高效实现Word文档内容查找与替换的6种方法

《C#高效实现Word文档内容查找与替换的6种方法》在日常文档处理工作中,尤其是面对大型Word文档时,手动查找、替换文本往往既耗时又容易出错,本文整理了C#查找与替换Word内容的6种方法,大家可以... 目录环境准备方法一:查找文本并替换为新文本方法二:使用正则表达式查找并替换文本方法三:将文本替换为图

基于Python编写自动化邮件发送程序(进阶版)

《基于Python编写自动化邮件发送程序(进阶版)》在数字化时代,自动化邮件发送功能已成为企业和个人提升工作效率的重要工具,本文将使用Python编写一个简单的自动化邮件发送程序,希望对大家有所帮助... 目录理解SMTP协议基础配置开发环境构建邮件发送函数核心逻辑实现完整发送流程添加附件支持功能实现htm

Android 缓存日志Logcat导出与分析最佳实践

《Android缓存日志Logcat导出与分析最佳实践》本文全面介绍AndroidLogcat缓存日志的导出与分析方法,涵盖按进程、缓冲区类型及日志级别过滤,自动化工具使用,常见问题解决方案和最佳实... 目录android 缓存日志(Logcat)导出与分析全攻略为什么要导出缓存日志?按需过滤导出1. 按

Linux从文件中提取特定内容的实用技巧分享

《Linux从文件中提取特定内容的实用技巧分享》在日常数据处理和配置文件管理中,我们经常需要从大型文件中提取特定内容,本文介绍的提取特定行技术正是这些高级操作的基础,以提取含有1的简单需求为例,我们可... 目录引言1、方法一:使用 grep 命令1.1 grep 命令基础1.2 命令详解1.3 高级用法2

基于Python实现进阶版PDF合并/拆分工具

《基于Python实现进阶版PDF合并/拆分工具》在数字化时代,PDF文件已成为日常工作和学习中不可或缺的一部分,本文将详细介绍一款简单易用的PDF工具,帮助用户轻松完成PDF文件的合并与拆分操作... 目录工具概述环境准备界面说明合并PDF文件拆分PDF文件高级技巧常见问题完整源代码总结在数字化时代,PD