概述
Content Provider 以資料表的形式向外部應用程式提供資料,這與關係型資料庫中的表很類似。 其中,行(row)表示由多個不同型別資料構成的單個實體,每行資料中的列(column)代表實體中的一個數據項。
例如,使用者詞典就是 Android 系統內建的 Provider 之一,裡面記錄著使用者需要留存的自定義拼寫規則的單詞。 表1例舉了此 Provider 資料表中可以查詢的欄位資訊:
表1:
使用者詞典表舉例
word
app id
frequency
locale
_ID
mapreduce
user1
100
en_US
1
precompiler
user14
200
fr_FR
2
applet
user2
225
fr_CA
3
const
user1
255
pt_BR
4
int
user5
100
en_UK
5
在表1中,每行代表一個可能無法在標準詞典中查到的單詞。 每列代表與單詞相關的資料,比如首次使用時的地區(語言)。 每列的標題即為儲存時的列名稱。 引用
locale
列就可以得到每一行資料的地區資訊。 這裡的
_ID
列被用作“主鍵”(primary key),並且是由 Provider 自動維護的。
注意:
Provider 本身不需要用到主鍵,主鍵的名稱也不一定要是
_ID
。 但是,如果要把 Provider 作為資料來源與
ListView
繫結,則必須有一個列的名稱是
_ID
。 詳細要求將在 顯示查詢結果中描述。
訪問 Provider
應用程式是透過客戶端物件
ContentResolver
訪問 Content Provider 的。 此物件中包含一些方法,這些方法將會呼叫 Provider 物件中的同名方法。而 Provider 物件是
ContentProvider
某個具體子類的例項。
ContentResolver
中的方法內建了基本的“CRUD”(建立、查詢、更新、刪除(create、retrieve、update 和 delete))功能。
ContentResolver
物件運行於客戶端應用的程序中,而
ContentProvider
運行於提供 Provider 應用的程序中,兩者會自動完成程序間的通訊。
ContentProvider
還發揮著資料抽象層的作用,負責將內部資料以資料庫表的形式提供出來。
注意:
為了訪問 Provider,應用程式通常必須在 Manifest 檔案中請求相應的許可權
例如,要從 User Dictionary Provider 中讀取單詞及地區列表,就要用到
ContentResolver。query()
。
query()
方法會去呼叫 User Dictionary Provider 中對應的
ContentResolver。query()
方法。以下程式碼演示了
ContentResolver。query()
的呼叫過程:
1 // 查詢使用者詞典並返回結果2 mCursor = getContentResolver()。query(3 UserDictionary。Words。CONTENT_URI, // 單詞表的 Content URI4 mProjection, // 需要返回的列5 mSelectionClause, // 查詢條件6 mSelectionArgs, // 查詢條件的引數7 mSortOrder); // 返回結果的排序要求
表2給出了
query(Uri,projection,selection,selectionArgs,sortOrder)
的引數與 SQL SELECT 語句的對應關係:
表2:
Query() 與 SQL 查詢的對比
query() 引數
SELECT 關鍵字/引數
說明
Uri
FROM *table_name*
Uri
對應於
table_name
指定的 Provider 資料表名。
projection
*col,col,col,。。。*
projection
是包含返回列名稱的陣列。
selection
WHERE *col* = *value*
selection
指定查詢條件。
selectionArgs
(沒有固定值,該查詢引數將會替換查詢語句中的佔位符“?”。)
sortOrder
ORDER BY *col,col,。。。*
sortOrder
指定了返回
Cursor
中各行的顯示順序。
Content URI
Content URI
是一種用於標識 Provider 資料的 URI。 Content URI 包括了整個 Provider 的符號名稱(
authority
)和表名(
path
)。 呼叫客戶端的方法訪問 Provider 資料表時,表的 Content URI 是引數之一。
在前面的程式碼中,常量
CONTENT_URI
包含了指向使用者詞典中 “word” 表的 Content URI。
ContentResolver
物件將分離出 URI 中的 authority ,並用它“解析” 出 Provider,這是透過將 authority 與系統記錄的已有 Provider 清單進行比較來實現的。 然後
ContentResolver
就可以將查詢引數傳送給相應的 Provider 了。
ContentProvider
用 Content URI 的 path 部分選擇要訪問的資料表。 通常, Provider 公開的所有資料表都會帶有自己的
path
。
在上述程式碼中,“word”表的完整 URI 為:
content://user_dictionary/words
這裡的字串
user_dictionary
是 Provider 的 authority 部分, 字串
words
是資料表的 path 部分。 字串
content://
(
scheme
)是必須指定的,以表明這是一個 Content URI。
很多 Provider 提供了對單條記錄的訪問能力,只要在 URI 後面跟一個 ID 值即可。 例如,要讀取使用者詞典中
_ID
為
4
的資料行,可以使用以下 Content URI:
Uri singleUri =ContentUris。withAppendedId(UserDictionary。Words。CONTENT_URI,4);
如果已經讀取了一些資料,然後需要修改或刪除其中的某一條,這時就經常會用到 ID 值了。
注意:
Uri
和
Uri。Builder
類中已內建了一些工具性的方法,可以由字串搭建合乎規則的 Uri 物件。
ContentUris
中有一些在 URI 後面追加 ID 值的常用方法。 上述程式碼就用了
withAppendedId()
把 ID 追加到 UserDictionary 的 Content URI 之後。
從 Provider 讀取資料
本節將介紹從 Provider 讀取資料的過程,還是以 User Dictionary Provider 為例。
為了清晰起見,本節中的程式碼將會呼叫“UI 執行緒”中的
ContentResolver。query()
。但是在實際的程式碼中,應該在單獨的執行緒中實現非同步查詢。 一種方案是利用
CursorLoader
類,而且,以下只給出了部分程式碼,而非一個完整的應用程式。
從 Provider 中讀取資料的基本步驟如下所示:
申請讀取 Provider 的許可權。
編寫向 Provider 傳送查詢請求的程式碼。
申請讀取許可權
要從 Provider 讀取資料,應用程式需要擁有對 Provider 的“讀許可權”。 在執行時是無法申請該許可權的,只能在 Manifest 檔案中透過
指定。在 Manifest 檔案中的定義,實際上是表明此應用程式需要“申請”該許可權。 這樣使用者在安裝此應用程式時,就可以明確授權。
在 Provider 的參考文件中,給出了其用到的全部許可權的準確名稱。
User Dictionary Provider 在其 Manifest 檔案中定義了
android。permission。READ_USER_DICTIONARY
許可權, 因此要讀它的應用程式就必須請求該許可權。
構建查詢
接下來是構建查詢請求。 以下程式碼定義了一些變數,在訪問 User Dictionary Provider 時將會用到:
1 // “projection” 定義了要返回的資料列 2 String[] mProjection = 3 { 4 UserDictionary。Words。_ID, &n // 對應列名為 _ID 的 Contract Class 常量 5 UserDictionary。an class=“typ”>Words。WORD, // 對應列名為 word 的 Contract Class 常量 6 UserDictionary。an class=“typ”>Words。LOCALE &nbLOCALE // 對應列名為 local 的 Contract Class 常量 7 }; 8 9 // 定義存放查詢條件的字串10 String mSelectionClause =an class=“pln”> null; mSelectionArgs ={“”};
接下來的程式碼演示了
ContentResolver。query()
的使用方法,這裡以 User Dictionary Provider 為例。 Provider 客戶端查詢與 SQL 查詢很類似,也包含了需返回的列名、查詢條件和排序要求。
查詢返回的列名集合物件被稱為”投影“(
Projection
)(即變數
mProjection
)。
查詢資料的表示式被拆分為查詢條件和查詢引數。 查詢條件是由邏輯/布林表示式、列名、數值組成(即變數
mSelectionClause
)。 如果用引數
?
代替了具體數值,則查詢方法將會從查詢引數陣列(變數
mSelectionArgs
)中讀取實際的值。
在以下程式碼中,如果使用者沒有輸入單詞,則查詢語句將被置為
null
,這樣查詢將會返回 Provider 中的所有單詞。 如果使用者輸入了單詞,那麼查詢語句將會是
UserDictionary。Words。WORD + “ = ?”
,且查詢引數陣列中的第一個成員被設為使用者輸入的單詞。
1 /* 2 * 定義只有一個成員的字串陣列,用於存放查詢引數。 3 */ 4 String[] mSelectionArgs ={“”}; 5 6 // 從使用者介面讀取一個單詞 7 mSearchString = mSearchWord。getText()。toString(); 8 9 // 別忘了在這裡新增檢查輸入內容是否非法或惡意的程式碼10 11 // 如果單詞為空字串,則讀取所有資料12 if(TextUtils。isEmpty(mSearchString)){13 // 將查詢語句設為 null 將返回所有資料14 mSelectionClause =null;15 mSelectionArgs[0]=“”;16 17 }else{18 // 由使用者錄入單詞構建查詢語句19 mSelectionClause =UserDictionary。Words。WORD +“ = ?”;20 21 // 將使用者錄入的字串置入查詢引數陣列中22 mSelectionArgs[0]= mSearchString;23 24 }25 26 // 查詢資料並返回遊標(Cursor)物件27 mCursor = getContentResolver()。query(28 UserDictionary。Words。CONTENT_URI, // 單詞表的 Content URI29 mProjection, // 需返回的列30 mSelectionClause // 為 null 或是使用者錄入的單詞31 mSelectionArgs, // 為空或是使用者錄入的字串32 mSortOrder); // 定義返回資料的排序規則33 34 // 在出錯時,某些 Provider 返回 null,另一些會丟擲異常35 if(null== mCursor){36 /*37 * 在這裡插入處理錯誤的程式碼。38 * 請勿在這裡使用遊標!39 * 可能需要呼叫 40 */41 // 如果遊標中沒有內容,表示 Provider 沒找到匹配的記錄。42 }elseif(mCursor。getCount()<1){43 44 /*45 * 在這裡插入通知使用者查詢失敗的程式碼。46 * 這不一定是出錯了,可以讓使用者錄入新記錄,也可以重新輸入查詢條件。47 */48 49 }else{50 // 在這裡插入處理查詢結果的程式碼。51 52 }
查詢的語句與以下 SQL 語句類似:
SELECT _ID, word, locale FROM words WHERE word =
這條 SQL 語句中使用的是真實的列名,而不是 Contract 類常量。
防止非法輸入
如果 Content Provider 管理的資料存放於 SQL 資料庫中,那麼在 SQL 語句中插入某些非法資訊可能會引發 SQL 注入問題。
請看下面這條查詢語句:
// 將使用者輸入內容拼接在列名之後,構造一條查詢語句。String mSelectionClause = “var = ”+ mUserInput;
這時,使用者就可以將惡意 SQL 拼接到查詢語句中。 比如,使用者可以將
mUserInput
輸入為“nothing; DROP TABLE *;”,這樣查詢語句就會成為“
var = nothing; DROP TABLE *;
”。 因為查詢語句將用作 SQL 語句,所以會導致 Provider 刪除底層 SQLite 資料庫中的所有資料表(除非 Provider 設定為捕獲 SQL 注入異常)。
為了避免這類問題,可以在查詢語句中使用
?
作為可替代引數,並用另一個數組作為實際的引數值。 這樣,使用者的輸入就與查詢直接關聯,而不會被解釋為 SQL 語句的一部分。 因為不再用作 SQL 語句,使用者輸入就無法注入惡意 SQL 了。 使用者的輸入內容不直接用於拼接 SQL 語句,查詢語句如下:
// 用可替代引數構造查詢語句String mSelectionClause = “var = ?”;
查詢引數陣列定義如下:
// 定義存放查詢引數值的陣列String[] selectionArgs ={“”};
在陣列中放入一個查詢引數值:
// 將查詢引數賦為使用者的輸入值selectionArgs[0]= mUserInput;
在構造查詢時,推薦使用這種將
?
作為形參、陣列提供實參的查詢語句,即使不是基於 SQL 資料庫的 Provider 也可以使用。
顯示查詢結果
客戶端方法
ContentResolver。query()
將返回一個
Cursor
,其中的資料列由對應查詢條件的 Projection 指定。
Cursor
物件支援對資料行和資料列的隨機讀取。透過
Cursor
的內部方法,可以遍歷結果資料行、獲取每一列的資料型別、讀取某一欄位的資料並檢查其他屬性。 某些
Cursor
物件可以在 Provider 的資料發生變化時進行自動更新,或是在
Cursor
資料變動時觸發其他監聽物件的方法。
注意:
根據建立查詢的物件性質, Provider 可以限制對資料列的訪問。 比如,聯絡人 Provider 就不允許 Sync Adapter 訪問某些資料列,也就不會在 Activity 和服務中返回這些列。
如果沒有找到符合條件的資料, Provider 就會返回一個
Cursor。getCount()
為 0 的
Cursor
物件(即空遊標)。
如果發生了內部錯誤,查詢返回的結果將視 Provider 的不同而定。 可能是返回
null
,也可能丟擲一個
Exception
。
因為
Cursor
是一個數據行的“列表”,所以一種較好的顯示方式就是透過
SimpleCursorAdapter
把它與
ListView
關聯起來。
以下程式碼將延續上面的程式碼。 建立了一個含有
Cursor
的
SimpleCursorAdapter
物件,並將其設定為一個
ListView
的資料來源介面卡(Adapter):
1 // 定義需要從 Cursor 讀取並顯示出來的資料列 2 String[] mWordListColumns = 3 { 4 UserDictionary。Words。WORD, // 對應 word 列的 Contract 類常量 5 UserDictionary。Words。LOCALE // 對應 locale 列的 Contract 類常量 6 }; 7 8 // 定義 View ID 列表,用於儲存 Cursor 返回的一行資料。 9 int[] mWordListItems ={ R。id。dictWord, R。id。locale};10 11 // 新建一個 SimpleCursorAdapter 物件12 mCursorAdapter =newSimpleCursorAdapter(13 getApplicationContext(), // 應用程式的 Context 物件14 R。layout。wordlistrow, // XML 格式的 Layout,用於 ListView 中每一行的佈局15 mCursor, // 查詢結果16 mWordListColumns, // 字串陣列,存放遊標中的列名17 mWordListItems, // 整形陣列,存放行佈局中的 View ID18 0); // 標誌位(一般用不上)19 20 // 設定 ListView 的 Adapter21 mWordList。setAdapter(mCursorAdapter);
注意:
要將
Cursor
用作
ListView
的後臺資料來源,遊標必須包含一個名為
_ID
的資料列。 因此,上述查詢從“word”表中讀取了
_ID
列,當然
ListView
並不會顯示這個欄位。 這也是大部分 Provider 中的資料表都帶有
_ID
列的原因所在。
從查詢結果中讀取資料
查詢結果不只是簡單地用於顯示,還可以用來完成其他操作。 比如,可以從使用者詞典中讀取單詞並在其他 Provider 中進行檢索。 這時就需要遍歷
Cursor
中的每行資料:
1 // 找到列名為“word”的欄位編號 2 int index = mCursor。getColumnIndex(UserDictionary。Words。WORD); 3 4 /* 5 * 僅當遊標可用時才會執行。 6 * 如果發生內部錯誤,User Dictionary Provider 將會返回 null。而其他 Provider 可能會丟擲異常。 7 */ 8 9 if(mCursor !=null){10 /*11 * 前進至下一行。 12 * 在第一次移動之前,“記錄指標”為 -1,如果這時讀取資料,將會觸發異常。13 */14 while(mCursor。moveToNext()){15 16 // 讀取值17 newWord = mCursor。getString(index);18 19 // 在這裡插入處理返回單詞的程式碼20 21 。。。22 23 // while 迴圈結束24 }25 }else{26 27 // 如果遊標為空或 Provider 丟擲異常,在這裡插入顯示錯誤的程式碼。28 }
Cursor
中有很多用於讀取不同型別資料的“get”方法。 例如,上述程式碼中用到了
getString()
。還有一個
getType()
方法用於返回欄位的型別。
本文原始碼獲取方式:私信 傳送 “底層原始碼” 即可 免費獲取