對于ContentProvider, 可以把它看做為一個(gè)數(shù)據(jù)庫, 數(shù)據(jù)庫中包含表, 而Provider中可以包含任意數(shù)量的path.
作用
主要目的是為了處理app數(shù)據(jù), 包括
- 獲取自身的數(shù)據(jù), 一般是特殊用途, 例如為了配合搜索框架實(shí)現(xiàn)搜索推薦功能
- 獲取其他app的數(shù)據(jù)
- 共享數(shù)據(jù)給其他app
注意: 不同app是運(yùn)行在不同進(jìn)程中的, 所以ContentProvider也是一種進(jìn)程間傳遞數(shù)據(jù)的方式.
作用2, 3其實(shí)是一樣的, 最主要的目的是為了實(shí)現(xiàn)不同app間共享數(shù)據(jù), 當(dāng)然也可以用來抽象本地?cái)?shù)據(jù)獲取的方式, 不過如果僅僅是想抽象本地獲取數(shù)據(jù)的方式, 沒有必要使用ContentProvider.
官方用途
- 使用search framework實(shí)現(xiàn)search suggestions
- 共享數(shù)據(jù)給widget(什么widget沒有細(xì)說, 暫不探究)
- 共享數(shù)據(jù)給其他app
注: SDK自帶的ContentProvider都在android.provider包中
優(yōu)點(diǎn)
權(quán)限控制
共享數(shù)據(jù)給其他app時(shí)可以增加權(quán)限控制, 甚至分別控制讀數(shù)據(jù)和寫數(shù)據(jù)的權(quán)限, 增加安全性.
注意: 這里的權(quán)限讀取數(shù)據(jù)的app需要在AndroidManifest.xml
中使用<uses-permission>
靜態(tài)指定, 不能在運(yùn)行時(shí)申請權(quán)限.
如果在安裝時(shí)用戶拒絕給權(quán)限, 在進(jìn)行讀寫操作時(shí)應(yīng)該會拋出異常導(dǎo)致app崩潰.
所以在通過Provider獲取其他app的數(shù)據(jù)時(shí)應(yīng)該進(jìn)行權(quán)限檢查避免app崩潰.
注意
- app自身內(nèi)的組件能夠任意讀寫數(shù)據(jù)
- 如果app不指定需要的權(quán)限, 表示不共享數(shù)據(jù)
- 對于本來就公開的數(shù)據(jù), 例如公共儲存區(qū)域(SD卡)的數(shù)據(jù)庫或文件, Provider的權(quán)限控制不會起作用
Provider-level權(quán)限
指定整個(gè)Provider的訪問權(quán)限, 類比成控制訪問整個(gè)數(shù)據(jù)庫的權(quán)限
讀寫權(quán)限
通過<provider android:permission>
屬性來同時(shí)指定讀操作和寫操作需要的權(quán)限
讀權(quán)限
通過<provider android:readPermission>
屬性來指定讀權(quán)限, 會覆蓋讀寫權(quán)限
寫權(quán)限
通過<provider android:writePermission>
屬性來指定寫權(quán)限, 會覆蓋讀寫權(quán)限
Path-level權(quán)限
想指定具體某個(gè)Content URI的權(quán)限, 類比成單獨(dú)控制數(shù)據(jù)庫中某個(gè)表的權(quán)限, 可以通過<path-permisson>
來實(shí)現(xiàn), 同時(shí)會覆蓋Provider-level權(quán)限, 可以設(shè)定讀寫權(quán)限, 讀權(quán)限和寫權(quán)限, 跟Provider-level權(quán)限一樣, 讀權(quán)限和寫權(quán)限會覆蓋讀寫權(quán)限
臨時(shí)權(quán)限
臨時(shí)權(quán)限機(jī)制可以允許沒有申請置頂上述置頂權(quán)限的其他app臨時(shí)訪問數(shù)據(jù). 一般結(jié)合startActivityForResult
來獲取包含臨時(shí)權(quán)限的URI, 然后通過URI來訪問數(shù)據(jù).
官網(wǎng)的例子是:
你寫了一個(gè)email客戶端, 里面保存了一堆圖片附件, 你希望共享這些圖片文件給其他app.
假設(shè)現(xiàn)在有個(gè)圖片瀏覽app, 它沒有聲明獲取圖片附件的權(quán)限, 所以不能直接通過ContentResolver來獲取email客戶端的圖片文件, 如果想通過臨時(shí)權(quán)限獲取圖片文件, 一般是,
圖片瀏覽app先通過Intent隱式打開email客戶端的一個(gè)頁面, 例如選擇圖片的列表頁, 然后email客戶端返回一個(gè)包含Content URI的Intent給圖片瀏覽app, 這個(gè)Content URI包含了臨時(shí)訪問權(quán)限的flag, 然后圖片瀏覽app就可以通過這個(gè)URI訪問email客戶端中的圖片文件了.
臨時(shí)的意思是, 一旦離開了獲取臨時(shí)權(quán)限的Activity, 那這個(gè)訪問權(quán)限就失效了, 需要重新獲取.
注意: Intent中還有FLAG_GRANT_PERSISTABLE_URI_PERMISSION
可以讓被授權(quán)的app保留這個(gè)臨時(shí)權(quán)限, 直到主動放棄授權(quán).
關(guān)鍵
-
android:grantUriPermissions
: 默認(rèn)為false
, 當(dāng)為false
時(shí)需要增加<grant-uri-permission>
來指定允許臨時(shí)權(quán)限訪問的具體Content URI, 設(shè)置為true
時(shí)表示整個(gè)Provider都接受臨時(shí)權(quán)限訪問數(shù)據(jù). -
<grant-uri-permission>
: 用于指定具體的Content URI, 和上面的Path-level權(quán)限的<path-permisson>
類似 -
FLAG_GRANT_READ_URI_PERMISSION
和FLAG_GRANT_WRITE_URI_PERMISSION
: 配合Intent#setFlags
讓Intent中的URI能夠被無權(quán)限的app臨時(shí)訪問.
抽象數(shù)據(jù)獲取方式
ContentProvider實(shí)際上是中間層, 所有人都通過它獲取數(shù)據(jù), 而不用理會具體數(shù)據(jù)儲存的方式.
一般情況下app都會抽象數(shù)據(jù)獲取方式, 當(dāng)你需要共享數(shù)據(jù)給其他app的時(shí)候, 使用ContentProvider顯得更加統(tǒng)一. 但是當(dāng)你不需要ContentProvider提供的特性時(shí), 自己抽象會更加方便.
聲明ContentProvider
<provider>
給app增加ContentProvider需要在<application>
下添加<provider>
標(biāo)簽來聲明Provider.
屬性
-
android:authorities
: 指定authority, 可以指定多個(gè), 至少指定一個(gè). 必須在整個(gè)系統(tǒng)里唯一, 所以一般會帶上包名, 這里指定的authority就是Content URI中的authority. -
android:enabled
: 是否啟用這個(gè)Provider, 默認(rèn)為true
,<application>
也有一個(gè)類似的屬性, 也是默認(rèn)為true
, 當(dāng)兩個(gè)都未true
時(shí)才會初始化Provider -
android:exported
: SDK17之前不能設(shè)置, 默認(rèn)為true
, SDK17及之后默認(rèn)為false
,true
表示其他app可以訪問這個(gè)Provider, 反之則只有相同UID(user ID)的app可以訪問, 可以理解為其他app不能訪問. -
android:grantUriPermissions
: 是否授權(quán)臨時(shí)權(quán)限訪問這個(gè)Provider, 默認(rèn)是false
, 當(dāng)為false
時(shí), 只有<grant-uri-permission>
指定的URI能夠通過臨時(shí)權(quán)限訪問, 如果未true
則所有URI都可以. -
android:initOrder
: 指定同一進(jìn)程中的Provider初始化順序, 數(shù)字越大, 越早初始化, 一般用來解決Provider的依賴問題. -
android:multiprocess
: 當(dāng)app包含多個(gè)進(jìn)程的時(shí)候用來指定不同的進(jìn)程是共用一個(gè)Provider還是各自持有自己的Provider.true
表示每個(gè)進(jìn)程都有一個(gè)Provider實(shí)例, 可以減少進(jìn)程間的通訊, 但增加內(nèi)存消耗. 默認(rèn)是false
. -
android:name
: 具體的Provider類, 必須指定, 如果是.
開頭則會自動添加app的包名為前綴. -
android:process
: 指定Provider運(yùn)行的進(jìn)程名, 默認(rèn)是app進(jìn)程(名稱是app的包名), 如果是:
開頭則會是app的私有進(jìn)程, 如果是小寫字母開頭則是全局進(jìn)程. 會覆蓋<application>
中的同名屬性設(shè)置. -
android:permission
: 指定整個(gè)Provider的讀權(quán)限和寫權(quán)限, 會被readPermission
和writePermission
覆蓋 -
android:readPermission
和android:writePermission
: 單獨(dú)控制讀權(quán)限和寫權(quán)限, 會覆蓋android:permission
-
android:syncable
: Provider中的數(shù)據(jù)是否會被同步的遠(yuǎn)程服務(wù)器 -
android:icon
和android:label
: 在Setting -> Apps -> All
里面顯示
子標(biāo)簽
<path-permission>
用來指定該P(yáng)rovider下具體的Content URI需要的權(quán)限.
-
android:path
: 具體的path, 注意path需要帶上/
前綴, 例如/path
-
android:pathPrefix
: 所有path帶該前綴的URI -
android:pathPattern
: 匹配值,\\*
匹配任意數(shù)量的前一個(gè)字符,\\.\\*
匹配任何數(shù)量的任意字符. 以上3個(gè)屬性只能指定其中一個(gè) -
android:permission
: 指定該URI讀和寫的權(quán)限 -
android:readPermission
和android:writePermission
: 單獨(dú)設(shè)置讀寫權(quán)限, 會覆蓋android:permission
.
<grant-uri-permisstion>
用來指定可以通過臨時(shí)權(quán)限訪問的Content URI.
包含android:path
, android:pathPrefix
和android:pathPattern
, 限制和作用同上.
相關(guān)知識
Content URI
ContentResolver中大部分方法都需要指定一個(gè)Content URI.
Content URI是一個(gè)指定了ContentProvider中的數(shù)據(jù)的URI
形式為
content://providerName/tableName
// 指向某一行
content://providerName/tableName/id
URI的scheme固定為content://
, authority(host:port)為指向操作的ContentProvider, path為ContentProvider中的表名, 另外可以加id來指向具體的某一行, 可以使用ContentUris#withAppendedId
來組合URI.
可以把一個(gè)ContentProvider看作一個(gè)數(shù)據(jù)庫, path是一個(gè)表, 數(shù)據(jù)庫中可以存在多個(gè)表, id則是表中具體的一條數(shù)據(jù).
MIME類型
- 標(biāo)準(zhǔn)的類型可以參考IANA MIME Media Types, 標(biāo)準(zhǔn)類型包含
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)中唯一, 一般取表名, 例如一個(gè)多個(gè)行的MIME類型會是vnd.android.cursor.dir/vnd.com.example.provider.table1
獲取數(shù)據(jù)用法
ContentResolver
雖然ContentProvider是中間層, 不過還是可以把它看作一個(gè)數(shù)據(jù)容器, 通過ContentResolver從這個(gè)容器中提取數(shù)據(jù).
跟數(shù)據(jù)庫類似, 可以通過ContentResolver對ContentProvider中的數(shù)據(jù)進(jìn)行CRUD(create, retrieve, update, and delete)操作.
通過Context#getContentResolver()
來獲取一個(gè)ContentResolver實(shí)例.
然后通過ContentResolver#query()
來獲取數(shù)據(jù), 得到一個(gè)Cursor
實(shí)例
Cursor
-
Cursor
是一個(gè)接口, 表示行數(shù)據(jù)的集合. -
ListView
有一個(gè)SimpleCursorAdapter
可以方便顯示Cursor
中的數(shù)據(jù)
ContentValues
通過ContentValues
賦值ContentProvider中字段, 然后通過ContentResolver#insert()
插入數(shù)據(jù)或者通過ContentResolver#update()
更新數(shù)據(jù)
ContentProviderOperation
通過ContentProviderOperation
和ContentResolver#applyBatch()
可以批量處理數(shù)據(jù), 例如一次插入多條數(shù)據(jù), 或者同時(shí)向一個(gè)ContentProvider中的不同表插入數(shù)據(jù)
這個(gè)組合的主要作用是保持一系列操作的原子性, 即要么所有操作都成功, 要么所有操作都不成功.
注意, 這里的批量操作都是對同一個(gè)ContentProvider操作的.
Loader
通過CursorLoader
異步處理數(shù)據(jù)
通過Intent間接處理數(shù)據(jù)
注意, 并不是直接將Intent
直接傳遞給ContentProvider
當(dāng)你的app沒有權(quán)限訪問某個(gè)ContentProvider的數(shù)據(jù)時(shí), 通過Intent
啟動有權(quán)限的app, 該app獲取到數(shù)據(jù)后返回一個(gè)特殊的URI給你, 然后你的app可以通過這個(gè)URI臨時(shí)獲得訪問數(shù)據(jù)的權(quán)限.
大致流程:
- 使用
Context#startActivityForResult()
通過非指定的方式啟動app - 根據(jù)
Intent
中的信息, 相應(yīng)的app會被啟動, 用戶在該app操作結(jié)束后, 該app通過setResult()
返回一個(gè)Intent
到你的app - 返回的
Intent
中包含指定數(shù)據(jù)的content URI, 然后你的app可以通過這個(gè)URI從ContentProvider中獲取該URI指定的數(shù)據(jù)
提供數(shù)據(jù)的方法
創(chuàng)建一個(gè)ContentProvider來提供數(shù)據(jù).
除了上面提及的官方用途, 如果你想使用AbstractThreadedSyncAdapter
, CursorAdapter
, 或者CursorLoader
, 那么你也需要?jiǎng)?chuàng)建一個(gè)ContentProvider.
如果這些你都不需要, 那你很可能并不需要?jiǎng)?chuàng)建ContentProvider.
實(shí)現(xiàn)步驟
1. 創(chuàng)建具體的類繼承ContentProvider
ContentProvider是一個(gè)抽象類, 需要實(shí)現(xiàn)對應(yīng)的方法, ContentResolver是通過這些方法對數(shù)據(jù)進(jìn)行操作.
-
onCreate
: 當(dāng)ContentProvider被創(chuàng)建時(shí)會回調(diào), 一般ContentProvider是在ContentResolver嘗試操作provider的時(shí)候才會被創(chuàng)建, 必須返回true
Provider才會生效. -
getType()
: 返回MIME類型, 必須實(shí)現(xiàn), 實(shí)際上就是對外聲明Provider會返回的數(shù)據(jù)類型 -
getStreamTypes()
: 如果是提供File Data則期望實(shí)現(xiàn), 返回一個(gè)指定文件類型的字符串?dāng)?shù)組, 數(shù)組中的值為MIME - 操作數(shù)據(jù)的方法, 操作數(shù)據(jù)的方法會被ContentResolver對應(yīng)的方法調(diào)用, 在這里你可以實(shí)現(xiàn)任意邏輯, 并不一定要按方法名操作數(shù)據(jù), 例如可以拒絕返回某列數(shù)據(jù), 或者固定返回某條數(shù)據(jù)等.
注意:
- 操作數(shù)據(jù)的方法需要考慮線程安全, 能夠被不同的線程同時(shí)調(diào)用
- 對于Content URI, 可以利用
UriMatcher
工具類來解析傳進(jìn)來的URI
2. 約定數(shù)據(jù)格式
通常還需要在ContentProvider類中增加Contract類來約定數(shù)據(jù)格式. 實(shí)際就是包含一系列常量的接口.
通常需要指定的內(nèi)容包括:
- content URIs, 包括authority, path, 相當(dāng)于指定數(shù)據(jù)庫名字和包含的表
- 列名, 相當(dāng)于聲明表的結(jié)構(gòu)
3. 聲明Provider
在AndroidManifest.xml中聲明Provider, 并進(jìn)行相關(guān)設(shè)置, 例如是否啟動, 權(quán)限控制等, 具體看上文<provider>
節(jié)
4. 允許臨時(shí)權(quán)限
如果想授權(quán)臨時(shí)權(quán)限, 除了在AndroidManifest.xml中聲明外, 一般還需要添加一個(gè)Activity來處理其他app發(fā)起的隱式Intent, 然后通過Activity#setResult()
方法來把指定的URI返回給其他app.
特殊用法
參考How does Firebase initialize on Android?
因?yàn)镻rovider具有以下特性
- app進(jìn)程啟動時(shí), Provider比Activity, Service和BroadcastReceiver都要早初始化, 而且啟動時(shí)在
ContentProvider#onCreate()
回調(diào)中能夠獲取到Context
實(shí)例. - 可以通過文件合并在build的時(shí)候自動合并進(jìn)AndroidManifest.xml, 所以不需要在主項(xiàng)目中聲明.
所以可以用來在app啟動時(shí)初始化第三方服務(wù).
注意: 這種用法明顯不符合ContentProvider的設(shè)計(jì)初衷, 而且引用的文章中還提到一些需要注意的點(diǎn). 但是不得不說對于SDK的初始化來說, 這種方式非常優(yōu)雅, 不需要添加任何初始化代碼, 因此附加在這里.