筆記: ContentProvider

參考Content Providers

對于ContentProvider, 可以把它看做為一個(gè)數(shù)據(jù)庫, 數(shù)據(jù)庫中包含表, 而Provider中可以包含任意數(shù)量的path.

作用

主要目的是為了處理app數(shù)據(jù), 包括

  1. 獲取自身的數(shù)據(jù), 一般是特殊用途, 例如為了配合搜索框架實(shí)現(xiàn)搜索推薦功能
  2. 獲取其他app的數(shù)據(jù)
  3. 共享數(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.

官方用途

  1. 使用search framework實(shí)現(xiàn)search suggestions
  2. 共享數(shù)據(jù)給widget(什么widget沒有細(xì)說, 暫不探究)
  3. 共享數(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崩潰.

注意

  1. app自身內(nèi)的組件能夠任意讀寫數(shù)據(jù)
  2. 如果app不指定需要的權(quán)限, 表示不共享數(shù)據(jù)
  3. 對于本來就公開的數(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)鍵

  1. 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ù).
  2. <grant-uri-permission> : 用于指定具體的Content URI, 和上面的Path-level權(quán)限的<path-permisson>類似
  3. FLAG_GRANT_READ_URI_PERMISSIONFLAG_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.

屬性

  1. android:authorities : 指定authority, 可以指定多個(gè), 至少指定一個(gè). 必須在整個(gè)系統(tǒng)里唯一, 所以一般會帶上包名, 這里指定的authority就是Content URI中的authority.
  2. android:enabled : 是否啟用這個(gè)Provider, 默認(rèn)為true, <application>也有一個(gè)類似的屬性, 也是默認(rèn)為true, 當(dāng)兩個(gè)都未true時(shí)才會初始化Provider
  3. android:exported : SDK17之前不能設(shè)置, 默認(rèn)為true, SDK17及之后默認(rèn)為false, true表示其他app可以訪問這個(gè)Provider, 反之則只有相同UID(user ID)的app可以訪問, 可以理解為其他app不能訪問.
  4. 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都可以.
  5. android:initOrder : 指定同一進(jìn)程中的Provider初始化順序, 數(shù)字越大, 越早初始化, 一般用來解決Provider的依賴問題.
  6. 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.
  7. android:name : 具體的Provider類, 必須指定, 如果是.開頭則會自動添加app的包名為前綴.
  8. android:process : 指定Provider運(yùn)行的進(jìn)程名, 默認(rèn)是app進(jìn)程(名稱是app的包名), 如果是:開頭則會是app的私有進(jìn)程, 如果是小寫字母開頭則是全局進(jìn)程. 會覆蓋<application>中的同名屬性設(shè)置.
  9. android:permission : 指定整個(gè)Provider的讀權(quán)限和寫權(quán)限, 會被readPermissionwritePermission覆蓋
  10. android:readPermissionandroid:writePermission : 單獨(dú)控制讀權(quán)限和寫權(quán)限, 會覆蓋android:permission
  11. android:syncable : Provider中的數(shù)據(jù)是否會被同步的遠(yuǎn)程服務(wù)器
  12. android:iconandroid:label : 在Setting -> Apps -> All里面顯示

子標(biāo)簽

<path-permission>

用來指定該P(yáng)rovider下具體的Content URI需要的權(quán)限.

  1. android:path : 具體的path, 注意path需要帶上/前綴, 例如/path
  2. android:pathPrefix : 所有path帶該前綴的URI
  3. android:pathPattern : 匹配值, \\*匹配任意數(shù)量的前一個(gè)字符, \\.\\*匹配任何數(shù)量的任意字符. 以上3個(gè)屬性只能指定其中一個(gè)
  4. android:permission : 指定該URI讀和寫的權(quán)限
  5. android:readPermissionandroid:writePermission: 單獨(dú)設(shè)置讀寫權(quán)限, 會覆蓋android:permission.
<grant-uri-permisstion>

用來指定可以通過臨時(shí)權(quán)限訪問的Content URI.
包含android:path, android:pathPrefixandroid: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類型

  1. 標(biāo)準(zhǔn)的類型可以參考IANA MIME Media Types, 標(biāo)準(zhǔn)類型包含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)中唯一, 一般取表名, 例如一個(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

  1. Cursor是一個(gè)接口, 表示行數(shù)據(jù)的集合.
  2. ListView有一個(gè)SimpleCursorAdapter可以方便顯示Cursor中的數(shù)據(jù)

ContentValues

通過ContentValues賦值ContentProvider中字段, 然后通過ContentResolver#insert()插入數(shù)據(jù)或者通過ContentResolver#update()更新數(shù)據(jù)

ContentProviderOperation

通過ContentProviderOperationContentResolver#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)限.

大致流程:

  1. 使用Context#startActivityForResult()通過非指定的方式啟動app
  2. 根據(jù)Intent中的信息, 相應(yīng)的app會被啟動, 用戶在該app操作結(jié)束后, 該app通過setResult()返回一個(gè)Intent到你的app
  3. 返回的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)行操作.

  1. onCreate : 當(dāng)ContentProvider被創(chuàng)建時(shí)會回調(diào), 一般ContentProvider是在ContentResolver嘗試操作provider的時(shí)候才會被創(chuàng)建, 必須返回trueProvider才會生效.
  2. getType(): 返回MIME類型, 必須實(shí)現(xiàn), 實(shí)際上就是對外聲明Provider會返回的數(shù)據(jù)類型
  3. getStreamTypes(): 如果是提供File Data則期望實(shí)現(xiàn), 返回一個(gè)指定文件類型的字符串?dāng)?shù)組, 數(shù)組中的值為MIME
  4. 操作數(shù)據(jù)的方法, 操作數(shù)據(jù)的方法會被ContentResolver對應(yīng)的方法調(diào)用, 在這里你可以實(shí)現(xiàn)任意邏輯, 并不一定要按方法名操作數(shù)據(jù), 例如可以拒絕返回某列數(shù)據(jù), 或者固定返回某條數(shù)據(jù)等.

