1. 程式人生 > >解讀Android之ContentProvider(2)建立自己的Provider

解讀Android之ContentProvider(2)建立自己的Provider

本文翻譯自android官方文件,結合自己測試,整理如下。

content provider管理資料的訪問,我們可以在自己的應用程式中實現一個或多個自定義的provider(通過繼承抽象類ContentProvider),當然這些provider需要在manifest檔案中註冊。儘管content provider是用來為其它程式來訪問資料的,但是在自己程式中的activities顯然可以對這些資料進行處理。

建立provider之前注意事項

確定是否需要提供content provider。若有以下一種或多種需求的話需要建立content provider:

  • 想提供給其他程式複雜的資料或檔案;
  • 想允許使用者將複雜的資料從我們的程式複製到另外的程式中;
  • 想使用查詢框架提供自定義查詢建議。

若在程式內部使用SQLite資料庫,則不需要provider。

接下來,通過下列步驟建立provider(先簡單的總結,後續詳細介紹):

  1. 為資料設計儲存方式,content provider提供兩種方式:

    • 檔案資料
      資料儲存在檔案中,例如圖片,視訊,音訊等。這些檔案儲存在私有的空間,provider可以提供外部程式訪問。
    • 結構化的資料
      這樣的資料通常儲存在資料庫,陣列,或者相似的結構中,當然可以把這些資料以相容的方式儲存在表中。表中的一行表示一個實體(記錄),一列表示實體相關屬性的取值。通常這些資料儲存在SQLite資料庫中,當然也可以採用其他永久儲存資料的方式。
  2. 需要繼承抽象類ContentProvider,並且覆蓋必要的方法。這個類是我們的資料和其他程式互動的介面。

  3. 定義provider的許可權(authority),內容URIs和列名。若程式想要處理intents,則還必須定義intent的action,extra data和flags。同樣需要定義其它程式想要訪問該provider必須請求的許可(permission)。通常可以考慮把這些值定義為常量並定義在另一個類中。
  4. 新增額外的資訊。

下面詳細講解以上步驟。

設計資料儲存

在提供provider之前,我們必須要確定我們的資料該如何儲存,當然儲存方式我們可以任意指定,然後再針對該儲存方式設計provider。

有下列幾種儲存方式:

  • 儲存在SQLite資料庫中。
  • 儲存在檔案中。
  • 儲存在網路中。

注意事項

  • 表資料通常需要主鍵,provider為每行賦值一個唯一的數值。儘管主鍵這一列可以任意名稱,但是推薦使用BaseColumns._ID,這樣的話,在ListView就能很方便的檢索。
  • 若提供點陣圖或者更大的檔案資料的話,這些資料會儲存在檔案中,以間接地方式提供。若是使用這些資料的話,我們應該通知外部應用程式它們應該使用ContentResovler類中的檔案方法來獲取這些資料,例如openInputStream(Uri uri)openOutputStream(Uri uri)等方法。
  • 儲存BLOB資料型別的話大小或結構會發生變化。我們可以使用BLOB實現一個模式獨立的表,該型別的表,我們需要定義一個主鍵,一個MIME型別列,一個或多個BLOB型別的列。在BLOB列中資料的含義是通過MIME型別列中的值表示,這種方式允許在相同的表中儲存不同的行型別。

設計內容URIs

內容URI是能夠識別provider中資料的URI,包括許可權(authority)和路徑(path)。許可權找到provider,路徑找到表或檔案。還可以有一個id,能夠表示某一行。

許可權用於區分不同程式的provider,一般為了避免衝突,都會採用包名的形式命名。例如包名為:com.example.sywyg,則該許可權就可以為:com.example.sywyg.provider。可以在AndroidManifest配置檔案中的<provider>標籤中設定<android:authority>標籤。

設計路徑path

URI是許可權加路徑的方式來查詢指定的表。路徑是區分同一程式中表或者其它形式(例如檔案)的,可以直接新增在許可權後面。例如table1和table2,則形成的URI分別為:com.example.sywyg.provider/table1com.example.sywyg.provider/table2

