0x00 前言
這篇關(guān)于 ContentProvider 的總結(jié)文章,早在四月份就想寫了,但是一直沒有時間和精力去寫,而且我也不想隨便寫一下,還是希望能保證一定的文章質(zhì)量。恰巧昨天為自己定了一個目標,就是每周學習一個主題,并輸出相關(guān)總結(jié)文章或例程。所以,第一周的主題就定為ContentProvider。下面看下本文會涉及到的幾點內(nèi)容:
- 什么是 ContentProvider;
- 使用 ContentProvider 的優(yōu)點;
- 一個使用系統(tǒng) ContentProvider 的例程。
0x01 ContentProvider簡介
關(guān)于 ContentProvider,搞 Android 的人都不會陌生,它是 Andriod 中的四大組件之一,但或許是存在感最低的一個組件。為什么這么說呢?因為一般來說我們開發(fā)應用很少會去自定義這種組件,但是其實它非常重要和有用。我們先來看下它的作用:
一看到它的名字——內(nèi)容提供者,就能大概猜到它與數(shù)據(jù)是相關(guān)的,它的作用就是實現(xiàn)數(shù)據(jù)共享,這個共享不僅僅限于本應用內(nèi)部,還包括系統(tǒng)中的其他應用(即跨進程的數(shù)據(jù)共享)。為此,它實現(xiàn)了一套標準的接口方法用于存取(增、刪、改、查,這些操作其實和標準的 SQL 操作是類似的)它管理的數(shù)據(jù),而底層的數(shù)據(jù)如何存儲(存儲于文件系統(tǒng)或者 SQLite 數(shù)據(jù)庫等),調(diào)用者根本不用去關(guān)心:
// 查詢記錄
Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
// 插入記錄
Uri insert (Uri uri, ContentValues values)
// 更新記錄
int update (Uri uri, ContentValues values, String selection, String[] selectionArgs)
// 刪除記錄
int delete (Uri uri, String selection, String[] selectionArgs)
雖然 ContentProvider 定義了一系列的方法,但是我們的應用并不直接調(diào)用這些方法,而是使用一個ContentResolver對象,通過調(diào)用它的方法(與 ContentProvider 中的方法一一對應)作為替代,如圖1所示。