注意:

  1. 操作數(shù)據(jù)的方法需要考慮線程安全, 能夠被不同的線程同時(shí)調(diào)用
  2. 對于Content URI, 可以利用UriMatcher工具類來解析傳進(jìn)來的URI

2. 約定數(shù)據(jù)格式

通常還需要在ContentProvider類中增加Contract類來約定數(shù)據(jù)格式. 實(shí)際就是包含一系列常量的接口.
通常需要指定的內(nèi)容包括:

  1. content URIs, 包括authority, path, 相當(dāng)于指定數(shù)據(jù)庫名字和包含的表
  2. 列名, 相當(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具有以下特性

  1. app進(jìn)程啟動時(shí), Provider比Activity, Service和BroadcastReceiver都要早初始化, 而且啟動時(shí)在ContentProvider#onCreate()回調(diào)中能夠獲取到Context實(shí)例.
  2. 可以通過文件合并在build的時(shí)候自動合并進(jìn)AndroidManifest.xml, 所以不需要在主項(xiàng)目中聲明.

所以可以用來在app啟動時(shí)初始化第三方服務(wù).
注意: 這種用法明顯不符合ContentProvider的設(shè)計(jì)初衷, 而且引用的文章中還提到一些需要注意的點(diǎn). 但是不得不說對于SDK的初始化來說, 這種方式非常優(yōu)雅, 不需要添加任何初始化代碼, 因此附加在這里.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瞬欧,一起剝皮案震驚了整個(gè)濱河市抢蚀,隨后出現(xiàn)的幾起案子窟感,更是在濱河造成了極大的恐慌贸铜,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件檩小,死亡現(xiàn)場離奇詭異浅妆,居然都是意外死亡穿仪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門框舔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蹦玫,“玉大人,你說我怎么就攤上這事刘绣∮8龋” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵纬凤,是天一觀的道長福贞。 經(jīng)常有香客問我,道長停士,這世上最難降的妖魔是什么挖帘? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮向瓷,結(jié)果婚禮上肠套,老公的妹妹穿的比我還像新娘。我一直安慰自己猖任,他們只是感情好你稚,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著朱躺,像睡著了一般刁赖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上长搀,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天宇弛,我揣著相機(jī)與錄音,去河邊找鬼源请。 笑死枪芒,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谁尸。 我是一名探鬼主播舅踪,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼良蛮!你這毒婦竟也來了抽碌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤决瞳,失蹤者是張志新(化名)和其女友劉穎货徙,沒想到半個(gè)月后左权,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡痴颊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年赏迟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祷舀。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瀑梗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出裳扯,到底是詐尸還是另有隱情抛丽,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布饰豺,位于F島的核電站亿鲜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏冤吨。R本人自食惡果不足惜蒿柳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望漩蟆。 院中可真熱鬧垒探,春花似錦、人聲如沸怠李。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捺癞。三九已至夷蚊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間髓介,已是汗流浹背惕鼓。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留唐础,地道東北人箱歧。 一個(gè)月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像一膨,于是被迫代替她去往敵國和親呀邢。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容