最後內容URI需要在許可權和路徑前加上content://表示內容URI。例如,一個標準的內容URI寫法如下:content://com.example.sywyg.provider/table1

處理URI的ID

將ID追加到URI後面的話就可以檢索到表中的指定的行,ID對應的列名為_ID。

URI模式匹配

UriMatcher類對映內容URI模式到一個integer型別數,我們可以使用該值進行模式匹配。

URI模式通過萬用字元匹配:

  • *:匹配任意長度和有效的字串;
  • #:匹配任意長度的數字字元;

假設許可權為:com.example.app.provider,識別下面的URI對應的表:

`
content://com.example.app.provider/table1:表為table1.
content://com.example.app.provider/table2/dataset1:表為dataset1.
content://com.example.app.provider/table2/dataset2:表為dataset2.
content://com.example.app.provider/table3:表為table3.

`

若帶有ID同樣可以識別:
content://com.example.app.provider/table3/1 表table3中的第1行

下面的URI模式:
content://com.example.app.provider/*
匹配provider中的任意URI

content://com.example.app.provider/table2/*將會匹配表dataset1和dataset2,但是不會匹配table1或table3。

content://com.example.app.provider/table3/#將會匹配table3中的任意行。
content://com.example.app.provider/table3/6將會匹配table3中的第6行。

總結起來,URI標準就是:content:///或content:////,前者針對表,後者針對指定行。

我們可以藉助UriMatcher類快速實現內容URI的匹配。常用程式碼如下:


public static UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(authority,path,customMatch);

addURI()能夠將字串authority和字串path解析為:content://<authority>/<path>(這裡的path中可以帶有id),然後通過match(URI)方法進行匹配字串customMatch,match(URI)返回的就是對應URI的customMatch(addURI()中的第三個引數),利用這個引數就可以響應呼叫者期望訪問的資料。

其它的類,如ContentUris,Uri和Uri.Builder。ContentUris可以方便的在uri後面新增id,Uri和Uri.builder能夠方便的解析Uri物件以及生成新的Uri物件。

繼承ContentProvider類

ContentProvider類能夠管理我們provider中的資料,外部程式通過ContentResovler物件可以呼叫對應的ContentProvider方法實現操作資料。因此,我們必須要提供相應的方法來操作資料。

覆蓋方法

我們需要實現以下方法,才能方便ContentResovler訪問資料。

  1. query()
    檢索資料,返回Cursor物件。
  2. insert()
    插入一行資料,返回新插入行的URI。
  3. update()
    更新存在的某行,返回更新的行號。
  4. delete()
    刪除存在的某行,返回刪除的行號。
  5. getType()
    返回對應URI的MIME型別。
  6. onCreate()
    初始化provider。當建立provider物件時,就會立即呼叫。注意只有ContentResovler物件試圖訪問資料時才會建立provider物件。

可以看到對於上述幾個操作資料的方法,在ContentResovler有同樣名稱的方法,並且引數也一致。

在覆蓋方法時需注意以下幾點:

  • 除了onCreate()之外的方法都要注意多執行緒安全問題。
  • 避免在onCreate()進行長時間的操作。直到需要時再初始化對應的內容。
  • 儘管我們需要實現這些方法,但是除了返回值外,我們可以不用做任何事。例如我們不希望外界刪除資料,因此我們只需要返回對應的行號,而不在方法中寫任何程式碼。

實現query()方法

該方法會返回一個Cursor物件,或者失敗的話丟擲異常。若沒有查到對應的行則應該返回一個getCount()方法為0的Cursor物件。只有在內部出現錯誤是才返回null。若使用SQLite資料庫儲存資料的話,可以直接呼叫SQLiteDatabase類的query()方法返回Cursor物件。若不使用的話,就要使用Cursor類的具體子類。

在查詢時可能會丟擲下列異常:

  • IllegalArgumentException(當接收到無效URI時)
  • NullPointerException

若訪問的是SQLite資料庫,則query()簡單實現程式碼如下:


public class ExampleProvider extends ContentProvider {
    private static final UriMatcher sUriMatcher;
    sUriMatcher.addURI("com.example.app.provider", "table3", 1);
    sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
    // 引數為ContentResovler呼叫query()方法傳遞過來的
    public Cursor query(
        Uri uri,
        String[] projection,
        String selection,
        String[] selectionArgs,
        String sortOrder) {
        switch (sUriMatcher.match(uri)) {
            // 對應table3
            case 1:
                if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
                break;
            // 對應帶有id的table3
            case 2:
                selection = selection + "_ID = " uri.getLastPathSegment();
                break;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);  
      }
        // 實際查詢SQLite資料庫語句

    }

實現insert()方法

插入方法將新的一行新增到指定的表中,資料來自於引數ContentValues 傳遞的資料,若某一列沒有指定,則提供預設值(該預設值取決於provider或資料庫本身)。

該方法會返回新行的URI。我們可以通過ContentUris的withAppendId()方法在URI後新增主鍵ID(通常是_ID)來構建該URI。直接通過parse()方法也行。

實現update()方法

和插入類似,不再介紹。

實現delete()方法

刪除指定的行。該方法不需要真的刪除某行,若我們使用同步介面卡的話,可以考慮先把要刪除的資料進行標記刪除。該同步介面卡可以在從provider中真的刪除資料之前能夠檢查出刪除的行,並把這些排除,實現假刪除。

實現getType()方法

將在下面部分詳細講解。

實現onCreate()方法

當建立provider時,系統呼叫onCreate()方法。我們應該保證在這裡初始化的內容是必須的且能夠快速執行,對於不是必須的且耗時的可以在需要時再初始化。例如資料庫建立以及資料載入可以在真正請求操作資料時再執行。若太耗時的話,provider啟動就會耗時,顯然這會影響響應請求該provider的程式。

ContentProvider的MIME型別

ContentProvider有兩個方法能返回型別:

  • getType()
    這個需要在子類中實現
  • getStreamTypes()
    若provider提供檔案訪問的話,就需要實現這個。

表的MIME型別

getType()方法返回一個MIME格式的字串,用來描述引數URI對應的 資料型別。引數Uri可以是一個匹配模式而不是必須是詳細的URI,這樣我們應該返回和匹配模式的URIs相關的MIME型別。

對於常見的型別:text,HTML,JPEG,getType()方法應該返回標準的MIME型別。

對於指定一行或多行的URIs,getType()方法應該返回android指定的MIME格式:

  • 型別部分:vnd
  • 子型別部分:
    • URI模式只有一行:android.cursor.item/
    • URI模式有多行:android.cursor.dir/
  • Provider指定部分:vnd..
    name應該是全域性唯一的,type應該是對應URI模式唯一的。通常,name應該是包名,type應該是和URI相關的表名。
    例如,provider許可權為com.example.app.provider,表名為:table1,則table1多行的MIME型別為:
    vnd.android.cursor.dir/vnd.com.example.provider.table1
    單行的MIME型別為:
    vnd.android.cursor.item/vnd.com.example.provider.table1

檔案的MIME型別

若provider提供檔案訪問的話,需要實現getStreamTypes()方法。該方法返回一個MIME型別的字串陣列,根據給定的URI。我們應該根據MIME型別過濾引數過濾MIME型別,以便返回外部程式想處理的MIME型別。

