前言
- ContentProvider 是 Android 四大组件之一,属于内容共享型组件;
- 在这篇文章里,我将讨论 ContentProvider 的基本使用方法,在下篇文章里我会介绍 ContentProvider 的原理 & 源码分析。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
目录
前置知识
这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~
- Binder 机制: 【点赞催更】
1. 概述
1.1 作用
ContentProvider 是进程间内容共享的统一接口。注意:ContentProvider 的作用不是实现进程间通信,它只是为进程间通信提供了一套统一接口,真正实现进程间通信的是底层的 Binder 机制。
1.2 优点:透明地提供内容
使用 ContentProvider 允许应用透明地将数据开放给其它应用,无论底层数据采用何种实现方式(网络、内存、文件或数据库),外界对于数据的访问方式都是统一的 & 固定的。外界只关心采用 CURD 来访问 ContentProvider 的数据,至于其内部数据的实现是采用文件存储还是数据库存储,外界是不感知的。
1.3 ContentProvider 是单例吗?
通常来说,ContentProvider 是单例的,特殊情况可以设置android:multiprocess
属性来决定是不是单例:当属性值为 true 时,每个调用者进程都会存在一个 ContentProvider 实例,官方的解释是可以避免进程间通讯的开销,但是这种方式在实际开发中很少运用。因此我们说一般情况下 ContentProvider 是单例的,只在服务提供进程创建实例。
2. 相关概念
2.1 统一资源标识符(URI)
统一资源标识符(Uniform Resource Indentifier)的作用是 唯一标识 ContentProvider 的数据。在通过 ContentResolver 解析数据时,URI 是必要的参数,其遵循的格式体现在ContentUris.java
:
Content URIs have the syntax:content://authority/path/id 复制代码
可以看到,URI 遵循固定的格式,一共分为四个部分:schema://authority/path/idschema://authority/path/idschema://authority/path/id
例如:content://com.xurui/user/1content://com.xurui/user/1content://com.xurui/user/1
元素 | 描述 |
schema(方案) | 固定为 content:// |
authority(权威) | 标识 ContentProvider 的唯一字符串,对应于注册时指定的 android:authority 属性 |
path(路径) | 标识 authority 数据的某些子集 |
id(记录 id) | 标识 path 子集中的某个记录(不指定是标识全部记录) |
系统预置了一些 ContentProvider,例如通讯录、媒体资源等,这里举出一些常用的系统 ContentProvider 的 Authority,它们的接口约定定义在目录/android.provider
:
Authority | 描述 |
com.android.contacts | 通讯录 |
media | 媒体 |
com.android.calendar | 日历 |
user_dictionary | 用户词典 |
2.2 MIME 数据类型
MIME类型(Multipurpose Internal Mail Extensions,多用途互联网邮件扩展类型)是一种互联网标准,用于指定某种扩展名的文件与应用程序的对应关系。一个 MIME 类型分为「主类型」+「子类型」,例如 .html 文件对应的 MIME 类型为 text/html,其中 text 为主类型,html 为子类型。
在 ContentProvider 中,通过 getType(Uri) 方法来确定 URI 对应的 MIME 类型,返回值可以返回 标准 MIME 类型或者自定义 MIME 类型,这是一个抽象方法,需要由子类实现:
ContentProvider.java
public abstract String getType(Uri uri); 复制代码
2.2.1 标准 MIME 类型
标准 MIME 类型中常见的主类型有:
- 声音:audio
- 视频:video
- 图像:image
- 文本:text
对应的 MIMIE 类型举例:
扩展名 | MIME |
.html | text/html |
.txt | text/plain |
.png | image/png |
.jpeg | image/jpeg |
2.2.2 自定义 MIME 类型
在 Android 中,自定义 MIME 类型的主类型只有两种:
vnd.android.cursor.item
:单行记录vnd.android.cursor.dir
:多行记录(集合)
例如通讯录 ContentProvider 定义了两种 MIME 类型,分别表示多条记录和单条记录:
ContactsContract.java
/** * The MIME type of {@link #CONTENT_URI} providing a directory of contact directories. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_directories"; /** * The MIME type of a {@link #CONTENT_URI} item. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_directory"; 复制代码
3. 主要方法
ContentProvider 使用表格的形式管理数据,对外暴露四个操作方法,分别是:添加、删除、更新、查询(insert、delete、update、query):
添加数据(Binder 线程) public abstract Uri insert(Uri uri, ContentValues values); 删除数据(Binder 线程) public abstract int delete(Uri uri, String selection, String[] selectionArgs); 更新数据(Binder 线程) public abstract int update(Uri uri, ContentValues values, String selection, String[] selectionArgs); 查询数据(Binder 线程) public abstract Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder); 复制代码
除了 4 个核心方法外,ContentProvider 还有其他比较重要的方法,例如:
启动回调(主线程) public abstract boolean onCreate(); 返回 Uri 对应的 MIME 类型(调用线程) public abstract String getType(Uri uri); 复制代码
需要注意:四个核心方法执行在 ContentProvider 注册进程,并在 Binder 线程池中执行,而不是主线程。考虑到存在多线程并发访问,为了保证数据安全在实现 ContentProvider 是还需要保证线程同步。而 onCreate() 方法执行在 ContentProvider 注册进程的主线程,因此不能执行耗时操作。关于 onCreate() 方法的调用我在 第 4 节 ContentProvider 的启动过程 中会详细介绍。
主要方法 | 执行线程 |
insert() | Binder 线程 |
delete() | Binder 线程 |
update() | Binder 线程 |
query() | Binder 线程 |
onCreate() | 主线程 |
3.1 插入数据
要插入一行新数据,需要使用 ContentProvider#insert(...)。例如,下面程序将一条日程数据插入的系统日历中:
ContentValues eventValues = new ContentValues(); eventValues.put(CalendarContract.Events.CALENDAR_ID, catId); // 日历账号 ID eventValues.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().getID()); // 时区 eventValues.put(CalendarContract.Events.DTSTART, beginTimeMillis); // 开始时间 eventValues.put(CalendarContract.Events.DTEND, endTimeMillis); // 结束时间 eventValues.put(CalendarContract.Events.TITLE, title); // 标题 eventValues.put(CalendarContract.Events.DESCRIPTION, description); // 描述 eventValues.put(CalendarContract.Events.EVENT_LOCATION, location); // 地点 Uri resultUri = context.getContentResolver().insert(CalendarContract.Events.CONTENT_URI, eventValues); if(null == resultUri) { // 插入失败 return; } 复制代码
插入成功后会返回该行的 Uri,格式如下:
content://com.android.calendar/events<id_value> 复制代码
URI 中的 <id_value> 就是该行 _ID 列的值,而前缀 content://com.android.calendar/events 正好就是插入数据时使用的 URI。需要注意的是,你不需要指定数据的 _ID
列,该列是表的主键,ContentProvider 会自动维护该列并分配一个唯一值。而要从 Uri 中提取 _ID 列的值,可以调用 ContentUris.parseId(...):
ContentUris.java
public static long parseId(Uri contentUri) { String last = contentUri.getLastPathSegment(); return last == null ? -1 : Long.parseLong(last); } 复制代码
提示: 客户端程序并非直接调用 ContentProvider#insert(),而是通过 ContentResolver#insert() 间接调用,下文会提到。
3.2 查询数据
从 ContentProvider 中查询数据的流程主要分为三个步骤:
3.2.1 请求访问权限
ContentProvider 程序可以指定其他应用程序必须具备的权限,例如读取用户词典需要android.permission.READ_USER_DICTIONARY
,写入用户词典需要android.permission.WRITE_USER_DICTIONARY
。
为了获取 ContentProvider 程序所需的权限,你的应用需要在 Manifest 文件中使用 来请求它们。当 Android Package Manager 安装 APK 时,会提示用户应用所需要的权限,用户继续安装相当于隐式授予权限。当然了,在 Android 6.0 以后部分权限还需要动态申请。
<uses-permission android:name = “ android.permission.READ_USER_DICTIONARY” > 复制代码
3.2.2 构造查询条件
ContentProvider 查询和 SQL 查询是相似的,如下表对比:
ContentProvider 查询 | SQL 查询 | 作用 |
Uri | FROM table_name | 查询的数据集合 |
projection | col,col,col... | 查询结果所需的列 |
selectionClause | WHERE col = value | 选择条件 |
selectionArgs | (没有确切地等效项) | 选择条件参数(如果 selection )中使用了 ? 占位符 |
sortOrder | ORDER BY col,col,... | 结果集 Cursor 的排序规则 |
cursor = context .getContentResolver().query( UserDictionary.Words.CONTENT_URI, projection, selectionClause, selectionArgs, sortOrder); 复制代码
例如查询手机通讯录:
Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI; String[] projection = { ContactsContract.Contacts._ID, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER }; String selectionClause = ContactsContract.CommonDataKinds.Phone.NUMBER + " = ?"; String[] selectionArgs = {"123456"}; Cursor cursor = getContentResolver().query(uri, projection, selectionClause, selectionArgs, "sort_key COLLATE LOCALIZED asc"); 复制代码
此查询类似于 SQL 查询:
SELECT _ID, displayName, data1 FROM content://com.android.contacts/data/phones WHERE data1 = "123456" ORDER BY sort_key COLLATE LOCALIZED asc 复制代码
3.2.3 处理结果集
查询结果是一个 Cursor 对象,处理范例如下:
if (null == mCursor) { // 失败 } else if (mCursor.getCount() < 1) { // 查询结果为空 } else { // 查询结果非空 while (cursor.moveToNext()) { // 联系人名称 String contractName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); // 联系人电话 String phone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)); ... } cursor.close(); // 记得关闭结果集 } 复制代码
3.3 删除数据
删除数据与查询类似,需要构造查询条件,删除操作结束会返回成功删除的行数。
int rowsDeleted = context.getContentResolver().delete(...); 复制代码
3.4 更新数据
更新操作类似于查询操作和插入操作的结合体,既需要构造 ContentValues 对象,也需要构造查询条件,删除操作结束后返回成功修改的行数。
int rowsUpdated = context.getContentResolver().update( UserDictionary.Words.CONTENT_URI, updateValues, selectionClause, selectionArgs ); 复制代码
4. ContentProvider 核心类
4.1 ContentResolver
外界(包括当前进程的其他组件)无法直接访问 ContentProvider 的,而是需要通过 ContentResolver 来间接访问。这种设计的优点是 统一管理应用依赖的 ContentProvider,而不需要关心真正操作的 ContentProvider 实现类。
ContentResolver 是一个抽象类,我们熟悉的 Context#getContentResolver() 获得的其实是它的子类 ApplicationContentResolver。
Context.java
public abstract ContentResolver getContentResolver(); 复制代码
ContextImpl.java
class ContextImpl extends Context { private final ApplicationContentResolver mContentResolver; @Override public ContentResolver getContentResolver() { return mContentResolver; } private static final class ApplicationContentResolver extends ContentResolver { private final ActivityThread mMainThread; @Override protected IContentProvider acquireProvider(Context context, String auth) { ... } @Override protected IContentProvider acquireExistingProvider(Context context, String auth) { ... } @Override public boolean releaseProvider(IContentProvider provider) { ... } ... } } 复制代码
在文章《Android | ContentProvider 精通篇》中,我会详细介绍 ContentResolver#query(...) 方法的执行过程,在那里我们再讨论 ApplicationContentResolver 方法体中的具体行为。
4.2 ContentUris
ContentUris 是 Uri 的工具类,在 ContentUris 的文档注释中主要描述了 ContentProvider URI 所遵循的格式,此外 ContentUris 还提供了三个工具方法:
1、从 Uri 中解析主键 id public static long parseId(Uri contentUri) { String last = contentUri.getLastPathSegment(); return last == null ? -1 : Long.parseLong(last); } 2、向 Uri 追加一个 id public static Uri.Builder appendId(Uri.Builder builder, long id) { return builder.appendEncodedPath(String.valueOf(id)); } 3、向 Uri 追加一个 id public static Uri withAppendedId(Uri contentUri, long id) { return appendId(contentUri.buildUpon(), id).build(); } 复制代码
4.3 UriMatcher
UriMatcher 是用于自定义 ContentProvider 的工具类,主要作用是根据 Uri 匹配对应的数据表。
public class ExampleProvider extends ContentProvider { 1、初始化 UriMatcher 对象,NO_MATCH 表示不匹配任何 Uri private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 2、注册 Uri 已经对应的返回码 static { uriMatcher.addURI("com.example.app.provider", "table3", 1); uriMatcher.addURI("com.example.app.provider", "table3/#", 2); } ... 3、 查询 public Cursor query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { switch (uriMatcher.match(uri)) { case 1: 3.1 匹配 table3 if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC"; break; case 2: 3.2 匹配 table3/# selection = selection + "_ID = " + uri.getLastPathSegment(); break; default: 3.3 默认 ... } 3.4 真正执行查询 } } 复制代码
可以使用通配符:
*
:匹配任意长度字符串#
:匹配任意长度的数字字符串
4.4 ContentObserver
ContentObserver .java
子类重写实现监听逻辑 public void onChange(boolean selfChange) { // Do nothing. Subclass should override. } public void onChange(boolean selfChange, Uri uri) { onChange(selfChange); } 复制代码
ContentObserver 用于监听 ContentProvider 中指定 Uri 标识数据的变化(增 / 删 / 改),使用时需要用到 ContentResolver 的两个方法:
ContentResolver.java
注册监听 public final void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer) 注销监听 public final void unregisterContentObserver(ContentObserver observer) 复制代码
需要注意:ContentProvider 内部需要手动通知修改事件,才能有效回调给 ContentResolver,例如:
ContentProvider 实现类
public class UserContentProvider extends ContentProvider { public Uri insert(Uri uri, ContentValues values) { ... 通知 getContext().getContentResolver().notifyChange(uri, null); } } 复制代码
5. 总结
- ContentProvider 是进程间内容共享的统一接口,底层实现进程间通信的是 Binder 机制,使用 ContentProvider 的优点是透明地提供内容,外界不用关心内容的层的数据实现方式。
- Uri 的作用是唯一标识 ContentProvider 的数据,MIME 类型描述了扩展名与应用程度的对应关系,例如 .html 对应的 MIME 类型为 text/html;
- ContentProvider 提供了 CURD 四个核心方法类访问数据,执行在服务提供进程的 Binder 线程池,而 onCreate() 方法执行在服务提供进程主线程