圖 1:圖片來自 Udacity 學院課程截圖
說到這里大家肯定會奇怪,ContentResolver 是如何定位到我們想要的那個的ContentProvider 的呢?你要知道Android系統(tǒng)已經(jīng)為我們提供了很多的 ContentProvider,例如日歷、聯(lián)系人、多媒體(音頻、視頻以及圖像)等數(shù)據(jù)。
其實,答案就是各個方法參數(shù)中的 Uri,就像你在瀏覽器的地址欄中輸入一個網(wǎng)址 (URL),按下回車后,瀏覽器就會向服務(wù)器發(fā)起請求,而對應的服務(wù)器會返回相應的結(jié)果給瀏覽器。類似地,當ContentResolver也是通過一個給定的 URI (UR L是 URI 的一個子集)發(fā)起請求,Android 系統(tǒng)會根據(jù)給定URI中的authority(URI 的組成部分找到那個 ContentProvider ,并將該請求傳遞給該 ContentProvider。每個 ContentProvider 的 authority 都是唯一的,而且都會在 Android 系統(tǒng)注冊。ContentProvider 收到請求后,會解析 URI 剩余的部分(通過輔助類UriMatcher實現(xiàn)),然后返回相應數(shù)據(jù)。
通過以上內(nèi)容,我想大家對 ContentProvider 應該有了一個大概的了解了吧,下面我將通過用戶詞典(user dictionary:用于存儲一些用戶想保存的非標準單詞的拼寫)這個系統(tǒng)內(nèi)置的 ContentProvider來向大家說明如何使用它。
0x02 如何使用 ContentProvider
我們要知道 ContentProvider 提供的數(shù)據(jù)的形式與一般關(guān)系型的數(shù)據(jù)庫是一樣的,都是通過一張或多張的數(shù)據(jù)庫表來實現(xiàn)的,例如用戶詞典的數(shù)據(jù)表是這樣的,其中每一行代碼一個單詞實例,每一列代表每個單詞的屬性:
表1:用戶詞典數(shù)據(jù)表樣例引用
| word | app id | frequency | locale | _ID |
|---|---|---|---|---|
| mapreduce | user12 | 100 | en_US | 1 |
| applet | user2 | 200 | pt_BR | 2 |
| const | user1 | 255 | en_UK | 3 |
上面這個表跟我們平時操作的數(shù)據(jù)表沒有什么區(qū)別嘛,說下本節(jié)我們要實現(xiàn)的功能就是通過一個ListView來顯示表1中的數(shù)據(jù),就這么簡單,具體步驟如下:
(1) 獲取相應 ContentProvider 的權(quán)限
Android 系統(tǒng)中內(nèi)置了很多的 ContentProvider,當我們訪問它們時,需要在AndroidManifest.xml文件中聲明相應的讀/寫權(quán)限,如果你聲明了寫權(quán)限,應該不需要再申請讀權(quán)限,用戶詞典的具體權(quán)限聲明如下所示:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY"/>
<uses-permission android:name="android.permission.WRITE_USER_DICTIONARY" />
(2) 訪問正確的 ContentProvider
如前面所述,我們的應用不會直接訪問 ContentProvider,我們需要通過 ContentResolver 以及正確的 URI,如圖2所示,同時調(diào)用相應的方法:
query、insert、update和delete。

圖 2:圖片來自 Udacity 學院課程截圖
我們這個例子只是簡單地查詢數(shù)據(jù),具體示例代碼如下所示:
// 獲取 ContentResolver 用于向 ContentProvider 發(fā)送數(shù)據(jù)請求
ContentResolver resolver = getContentResolver();
// 獲取 Cursor,它包含了Words數(shù)據(jù)表的所有行
Cursor cursor = resolver.query(UserDictionary.Words.CONTENT_URI, // Uri
null, // projection
null, // selection
null, // selectionArgs
null // sortOrder
);
Content URI
上述代碼中query方法第一個參數(shù)UserDictionary.Words.CONTENT_URI,即之前我們提到的 URI,它一般由三部分組成content://authority/path,它用于標識 ContentProvider 中的數(shù)據(jù)記錄,詳細請參考Content URIs。
(3) 在 UI 上展現(xiàn)數(shù)據(jù)
我們的目標是在 ListView 中顯示這些單詞,在第(2)個步驟得到的cursor其實就相當于一個數(shù)據(jù)源,所以只要實現(xiàn)相應的 Adapter 即可,具體代碼如下所示:
// 使用系統(tǒng)提供的 SimpleCursorAdapter
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
android.R.layout.two_line_list_item,
cursor,
COLUMNS_TO_BE_BOUND,
LAYOUT_ITEMS_TO_FILL,
0
);
dictListView.setAdapter(adapter);
0x03 總結(jié)
根據(jù)以上這些的內(nèi)容,我們稍微總結(jié)一下使用 ContentProvider 的兩個優(yōu)點:
- 底層的數(shù)據(jù)源對上層是透明的,可以非常方便地替換;
- 允許系統(tǒng)中的多個應用對同一個數(shù)據(jù)源進行(安全的)訪問和修改,而相應的修改對于多個應用來說都是可見的。
看到這里我想很多人會想:如果我們并不想與其他應用共享我們的應用的數(shù)據(jù),那么我們應該是不需要開發(fā)這種類型的組件。以前我也是這么想的,既然 ContentProvider 底層數(shù)據(jù)的存儲還是通過 SQLite 數(shù)據(jù)庫,為什么要用它呢?其實不然,首先我們使用 SQLite 數(shù)據(jù)庫存儲我們的數(shù)據(jù),我們還是需要在 SQLite 數(shù)據(jù)庫之上封裝一層類似 ContentProvider 提供的那套接口方法;其次,我們還可以利用 Android 系統(tǒng)提供的一些相關(guān)功能類與 ContentProvider 配合起來使用。 例如,SyncAdaters, Loaders, CursorAdapter。說到這里我推薦大家一個視頻吧,來自2010年 Google IO 大會,講的是 Android 應用的設(shè)計模式(架構(gòu)),其中一共提出了三種設(shè)計模式,而 ContentProvider 在其中扮演了非常重要的角色: