課程 3: Content Providers 簡介

這節課是 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 的優點

Content Providers 主要爲應用帶來三個方面的好處:node

UI 把 Content Providers 看做一個暗箱操做

首先,Content Providers 在 Activity (UI) 與數據庫之間添加一個抽象層,將數據庫內部抽象化,隱藏數據存儲的詳情;也就是說,對於 UI 而言,UI 代碼直接與 Content Providers 交互,不關心數據是存儲在數據庫中,仍是文本文件中。這樣一來,Content Providers 即便改變數據的存儲方式,UI 代碼也能夠保持不變。另外,Content Providers 一般也承擔着數據驗證 (Data Validation) 的重要角色,確保輸入數據庫的數據是有效的。android

Cursor Loader 須要藉助 Content Providers 實現異步查詢

其次,Content Providers 可以與其它框架類配合使用,例如 Cursor Loader 藉助 Content Providers 實現數據變化時自動更新,使列表內容保持最新狀態,這是下節課的主要內容;還有,桌面小部件 (App Widgets) 以及實現數據上傳雲端和提供搜索建議的 Sync Adapters 也須要利用 Content Providers。上面這些例子也證實了 Android 框架傾向於應用經過 Content Providers 來規範對結構化數據集的訪問。數據庫

經過 Content Providers 向得到訪問權限的應用分享數據

最後,在使用 Content Providers 以前,應用的數據是封閉的 (siloed),經過 Content Providers 能夠將應用數據分享給得到訪問權限的應用。這是一種安全的管理應用數據訪問的方式,其它應用根據應用規定的 Contract(主要是 Content URI 與 CRUD 操做對應的方法)與 Content Providers 交互獲取數據;不過其實應用訪問自身的數據的流程也相似,這就帶出了 Content Providers 工做流的話題。安全

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 操做對應的方法,名稱分別爲 queryupdateinsertdeleteapp

Content URI

在 Content Providers 的整個端到端的工做流中,一個關鍵的參數是 UI 發出的 URI (Uniform Resource Identifier),指統一資源標識符,以前在《課程 4: 偏好》中提到。對於 Content Providers 而言,URI 指定了須要進行 CRUD 操做的數據,它能夠是一行、多行、整個數據庫,或者一個文本文件、圖片文件等媒體文件。所以,在這裏 URI 被稱爲 Content URI,格式以下:框架

<scheme>://<content authority>/<type of data>/<id>
複製代碼
  1. Scheme: 固定爲 content:// 表示該 URI 爲 Content URI。

  2. 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,不過課程中沒有這麼作。

  1. Type of Data: 數據類型,它指定了須要進行操做的數據。常見的模式是,數據類型爲表名,表示須要訪問該表格的數據;若是在此以後 Content URI 沒有 ID 後綴,那麼就表示訪問整個表格的數據,這一般在 Create/Insert 新數據或者 Delete 整個表格的應用場景使用。例如在 Pets App 中表示須要訪問整個 pets 表格的 Content URI 爲:

    content://com.example.android.pets/pets
    複製代碼

Note:
當所需的數據不是數據庫,而是文件時,這個部分就是文件的目錄路徑,它多是多層結構。

  1. 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);
複製代碼

URI Matcher

設計好 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);
}
複製代碼
  1. UriMatcher 對象的名稱以一個小寫的 s 開頭表示靜態字段,這是由 Android 的 Java 代碼規範 建議的。相似的字段命名規範還有,非公開且非靜態的字段名稱以 m 開頭;其餘字段以小寫字母開頭;公開靜態 final 字段(常量)爲所有大寫並用下劃線鏈接。
  2. 新建 UriMatcher 對象時,其構造函數須要傳入一個初始匹配代碼,一般使用 UriMatcher 類自帶的 NO_MATCH 常量。
  3. 分別經過 addURI method 傳入每一個類型的 Content URI 及其對應的惟一代碼,添加匹配規則。
  4. 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;
}
複製代碼
  1. mDbHelper 對象在 onCreate method 中新建,而且定義爲全局變量,因此在這裏首先經過 mDbHelper 獲取一個 SQLiteDatabase 對象。
  2. 經過調用 UriMatcher 的 match method 對傳入的 Content URI 進行匹配,得到匹配代碼後經過 switch/case 語句對數據庫進行不一樣的操做。
    (1)對於查詢整個表格的 Content URI,直接將輸入參數,包括 selection、selectionArgs 等參數,傳入 SQLiteDatabase 對象的 query method,返回一個 Cursor 對象。
    (2)對於查詢表格中某一行的 Content URI,須要手動設置 selection 和 selectionArgs 參數,其中須要調用 ContentUrisparseId method 解析出 Content URI 的 ID,並調用 String.valueOf method 將 int 轉換爲 String。最後一樣是將參數傳入 SQLiteDatabase 對象的 query method,返回一個 Cursor 對象。
    (3)當 Content URI 未匹配以上兩種類型的任何一種時,拋出一個異常,告知開發者異常信息。

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);
}
複製代碼
  1. 因爲這裏從 UI 引入了新數據,因此須要先進行數據驗證 (Data Validation)。具體的作法是,調用 ContentValues 對象各個對應的 getter method,獲取必要的鍵的值,對值進行驗證。若是值不可接受,就拋出異常,告知開發者異常信息。例如,經過 getAsString 獲取傳入的 ContentValues 對象鍵爲 COLUMN_PET_NAME 的值並存爲字符串,若是該字符串爲 null,那麼就拋出一個異常。
  2. Content Providers 的 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);
    }
}
複製代碼
  1. 對於查詢整個表格的 Content URI,直接將輸入參數,包括 selection、selectionArgs 參數,傳入輔助方法 updatePet 進行處理。
  2. 對於查詢表格中某一行的 Content URI,須要手動設置 selection 和 selectionArgs 參數,方法與上述 query method 相同。最後一樣是將參數傳入輔助方法 updatePet 進行處理。
  3. 當 Content URI 未匹配以上兩種類型的任何一種時,拋出一個異常,告知開發者異常信息。

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);
}
複製代碼
  1. insert method 相似,這裏的重點也是在數據驗證上。但不一樣的是,更新數據時 ContentValues 對象中某些鍵/值對可能不存在,所以首先須要經過 containKey method 判斷,僅對存在的鍵/值對進行數據驗證。
  2. Content Providers 的 insert method 的返回值爲更新行的數量值。所以,若是數據驗證所有經過,直接將 SQLiteDatabase 的 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);
    }
}
複製代碼
  1. 對於查詢整個表格的 Content URI,直接將輸入參數,包括 selection、selectionArgs 參數,傳入 SQLiteDatabase 對象的 delete method,返回值爲受影響的行數。
  2. 對於查詢表格中某一行的 Content URI,須要手動設置 selection 和 selectionArgs 參數,方法與上述 query method 相同。最後一樣是將參數傳入 SQLiteDatabase 對象的 delete method,返回值爲受影響的行數。
  3. 當 Content URI 未匹配以上兩種類型的任何一種時,拋出一個異常,告知開發者異常信息。

MIME Type

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) 分爲兩種:

  • 目錄 MIME 基礎類型:vnd.android.cursor.dir
  • 子項 MIME 基礎類型:vnd.android.cursor.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);
    }
}
複製代碼
相關文章
相關標籤/搜索