对于ContentProvider, 可以把它看做为一个数据库, 数据库中包含表, 而Provider中可以包含任意数量的path.
作用
主要目的是为了处理app数据, 包括
- 获取自身的数据, 一般是特殊用途, 例如为了配合搜索框架实现搜索推荐功能
- 获取其他app的数据
- 共享数据给其他app
注意: 不同app是运行在不同进程中的, 所以ContentProvider也是一种进程间传递数据的方式.
作用2, 3其实是一样的, 最主要的目的是为了实现不同app间共享数据, 当然也可以用来抽象本地数据获取的方式, 不过如果仅仅是想抽象本地获取数据的方式, 没有必要使用ContentProvider.
官方用途
- 使用search framework实现search suggestions
- 共享数据给widget(什么widget没有细说, 暂不探究)
- 共享数据给其他app
注: SDK自带的ContentProvider都在android.provider包中
优点
权限控制
共享数据给其他app时可以增加权限控制, 甚至分别控制读数据和写数据的权限, 增加安全性.
注意: 这里的权限读取数据的app需要在AndroidManifest.xml
中使用<uses-permission>
静态指定, 不能在运行时申请权限.
如果在安装时用户拒绝给权限, 在进行读写操作时应该会抛出异常导致app崩溃.
所以在通过Provider获取其他app的数据时应该进行权限检查避免app崩溃.
注意
- app自身内的组件能够任意读写数据
- 如果app不指定需要的权限, 表示不共享数据
- 对于本来就公开的数据, 例如公共储存区域(SD卡)的数据库或文件, Provider的权限控制不会起作用
Provider-level权限
指定整个Provider的访问权限, 类比成控制访问整个数据库的权限
读写权限
通过<provider android:permission>
属性来同时指定读操作和写操作需要的权限
读权限
通过<provider android:readPermission>
属性来指定读权限, 会覆盖读写权限
写权限
通过<provider android:writePermission>
属性来指定写权限, 会覆盖读写权限
Path-level权限
想指定具体某个Content URI的权限, 类比成单独控制数据库中某个表的权限, 可以通过<path-permisson>
来实现, 同时会覆盖Provider-level权限, 可以设定读写权限, 读权限和写权限, 跟Provider-level权限一样, 读权限和写权限会覆盖读写权限
临时权限
临时权限机制可以允许没有申请置顶上述置顶权限的其他app临时访问数据. 一般结合startActivityForResult
来获取包含临时权限的URI, 然后通过URI来访问数据.
官网的例子是:
你写了一个email客户端, 里面保存了一堆图片附件, 你希望共享这些图片文件给其他app.
假设现在有个图片浏览app, 它没有声明获取图片附件的权限, 所以不能直接通过ContentResolver来获取email客户端的图片文件, 如果想通过临时权限获取图片文件, 一般是,
图片浏览app先通过Intent隐式打开email客户端的一个页面, 例如选择图片的列表页, 然后email客户端返回一个包含Content URI的Intent给图片浏览app, 这个Content URI包含了临时访问权限的flag, 然后图片浏览app就可以通过这个URI访问email客户端中的图片文件了.
临时的意思是, 一旦离开了获取临时权限的Activity, 那这个访问权限就失效了, 需要重新获取.
注意: Intent中还有FLAG_GRANT_PERSISTABLE_URI_PERMISSION
可以让被授权的app保留这个临时权限, 直到主动放弃授权.
关键
-
android:grantUriPermissions
: 默认为false
, 当为false
时需要增加<grant-uri-permission>
来指定允许临时权限访问的具体Content URI, 设置为true
时表示整个Provider都接受临时权限访问数据. -
<grant-uri-permission>
: 用于指定具体的Content URI, 和上面的Path-level权限的<path-permisson>
类似 -
FLAG_GRANT_READ_URI_PERMISSION
和FLAG_GRANT_WRITE_URI_PERMISSION
: 配合Intent#setFlags
让Intent中的URI能够被无权限的app临时访问.
抽象数据获取方式
ContentProvider实际上是中间层, 所有人都通过它获取数据, 而不用理会具体数据储存的方式.
一般情况下app都会抽象数据获取方式, 当你需要共享数据给其他app的时候, 使用ContentProvider显得更加统一. 但是当你不需要ContentProvider提供的特性时, 自己抽象会更加方便.
声明ContentProvider
<provider>
给app增加ContentProvider需要在<application>
下添加<provider>
标签来声明Provider.
属性
-
android:authorities
: 指定authority, 可以指定多个, 至少指定一个. 必须在整个系统里唯一, 所以一般会带上包名, 这里指定的authority就是Content URI中的authority. -
android:enabled
: 是否启用这个Provider, 默认为true
,<application>
也有一个类似的属性, 也是默认为true
, 当两个都未true
时才会初始化Provider -
android:exported
: SDK17之前不能设置, 默认为true
, SDK17及之后默认为false
,true
表示其他app可以访问这个Provider, 反之则只有相同UID(user ID)的app可以访问, 可以理解为其他app不能访问. -
android:grantUriPermissions
: 是否授权临时权限访问这个Provider, 默认是false
, 当为false
时, 只有<grant-uri-permission>
指定的URI能够通过临时权限访问, 如果未true
则所有URI都可以. -
android:initOrder
: 指定同一进程中的Provider初始化顺序, 数字越大, 越早初始化, 一般用来解决Provider的依赖问题. -
android:multiprocess
: 当app包含多个进程的时候用来指定不同的进程是共用一个Provider还是各自持有自己的Provider.true
表示每个进程都有一个Provider实例, 可以减少进程间的通讯, 但增加内存消耗. 默认是false
. -
android:name
: 具体的Provider类, 必须指定, 如果是.
开头则会自动添加app的包名为前缀. -
android:process
: 指定Provider运行的进程名, 默认是app进程(名称是app的包名), 如果是:
开头则会是app的私有进程, 如果是小写字母开头则是全局进程. 会覆盖<application>
中的同名属性设置. -
android:permission
: 指定整个Provider的读权限和写权限, 会被readPermission
和writePermission
覆盖 -
android:readPermission
和android:writePermission
: 单独控制读权限和写权限, 会覆盖android:permission
-
android:syncable
: Provider中的数据是否会被同步的远程服务器 -
android:icon
和android:label
: 在Setting -> Apps -> All
里面显示
子标签
<path-permission>
用来指定该Provider下具体的Content URI需要的权限.
-
android:path
: 具体的path, 注意path需要带上/
前缀, 例如/path
-
android:pathPrefix
: 所有path带该前缀的URI -
android:pathPattern
: 匹配值,\\*
匹配任意数量的前一个字符,\\.\\*
匹配任何数量的任意字符. 以上3个属性只能指定其中一个 -
android:permission
: 指定该URI读和写的权限 -
android:readPermission
和android:writePermission
: 单独设置读写权限, 会覆盖android:permission
.
<grant-uri-permisstion>
用来指定可以通过临时权限访问的Content URI.
包含android:path
, android:pathPrefix
和android:pathPattern
, 限制和作用同上.
相关知识
Content URI
ContentResolver中大部分方法都需要指定一个Content URI.
Content URI是一个指定了ContentProvider中的数据的URI
形式为
content://providerName/tableName
// 指向某一行
content://providerName/tableName/id
URI的scheme固定为content://
, authority(host:port)为指向操作的ContentProvider, path为ContentProvider中的表名, 另外可以加id来指向具体的某一行, 可以使用ContentUris#withAppendedId
来组合URI.
可以把一个ContentProvider看作一个数据库, path是一个表, 数据库中可以存在多个表, id则是表中具体的一条数据.
MIME类型
- 标准的类型可以参考IANA MIME Media Types, 标准类型包含
Type/SubType
, 例如text/html
- 如果是返回指向具体行的Content URI则需要返回Android's vendor-specific格式的MIME
Android's vendor-specific MIME format分成3部分
- 类型: 固定为
vnd
- 子类型: 如果是单行, 为
android.cursor.item/
, 多行则是android.cursor.dir/
- Provider特有部分:
vnd.<name><type>
,<name>
要求全局唯一, 一般取app的包名,<type>
要求URI模式(URI pattern)中唯一, 一般取表名, 例如一个多个行的MIME类型会是vnd.android.cursor.dir/vnd.com.example.provider.table1
获取数据用法
ContentResolver
虽然ContentProvider是中间层, 不过还是可以把它看作一个数据容器, 通过ContentResolver从这个容器中提取数据.
跟数据库类似, 可以通过ContentResolver对ContentProvider中的数据进行CRUD(create, retrieve, update, and delete)操作.
通过Context#getContentResolver()
来获取一个ContentResolver实例.
然后通过ContentResolver#query()
来获取数据, 得到一个Cursor
实例
Cursor
-
Cursor
是一个接口, 表示行数据的集合. -
ListView
有一个SimpleCursorAdapter
可以方便显示Cursor
中的数据
ContentValues
通过ContentValues
赋值ContentProvider中字段, 然后通过ContentResolver#insert()
插入数据或者通过ContentResolver#update()
更新数据
ContentProviderOperation
通过ContentProviderOperation
和ContentResolver#applyBatch()
可以批量处理数据, 例如一次插入多条数据, 或者同时向一个ContentProvider中的不同表插入数据
这个组合的主要作用是保持一系列操作的原子性, 即要么所有操作都成功, 要么所有操作都不成功.
注意, 这里的批量操作都是对同一个ContentProvider操作的.
Loader
通过CursorLoader
异步处理数据
通过Intent间接处理数据
注意, 并不是直接将Intent
直接传递给ContentProvider
当你的app没有权限访问某个ContentProvider的数据时, 通过Intent
启动有权限的app, 该app获取到数据后返回一个特殊的URI给你, 然后你的app可以通过这个URI临时获得访问数据的权限.
大致流程:
- 使用
Context#startActivityForResult()
通过非指定的方式启动app - 根据
Intent
中的信息, 相应的app会被启动, 用户在该app操作结束后, 该app通过setResult()
返回一个Intent
到你的app - 返回的
Intent
中包含指定数据的content URI, 然后你的app可以通过这个URI从ContentProvider中获取该URI指定的数据
提供数据的方法
创建一个ContentProvider来提供数据.
除了上面提及的官方用途, 如果你想使用AbstractThreadedSyncAdapter
, CursorAdapter
, 或者CursorLoader
, 那么你也需要创建一个ContentProvider.
如果这些你都不需要, 那你很可能并不需要创建ContentProvider.
实现步骤
1. 创建具体的类继承ContentProvider
ContentProvider是一个抽象类, 需要实现对应的方法, ContentResolver是通过这些方法对数据进行操作.
-
onCreate
: 当ContentProvider被创建时会回调, 一般ContentProvider是在ContentResolver尝试操作provider的时候才会被创建, 必须返回true
Provider才会生效. -
getType()
: 返回MIME类型, 必须实现, 实际上就是对外声明Provider会返回的数据类型 -
getStreamTypes()
: 如果是提供File Data则期望实现, 返回一个指定文件类型的字符串数组, 数组中的值为MIME - 操作数据的方法, 操作数据的方法会被ContentResolver对应的方法调用, 在这里你可以实现任意逻辑, 并不一定要按方法名操作数据, 例如可以拒绝返回某列数据, 或者固定返回某条数据等.
注意:
- 操作数据的方法需要考虑线程安全, 能够被不同的线程同时调用
- 对于Content URI, 可以利用
UriMatcher
工具类来解析传进来的URI
2. 约定数据格式
通常还需要在ContentProvider类中增加Contract类来约定数据格式. 实际就是包含一系列常量的接口.
通常需要指定的内容包括:
- content URIs, 包括authority, path, 相当于指定数据库名字和包含的表
- 列名, 相当于声明表的结构
3. 声明Provider
在AndroidManifest.xml中声明Provider, 并进行相关设置, 例如是否启动, 权限控制等, 具体看上文<provider>
节
4. 允许临时权限
如果想授权临时权限, 除了在AndroidManifest.xml中声明外, 一般还需要添加一个Activity来处理其他app发起的隐式Intent, 然后通过Activity#setResult()
方法来把指定的URI返回给其他app.
特殊用法
参考How does Firebase initialize on Android?
因为Provider具有以下特性
- app进程启动时, Provider比Activity, Service和BroadcastReceiver都要早初始化, 而且启动时在
ContentProvider#onCreate()
回调中能够获取到Context
实例. - 可以通过文件合并在build的时候自动合并进AndroidManifest.xml, 所以不需要在主项目中声明.
所以可以用来在app启动时初始化第三方服务.
注意: 这种用法明显不符合ContentProvider的设计初衷, 而且引用的文章中还提到一些需要注意的点. 但是不得不说对于SDK的初始化来说, 这种方式非常优雅, 不需要添加任何初始化代码, 因此附加在这里.