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

相关文章

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java进程异常故障定位及排查过程

《Java进程异常故障定位及排查过程》:本文主要介绍Java进程异常故障定位及排查过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、故障发现与初步判断1. 监控系统告警2. 日志初步分析二、核心排查工具与步骤1. 进程状态检查2. CPU 飙升问题3. 内存

深度解析Python装饰器常见用法与进阶技巧

《深度解析Python装饰器常见用法与进阶技巧》Python装饰器(Decorator)是提升代码可读性与复用性的强大工具,本文将深入解析Python装饰器的原理,常见用法,进阶技巧与最佳实践,希望可... 目录装饰器的基本原理函数装饰器的常见用法带参数的装饰器类装饰器与方法装饰器装饰器的嵌套与组合进阶技巧

Android DataBinding 与 MVVM使用详解

《AndroidDataBinding与MVVM使用详解》本文介绍AndroidDataBinding库,其通过绑定UI组件与数据源实现自动更新,支持双向绑定和逻辑运算,减少模板代码,结合MV... 目录一、DataBinding 核心概念二、配置与基础使用1. 启用 DataBinding 2. 基础布局

Android ViewBinding使用流程

《AndroidViewBinding使用流程》AndroidViewBinding是Jetpack组件,替代findViewById,提供类型安全、空安全和编译时检查,代码简洁且性能优化,相比Da... 目录一、核心概念二、ViewBinding优点三、使用流程1. 启用 ViewBinding (模块级

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja

从基础到进阶详解Pandas时间数据处理指南

《从基础到进阶详解Pandas时间数据处理指南》Pandas构建了完整的时间数据处理生态,核心由四个基础类构成,Timestamp,DatetimeIndex,Period和Timedelta,下面我... 目录1. 时间数据类型与基础操作1.1 核心时间对象体系1.2 时间数据生成技巧2. 时间索引与数据

Windows的CMD窗口如何查看并杀死nginx进程

《Windows的CMD窗口如何查看并杀死nginx进程》:本文主要介绍Windows的CMD窗口如何查看并杀死nginx进程问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录Windows的CMD窗口查看并杀死nginx进程开启nginx查看nginx进程停止nginx服务

Java进程CPU使用率过高排查步骤详细讲解

《Java进程CPU使用率过高排查步骤详细讲解》:本文主要介绍Java进程CPU使用率过高排查的相关资料,针对Java进程CPU使用率高的问题,我们可以遵循以下步骤进行排查和优化,文中通过代码介绍... 目录前言一、初步定位问题1.1 确认进程状态1.2 确定Java进程ID1.3 快速生成线程堆栈二、分析

Python实现自动化Word文档样式复制与内容生成

《Python实现自动化Word文档样式复制与内容生成》在办公自动化领域,高效处理Word文档的样式和内容复制是一个常见需求,本文将展示如何利用Python的python-docx库实现... 目录一、为什么需要自动化 Word 文档处理二、核心功能实现:样式与表格的深度复制1. 表格复制(含样式与内容)2