笔记: ContentProvider

参考Content Providers

对于ContentProvider, 可以把它看做为一个数据库, 数据库中包含表, 而Provider中可以包含任意数量的path.

作用

主要目的是为了处理app数据, 包括

  1. 获取自身的数据, 一般是特殊用途, 例如为了配合搜索框架实现搜索推荐功能
  2. 获取其他app的数据
  3. 共享数据给其他app

注意: 不同app是运行在不同进程中的, 所以ContentProvider也是一种进程间传递数据的方式.

作用2, 3其实是一样的, 最主要的目的是为了实现不同app间共享数据, 当然也可以用来抽象本地数据获取的方式, 不过如果仅仅是想抽象本地获取数据的方式, 没有必要使用ContentProvider.

官方用途

  1. 使用search framework实现search suggestions
  2. 共享数据给widget(什么widget没有细说, 暂不探究)
  3. 共享数据给其他app

注: SDK自带的ContentProvider都在android.provider包中

优点

权限控制

共享数据给其他app时可以增加权限控制, 甚至分别控制读数据和写数据的权限, 增加安全性.

注意: 这里的权限读取数据的app需要在AndroidManifest.xml中使用<uses-permission>静态指定, 不能在运行时申请权限.
如果在安装时用户拒绝给权限, 在进行读写操作时应该会抛出异常导致app崩溃.
所以在通过Provider获取其他app的数据时应该进行权限检查避免app崩溃.