例如,provider提供圖片檔案:.jpg,.png和.gif格式。若一個程式呼叫了getStreamTypes()使用過濾引數image/*,那麼該方法就會返回:
{ "image/jpeg", "image/png", "image/gif"}
若程式只需要.jpg的話,可以使用過濾引數*\/jpeg,則方法返回:
{"image/jpeg"}

若provider不提供型別的話,方法返回null。

實現相關類

一般需要一個public final型別的相關類來定義常量:URIs,列名,MIME或者其他和provider相關的資訊。該類使provider和其它程式建立一種關係,能夠確保provider被正確地獲取。對於其他程式來說,可以通過我們提供的.jar檔案來操作這個相關類,進而實現操作provider。

實現Content Provider許可

需要注意一下幾點:

  • 預設情況下,儲存在裝置記憶體(並不是執行記憶體)資料檔案是我們程式和provider私有的。
  • SQLite資料庫是我們程式和provider私有的。
  • 預設情況,儲存在儲存卡上的資料是公有的。外部程式可以訪問,我們不能使用provider限制該資料的訪問許可。
  • 開啟或建立儲存記憶體上的一個檔案或SQLite資料庫的方法呼叫潛在地授予了其它程式讀寫這些資料的許可。若是使用儲存記憶體上的資料作為provider的資料集的話,其它程式都有讀寫的許可權,而我們在manifest設定的將不起作用。預設的獲取資料是私有的,不應該改變。

若我們想使用content provider許可權控制資料的讀取,我們需要把資料儲存在內部檔案,SQLite資料庫或伺服器中,並且確保這些檔案和資料庫是私有的。

實現許可

預設情況下,provider沒有設定許可,所有的程式都能獲取provider資料。我們可以在mainfest檔案中<provider>標籤的屬性或子標籤配置。許可可以針對整個provider或者特定的表或者特定的記錄配置。

宣告許可使用<permission>標籤,例如:
<permission android:name="com.example.app.provider.permission.READ_PROVIDER">

下面描述了provider的詳細許可設定:

  • 單個provider讀寫許可(Single read-write provider-level permission)
    該許可是控制整個provider的讀寫許可。在<provider>標籤中的android:permission屬性中設定。
  • provider級別分開的讀或寫許可權(Separate read and write provider-level permission

    <provider>標籤中的android:readPermission屬性中設定讀許可;在<provider>標籤中的android:writePermission屬性中設定寫許可。這兩個許可優於android:permission設定的許可。
  • 路徑級別的許可(Path-level permission)
    讀或寫或讀寫指定URI的許可。在<provider>標籤中的<path-permission>子標籤中設定。該級別許可權優於上面的兩個許可。
  • 臨時許可(Temporary permission)
    授予程式臨時獲取資料的許可。
    <provider>標籤中的android:grantUriPermissions屬性中設定,或者在<provider>標籤中的<grant-uri-permission>子標籤中新增一個或多個。
    若使用了臨時許可,每當從provider移除對一個URI的支援時,必須呼叫Context.revokeUriPermission(),該URI和臨時許可相關。若該屬性設定了true,則系統支援授權臨時許可,且會覆蓋其它任何許可(provider級別或路徑級別的)。
    若設定了false,就需要在<provider>標籤中的<grant-uri-permission>子標籤中新增一個或多個。每一個子標籤指定一個或多個URIs有臨時被訪問的許可。

    為了在一個程式中委託臨時訪問許可,intent必須包含FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION中的一個或兩個flags(通過setFlags()設定)。

    若沒設定android:grantUriPermissions屬性,則被認為是false。

<provider>標籤

我們知道四大元件都需要在mainfest檔案中配置,ContentProvider的實現類通過<provider>標籤配置。該標籤中還包括一些重要的屬性和子標籤:

  • android:authorities
    用於在這個系統中識別provider(先識別應用程式)。
  • android:name
    ContentProvider的實現類類名。
  • Permission
    上面已經詳細描述,主要包括:

    - `android:grantUriPermssions`;
    - `android:permission`;
    - `android:readPermission`;
    - `android:writePermission`。
    
  • 啟動和控制屬性

    • android:enabled 是否允許例項化
    • android:exported 外部是否能使用
    • android:initOrder integer值,表示在同一個程序中被初始化的順序,值越大越早被初始化
    • android:multiProcess 是否允許在多個程序中例項化
    • android:process 所在的程序
    • android:syncable
  • 資訊屬性

    • android:icon 圖示
    • android:label 名稱

總結

ContentResovler和ContentProvider之間的協作關係以查詢SQLite資料庫為例進行描述:

ContentResovler物件的query()方法中的引數URI,通過URI中的許可權authority可以找到對應的ContentProvider實現類,對該類例項化並呼叫query()方法,在query()方法中通過UriMatcher.match()方法匹配Uri,匹配成功後交給SQLite資料庫的查詢方法,並返回Cursor,然後通過ContentProvider例項返回該Cursor給呼叫者。可以看到通過許可權可以確定一個provider的,因此一個程式中可以包含多個providers。