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

相关文章

使用Python实现获取网页指定内容

《使用Python实现获取网页指定内容》在当今互联网时代,网页数据抓取是一项非常重要的技能,本文将带你从零开始学习如何使用Python获取网页中的指定内容,希望对大家有所帮助... 目录引言1. 网页抓取的基本概念2. python中的网页抓取库3. 安装必要的库4. 发送HTTP请求并获取网页内容5. 解

Android Kotlin 高阶函数详解及其在协程中的应用小结

《AndroidKotlin高阶函数详解及其在协程中的应用小结》高阶函数是Kotlin中的一个重要特性,它能够将函数作为一等公民(First-ClassCitizen),使得代码更加简洁、灵活和可... 目录1. 引言2. 什么是高阶函数?3. 高阶函数的基础用法3.1 传递函数作为参数3.2 Lambda

Android自定义Scrollbar的两种实现方式

《Android自定义Scrollbar的两种实现方式》本文介绍两种实现自定义滚动条的方法,分别通过ItemDecoration方案和独立View方案实现滚动条定制化,文章通过代码示例讲解的非常详细,... 目录方案一:ItemDecoration实现(推荐用于RecyclerView)实现原理完整代码实现

Python实现常用文本内容提取

《Python实现常用文本内容提取》在日常工作和学习中,我们经常需要从PDF、Word文档中提取文本,本文将介绍如何使用Python编写一个文本内容提取工具,有需要的小伙伴可以参考下... 目录一、引言二、文本内容提取的原理三、文本内容提取的设计四、文本内容提取的实现五、完整代码示例一、引言在日常工作和学

Linux中的进程间通信之匿名管道解读

《Linux中的进程间通信之匿名管道解读》:本文主要介绍Linux中的进程间通信之匿名管道解读,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、基本概念二、管道1、温故知新2、实现方式3、匿名管道(一)管道中的四种情况(二)管道的特性总结一、基本概念我们知道多

Android App安装列表获取方法(实践方案)

《AndroidApp安装列表获取方法(实践方案)》文章介绍了Android11及以上版本获取应用列表的方案调整,包括权限配置、白名单配置和action配置三种方式,并提供了相应的Java和Kotl... 目录前言实现方案         方案概述一、 androidManifest 三种配置方式

Linux进程终止的N种方式详解

《Linux进程终止的N种方式详解》进程终止是操作系统中,进程的一个重要阶段,他标志着进程生命周期的结束,下面小编为大家整理了一些常见的Linux进程终止方式,大家可以根据需求选择... 目录前言一、进程终止的概念二、进程终止的场景三、进程终止的实现3.1 程序退出码3.2 运行完毕结果正常3.3 运行完毕

Java进阶学习之如何开启远程调式

《Java进阶学习之如何开启远程调式》Java开发中的远程调试是一项至关重要的技能,特别是在处理生产环境的问题或者协作开发时,:本文主要介绍Java进阶学习之如何开启远程调式的相关资料,需要的朋友... 目录概述Java远程调试的开启与底层原理开启Java远程调试底层原理JVM参数总结&nbsMbKKXJx

Android WebView无法加载H5页面的常见问题和解决方法

《AndroidWebView无法加载H5页面的常见问题和解决方法》AndroidWebView是一种视图组件,使得Android应用能够显示网页内容,它基于Chromium,具备现代浏览器的许多功... 目录1. WebView 简介2. 常见问题3. 网络权限设置4. 启用 JavaScript5. D

Windows命令之tasklist命令用法详解(Windows查看进程)

《Windows命令之tasklist命令用法详解(Windows查看进程)》tasklist命令显示本地计算机或远程计算机上当前正在运行的进程列表,命令结合筛选器一起使用,可以按照我们的需求进行过滤... 目录命令帮助1、基本使用2、执行原理2.1、tasklist命令无法使用3、筛选器3.1、根据PID