注意

  1. app自身内的组件能够任意读写数据
  2. 如果app不指定需要的权限, 表示不共享数据
  3. 对于本来就公开的数据, 例如公共储存区域(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保留这个临时权限, 直到主动放弃授权.

关键

  1. android:grantUriPermissions : 默认为false, 当为false时需要增加<grant-uri-permission>来指定允许临时权限访问的具体Content URI, 设置为true时表示整个Provider都接受临时权限访问数据.
  2. <grant-uri-permission> : 用于指定具体的Content URI, 和上面的Path-level权限的<path-permisson>类似
  3. FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION : 配合Intent#setFlags让Intent中的URI能够被无权限的app临时访问.

抽象数据获取方式

ContentProvider实际上是中间层, 所有人都通过它获取数据, 而不用理会具体数据储存的方式.
一般情况下app都会抽象数据获取方式, 当你需要共享数据给其他app的时候, 使用ContentProvider显得更加统一. 但是当你不需要ContentProvider提供的特性时, 自己抽象会更加方便.

声明ContentProvider

<provider>

给app增加ContentProvider需要在<application>下添加<provider>标签来声明Provider.

属性

  1. android:authorities : 指定authority, 可以指定多个, 至少指定一个. 必须在整个系统里唯一, 所以一般会带上包名, 这里指定的authority就是Content URI中的authority.
  2. android:enabled : 是否启用这个Provider, 默认为true, <application>也有一个类似的属性, 也是默认为true, 当两个都未true时才会初始化Provider
  3. android:exported : SDK17之前不能设置, 默认为true, SDK17及之后默认为false, true表示其他app可以访问这个Provider, 反之则只有相同UID(user ID)的app可以访问, 可以理解为其他app不能访问.
  4. android:grantUriPermissions : 是否授权临时权限访问这个Provider, 默认是false, 当为false时, 只有<grant-uri-permission>指定的URI能够通过临时权限访问, 如果未true则所有URI都可以.
  5. android:initOrder : 指定同一进程中的Provider初始化顺序, 数字越大, 越早初始化, 一般用来解决Provider的依赖问题.
  6. android:multiprocess : 当app包含多个进程的时候用来指定不同的进程是共用一个Provider还是各自持有自己的Provider. true表示每个进程都有一个Provider实例, 可以减少进程间的通讯, 但增加内存消耗. 默认是false.
  7. android:name : 具体的Provider类, 必须指定, 如果是.开头则会自动添加app的包名为前缀.
  8. android:process : 指定Provider运行的进程名, 默认是app进程(名称是app的包名), 如果是:开头则会是app的私有进程, 如果是小写字母开头则是全局进程. 会覆盖<application>中的同名属性设置.
  9. android:permission : 指定整个Provider的读权限和写权限, 会被readPermissionwritePermission覆盖
  10. android:readPermissionandroid:writePermission : 单独控制读权限和写权限, 会覆盖android:permission
  11. android:syncable : Provider中的数据是否会被同步的远程服务器
  12. android:iconandroid:label : 在Setting -> Apps -> All里面显示

子标签

<path-permission>

用来指定该Provider下具体的Content URI需要的权限.

  1. android:path : 具体的path, 注意path需要带上/前缀, 例如/path
  2. android:pathPrefix : 所有path带该前缀的URI
  3. android:pathPattern : 匹配值, \\*匹配任意数量的前一个字符, \\.\\*匹配任何数量的任意字符. 以上3个属性只能指定其中一个
  4. android:permission : 指定该URI读和写的权限
  5. android:readPermissionandroid:writePermission: 单独设置读写权限, 会覆盖android:permission.
<grant-uri-permisstion>

用来指定可以通过临时权限访问的Content URI.
包含android:path, android:pathPrefixandroid: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类型

  1. 标准的类型可以参考IANA MIME Media Types, 标准类型包含Type/SubType, 例如text/html
  2. 如果是返回指向具体行的Content URI则需要返回Android's vendor-specific格式的MIME

Android's vendor-specific MIME format分成3部分

  1. 类型: 固定为vnd
  2. 子类型: 如果是单行, 为android.cursor.item/, 多行则是android.cursor.dir/
  3. 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

  1. Cursor是一个接口, 表示行数据的集合.
  2. ListView有一个SimpleCursorAdapter可以方便显示Cursor中的数据

ContentValues

通过ContentValues赋值ContentProvider中字段, 然后通过ContentResolver#insert()插入数据或者通过ContentResolver#update()更新数据

ContentProviderOperation

通过ContentProviderOperationContentResolver#applyBatch()可以批量处理数据, 例如一次插入多条数据, 或者同时向一个ContentProvider中的不同表插入数据
这个组合的主要作用是保持一系列操作的原子性, 即要么所有操作都成功, 要么所有操作都不成功.

注意, 这里的批量操作都是对同一个ContentProvider操作的.

Loader

通过CursorLoader异步处理数据

通过Intent间接处理数据

注意, 并不是直接将Intent直接传递给ContentProvider
当你的app没有权限访问某个ContentProvider的数据时, 通过Intent启动有权限的app, 该app获取到数据后返回一个特殊的URI给你, 然后你的app可以通过这个URI临时获得访问数据的权限.

大致流程:

  1. 使用Context#startActivityForResult()通过非指定的方式启动app
  2. 根据Intent中的信息, 相应的app会被启动, 用户在该app操作结束后, 该app通过setResult()返回一个Intent到你的app
  3. 返回的Intent中包含指定数据的content URI, 然后你的app可以通过这个URI从ContentProvider中获取该URI指定的数据

提供数据的方法

创建一个ContentProvider来提供数据.
除了上面提及的官方用途, 如果你想使用AbstractThreadedSyncAdapter, CursorAdapter, 或者CursorLoader, 那么你也需要创建一个ContentProvider.
如果这些你都不需要, 那你很可能并不需要创建ContentProvider.

实现步骤

1. 创建具体的类继承ContentProvider

ContentProvider是一个抽象类, 需要实现对应的方法, ContentResolver是通过这些方法对数据进行操作.

  1. onCreate : 当ContentProvider被创建时会回调, 一般ContentProvider是在ContentResolver尝试操作provider的时候才会被创建, 必须返回trueProvider才会生效.
  2. getType(): 返回MIME类型, 必须实现, 实际上就是对外声明Provider会返回的数据类型
  3. getStreamTypes(): 如果是提供File Data则期望实现, 返回一个指定文件类型的字符串数组, 数组中的值为MIME
  4. 操作数据的方法, 操作数据的方法会被ContentResolver对应的方法调用, 在这里你可以实现任意逻辑, 并不一定要按方法名操作数据, 例如可以拒绝返回某列数据, 或者固定返回某条数据等.

注意:

  1. 操作数据的方法需要考虑线程安全, 能够被不同的线程同时调用
  2. 对于Content URI, 可以利用UriMatcher工具类来解析传进来的URI

2. 约定数据格式

通常还需要在ContentProvider类中增加Contract类来约定数据格式. 实际就是包含一系列常量的接口.
通常需要指定的内容包括:

  1. content URIs, 包括authority, path, 相当于指定数据库名字和包含的表
  2. 列名, 相当于声明表的结构

3. 声明Provider

在AndroidManifest.xml中声明Provider, 并进行相关设置, 例如是否启动, 权限控制等, 具体看上文<provider>

4. 允许临时权限

如果想授权临时权限, 除了在AndroidManifest.xml中声明外, 一般还需要添加一个Activity来处理其他app发起的隐式Intent, 然后通过Activity#setResult()方法来把指定的URI返回给其他app.

特殊用法

参考How does Firebase initialize on Android?
因为Provider具有以下特性

  1. app进程启动时, Provider比Activity, Service和BroadcastReceiver都要早初始化, 而且启动时在ContentProvider#onCreate()回调中能够获取到Context实例.
  2. 可以通过文件合并在build的时候自动合并进AndroidManifest.xml, 所以不需要在主项目中声明.

所以可以用来在app启动时初始化第三方服务.
注意: 这种用法明显不符合ContentProvider的设计初衷, 而且引用的文章中还提到一些需要注意的点. 但是不得不说对于SDK的初始化来说, 这种方式非常优雅, 不需要添加任何初始化代码, 因此附加在这里.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容