這節課是 Android 開發(入門)課程 的第四部分《數據與數據庫》的第三節課,導師依然是 Jessica Lin 和 Katherine Kuan。這節課拋棄上節課直接在 Activity 中操做數據庫的作法 ,引入 Content Providers 做爲數據庫和 Activity (UI) 之間的抽象層,使 Pets App 更符合 Android 框架的設計規範,保持以一致的方式管理對結構化數據集的訪問 (Keep a consistent way to manage access to a structured set of data)。html
關鍵詞:Content Providers、Content Resolver、Content URI & URI Matcher、Content Authority、Data Validation、MIME Typejava
Content Providers 主要爲應用帶來三個方面的好處:node
首先,Content Providers 在 Activity (UI) 與數據庫之間添加一個抽象層,將數據庫內部抽象化,隱藏數據存儲的詳情;也就是說,對於 UI 而言,UI 代碼直接與 Content Providers 交互,不關心數據是存儲在數據庫中,仍是文本文件中。這樣一來,Content Providers 即便改變數據的存儲方式,UI 代碼也能夠保持不變。另外,Content Providers 一般也承擔着數據驗證 (Data Validation) 的重要角色,確保輸入數據庫的數據是有效的。android
其次,Content Providers 可以與其它框架類配合使用,例如 Cursor Loader 藉助 Content Providers 實現數據變化時自動更新,使列表內容保持最新狀態,這是下節課的主要內容;還有,桌面小部件 (App Widgets) 以及實現數據上傳雲端和提供搜索建議的 Sync Adapters 也須要利用 Content Providers。上面這些例子也證實了 Android 框架傾向於應用經過 Content Providers 來規範對結構化數據集的訪問。數據庫
最後,在使用 Content Providers 以前,應用的數據是封閉的 (siloed),經過 Content Providers 能夠將應用數據分享給得到訪問權限的應用。這是一種安全的管理應用數據訪問的方式,其它應用根據應用規定的 Contract(主要是 Content URI 與 CRUD 操做對應的方法)與 Content Providers 交互獲取數據;不過其實應用訪問自身的數據的流程也相似,這就帶出了 Content Providers 工做流的話題。安全
首先,在 Activity (UI) 中調用 Content Resolver 的 CRUD 操做對應的方法,並傳遞一個 URI。而後 ContentResolver 對象可以把 Content Providers 做爲客戶端進行交互,也就是說 Content Resolver 可以根據 URI 選擇哪個 Content Providers 客戶端傳遞 CRUD 操做指令及其數據,最終由 Content Providers 實現數據訪問。所以,UI 其實是直接與 Content Resolver 交互的,選中的 Content Providers 完成數據訪問後返回數據給 Content Resolver 最終再傳遞給 UI。例如在 Pets App 中,CatalogActivity 經過調用 ContentResolver 的 query
method 獲取一個 Cursor 對象。bash
In CatalogActivity.java網絡
Cursor cursor = getContentResolver().query(
PetEntry.CONTENT_URI, // The content URI of the words table
projection, // The columns to return for each row
null, // Selection criteria
null, // Selection criteria
null); // The sort order for the returned rows
複製代碼
Tips:
Content Resolver 與 Content Provider 交互的一個重要特性是,UI 調用 ContentResolver 對象的 CRUD 操做對應的方法時,如 query
method,Content Resolver 會調用選中的 Content Providers 中相同名稱的方法 (identically-named methods),即相同的 query
method,以使 Content Providers 完成數據訪問。所以,在實現 Content Providers 這個抽象類的時候,必須 override 四個 CRUD 操做對應的方法,名稱分別爲 query
、update
、insert
、delete
。app
在 Content Providers 的整個端到端的工做流中,一個關鍵的參數是 UI 發出的 URI (Uniform Resource Identifier),指統一資源標識符,以前在《課程 4: 偏好》中提到。對於 Content Providers 而言,URI 指定了須要進行 CRUD 操做的數據,它能夠是一行、多行、整個數據庫,或者一個文本文件、圖片文件等媒體文件。所以,在這裏 URI 被稱爲 Content URI,格式以下:框架
<scheme>://<content authority>/<type of data>/<id>
複製代碼
Scheme: 固定爲 content://
表示該 URI 爲 Content URI。
Content Authority: 內容主機名,它是 Content URI 最重要的部分,它指定了所需的 Content Providers 客戶端。Content Authority 是由 AndroidManifest 中 provider 的 android:authorities
屬性決定的,一般設置爲應用獨一無二的包名,例如 Pets App 的 Content Authority 就設置爲 com.example.android.pets
。若是是應用訪問自身數據的 Content URI,那麼其 Content Authority 就要和 AndroidManifest 中的保持一致。
Note:
Android Developers 文檔推薦 Content Authority 在包名以外添加 provider 的字樣,以表示與包名有所區別。例如包名爲 com.example.<appname>
時,Content Authority 能夠是 com.example.<appname>.provider
,不過課程中沒有這麼作。
Type of Data: 數據類型,它指定了須要進行操做的數據。常見的模式是,數據類型爲表名,表示須要訪問該表格的數據;若是在此以後 Content URI 沒有 ID 後綴,那麼就表示訪問整個表格的數據,這一般在 Create/Insert 新數據或者 Delete 整個表格的應用場景使用。例如在 Pets App 中表示須要訪問整個 pets 表格的 Content URI 爲:
content://com.example.android.pets/pets
複製代碼
Note:
當所需的數據不是數據庫,而是文件時,這個部分就是文件的目錄路徑,它多是多層結構。
ID: 可選,指定表格中某一行數據進行操做,一般用於 Update 或 Delete 某一行數據的應用場景,例如在 Pets App 中表示須要訪問 pets 表格第 5 行的 Content URI 爲:(假設表格行數從 1 開始)
content://com.example.android.pets/pets/5
複製代碼
綜上所述,將 Content URI 寫進 Pets App,首先在 AndroidManifest 中設置 Content Provider 的內容主機名,其中第一行的屬性指定了 Content Provider 的名稱及其路徑,.data.PetProvider
表示 PetProvider 類放在 data 目錄下;第三行的屬性設置應用數據是否經過 Content Provider 對外開放,設爲 false
表示應用不對外分享數據。
In AndroidManifest.xml
<provider
android:name=".data.PetProvider"
android:authorities="com.example.android.pets"
android:exported="false" />
複製代碼
而後在 PetContract.java 中設置 Content URI 的各個常量,其中使用了 Uri 類的 parse
method 將字符串轉換爲一個 Uri 對象,以及 withAppendedPath
method 來構建新的 Uri 對象。
In PetContract.java
public static final String CONTENT_AUTHORITY = "com.example.android.pets";
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
public static final String PATH_PETS = "pets";
public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, PATH_PETS);
複製代碼
設計好 UI 端的 Content URI 以後,Content Providers 將經過 URI Matcher 接收並解析 URI,以肯定如何處理訪問數據的請求。例如在 Pets App 中,存在兩種類型的 Content URI,一種表示對整個 pets 表格進行操做,另外一種表示對 pets 表格的某一行進行操做,URI Matcher 須要判斷 UI 發出的 Content URI 是哪種類型,針對不一樣類型的 Content URI,Content Providers 須要進行不一樣的處理方法。
所以,URI Matcher 應該根據全部可能的 Content URI 類型構建出一個匹配模型。首先,爲全部可能的 Content URI 定義惟一的代碼,例如在 Pets App 中,爲兩種類型的 Content URI 分別定義兩個代碼。代碼數值可任意指定,可是要保證每一個代碼的惟一性。
URI pattern | Code | Constant Name |
---|---|---|
content://com.example.android.pets/pets | 100 | PETS |
content://com.example.android.pets./pets/# | 101 | PET_ID |
其中,對於第二種類型的 Content URI,使用了通用匹配符 #
表示任意長度數字的字符串;另外一個經常使用的通配符是 *
表示任意長度的字符串,例如 Contacts App 中的一個 Content URI 用於經過姓名查找聯繫人,所以 URI 會以未知長度的字符串結束。
content://com.android.contacts/lookup/*
複製代碼
在爲全部可能的 Content URI 定義惟一的代碼以後,新建一個 UriMatcher 對象,並經過 addURI
method 添加匹配規則,完整代碼以下。
In PetProvider.java
private static final int PETS = 100;
private static final int PET_ID = 101;
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(PetContract.CONTENT_AUTHORITY, PetContract.PATH_PETS, PETS);
sUriMatcher.addURI(PetContract.CONTENT_AUTHORITY, PetContract.PATH_PETS + "/#", PET_ID);
}
複製代碼
addURI
method 傳入每一個類型的 Content URI 及其對應的惟一代碼,添加匹配規則。addURI
method 放入 static 代碼塊中,確保當這個類內的任何方法被調用時,static 代碼塊內的代碼會首先運行。綜上所述,URI Matcher 的做用是,根據 Content URI 類型代碼匹配 UI 傳來的 Content URI,確保 Content Providers 僅處理正確的 Content URI 傳遞的數據訪問請求。
1. Query
例如在 Pets App 中,PetProvider 的 query
method 經過 URI Matcher 的匹配結果對數據庫進行不一樣的操做。
In PetProvider.java
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteDatabase database = mDbHelper.getReadableDatabase();
Cursor cursor;
int match = sUriMatcher.match(uri);
switch (match) {
case PETS:
cursor = database.query(PetEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
break;
case PET_ID:
selection = PetEntry._ID + "=?";
selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
cursor = database.query(PetEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Cannot query unknown URI " + uri);
}
return cursor;
}
複製代碼
onCreate
method 中新建,而且定義爲全局變量,因此在這裏首先經過 mDbHelper 獲取一個 SQLiteDatabase 對象。match
method 對傳入的 Content URI 進行匹配,得到匹配代碼後經過 switch/case 語句對數據庫進行不一樣的操做。query
method,返回一個 Cursor 對象。parseId
method 解析出 Content URI 的 ID,並調用 String.valueOf
method 將 int 轉換爲 String。最後一樣是將參數傳入 SQLiteDatabase 對象的 query
method,返回一個 Cursor 對象。2. Insert
相似地,在 Pets App 中,PetProvider 的 insert
method 經過 URI Matcher 的匹配,僅對請求整個表格的 Content URI 有效。
In PetProvider.java
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
final int match = sUriMatcher.match(uri);
switch (match) {
case PETS:
return insertPet(uri, contentValues);
default:
throw new IllegalArgumentException("Insertion is not supported for " + uri);
}
}
複製代碼
當傳入的 Content URI 匹配代碼爲 PETS 表明的 100 時,調用 insertPet
method 對數據庫進行插入新行操做,輸入參數爲 Content URI 和 ContentValues 對象。不然,拋出一個異常。
In PetProvider.java
private Uri insertPet(Uri uri, ContentValues values) {
String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
if (name == null) {
throw new IllegalArgumentException("Pet requires a name");
}
Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);
if (gender == null || !PetEntry.isValidGender(gender)) {
throw new IllegalArgumentException("Pet requires valid gender");
}
Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);
if (weight != null && weight < 0) {
throw new IllegalArgumentException("Pet requires valid weight");
}
SQLiteDatabase database = mDbHelper.getWritableDatabase();
long id = database.insert(PetEntry.TABLE_NAME, null, values);
if (id == -1) {
Log.e(LOG_TAG, "Failed to insert row for " + uri);
return null;
}
return ContentUris.withAppendedId(uri, id);
}
複製代碼
getAsString
獲取傳入的 ContentValues 對象鍵爲 COLUMN_PET_NAME
的值並存爲字符串,若是該字符串爲 null
,那麼就拋出一個異常。insert
method 的返回值爲帶新插入行 ID 的一個 Content Uri 對象。所以,這裏須要經過調用 ContentUris 的 withAppendedId
method 構建一個帶 ID 後綴的 Uri 對象,其中傳入的 ID 參數是 SQLiteDatabase 的 insert
method 的返回值。3. Update
相似地,在 Pets App 中,PetProvider 的 update
method 經過 URI Matcher 的匹配,須要對兩種 Content URI 都有效。
In PetProvider.java
@Override
public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
final int match = sUriMatcher.match(uri);
switch (match) {
case PETS:
return updatePet(uri, contentValues, selection, selectionArgs);
case PET_ID:
selection = PetEntry._ID + "=?";
selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
return updatePet(uri, contentValues, selection, selectionArgs);
default:
throw new IllegalArgumentException("Update is not supported for " + uri);
}
}
複製代碼
updatePet
進行處理。query
method 相同。最後一樣是將參數傳入輔助方法 updatePet
進行處理。In PetProvider.java
private int updatePet(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
if (values.containsKey(PetEntry.COLUMN_PET_NAME)) {
String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
if (name == null) {
throw new IllegalArgumentException("Pet requires a name");
}
}
if (values.containsKey(PetEntry.COLUMN_PET_GENDER)) {
Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);
if (gender == null || !PetEntry.isValidGender(gender)) {
throw new IllegalArgumentException("Pet requires valid gender");
}
}
if (values.containsKey(PetEntry.COLUMN_PET_WEIGHT)) {
Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);
if (weight != null && weight < 0) {
throw new IllegalArgumentException("Pet requires valid weight");
}
}
if (values.size() == 0) {
return 0;
}
SQLiteDatabase database = mDbHelper.getWritableDatabase();
return database.update(PetEntry.TABLE_NAME, values, selection, selectionArgs);
}
複製代碼
insert
method 相似,這裏的重點也是在數據驗證上。但不一樣的是,更新數據時 ContentValues 對象中某些鍵/值對可能不存在,所以首先須要經過 containKey
method 判斷,僅對存在的鍵/值對進行數據驗證。update
method 的返回值輸出;若是 ContentValues 對象爲空,返回值 0,表示沒有行被更新。4. Delete
相似地,在 Pets App 中,PetProvider 的 delete
method 經過 URI Matcher 的匹配,須要對兩種 Content URI 都有效。
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase database = mDbHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
switch (match) {
case PETS:
return database.delete(PetEntry.TABLE_NAME, selection, selectionArgs);
case PET_ID:
selection = PetEntry._ID + "=?";
selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
return database.delete(PetEntry.TABLE_NAME, selection, selectionArgs);
default:
throw new IllegalArgumentException("Deletion is not supported for " + uri);
}
}
複製代碼
delete
method,返回值爲受影響的行數。delete
method,返回值爲受影響的行數。MIME (Multipurpose Internet Mail Extensions) 類型,也稱爲內容類型 (Content Type)、媒體類型 (Media Type)。它是網絡傳輸內容的一種標識符,由文件格式及其內容決定。也就是說,一個 MIME 類型至少包括兩個部分:一個類型 (Type) 和一個子類型 (Subtype)。此外,它還可能包括一個或多個可選參數 (Optional Parameter)。例如,一個 HTML 文件的互聯網媒體類型多是
text/html; charset = UTF-8
複製代碼
在這個例子中,文件類型爲 text
,子類型爲 html
,而 charset
是一個可選參數,其值爲 UTF-8
。
對於 Android 應用而言,MIME 類型遵循特定的格式 (Vendor tree),即以 "vnd.android.cursor…" 開頭,後接 Content Authority 以及數據路徑。其中,開頭的基礎類型 (Base Type) 根據目錄 (directory, abbr. dir) 與子項 (item) 分爲兩種:
上面兩種基礎類型已經在 ContentResolver 類中分別定義爲常量,可直接調用。所以在 Pets App 中,爲數據庫 pets 自定義兩個 MIME 類型,代碼以下:
In PetContract.java
public static final String CONTENT_LIST_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PETS;
public static final String CONTENT_ITEM_TYPE =
ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PETS;
複製代碼
MIME 類型與文件拓展名相對應,所以計算機系統一般經過拓展名來肯定一個文件的媒體類型並決定與其相關聯的軟件。在 Android 中,MIME 類型一般在發送 Intent 請求時與 URI 配合,幫助系統肯定設備上最適合處理請求的應用組件。例如,一個可以顯示圖片的 Activity 不必定可以播放音頻,可是二者的 URI 相似,經過 MIME 類型就能夠明確其中一種。
針對 Content URI 的狀況,系統就會檢查相應的 Content Providers,經過其 getType()
method 獲取 MIME 類型。這是由於,全部 Content Providers 都會經過 MIME 類型來定義它所處理的數據類型,這是一種標準化的作法。這種作法既保證了代碼之間的標準化交互,也使不一樣數據類型之間不易混淆。例如一個 RailwayProvider 可能處理 trains、stations、tickets 等不一樣類型的數據,而經過獨一無二的 MIME 類型能將他們明確分別。
在 Pets App 中,Content Providers 在 getType()
method 返回對應的 MIME 類型。
In PetProvider.java
@Override
public String getType(Uri uri) {
final int match = sUriMatcher.match(uri);
switch (match) {
case PETS:
return PetEntry.CONTENT_LIST_TYPE;
case PET_ID:
return PetEntry.CONTENT_ITEM_TYPE;
default:
throw new IllegalStateException("Unknown URI " + uri + " with match " + match);
}
}
複製代碼