解讀Android之ContentProvider(2)建立自己的Provider
本文翻譯自android官方文件,結合自己測試,整理如下。
content provider管理資料的訪問,我們可以在自己的應用程式中實現一個或多個自定義的provider(通過繼承抽象類ContentProvider),當然這些provider需要在manifest檔案中註冊。儘管content provider是用來為其它程式來訪問資料的,但是在自己程式中的activities顯然可以對這些資料進行處理。
建立provider之前注意事項
確定是否需要提供content provider。若有以下一種或多種需求的話需要建立content provider:
- 想提供給其他程式複雜的資料或檔案;
- 想允許使用者將複雜的資料從我們的程式複製到另外的程式中;
- 想使用查詢框架提供自定義查詢建議。
若在程式內部使用SQLite資料庫,則不需要provider。
接下來,通過下列步驟建立provider(先簡單的總結,後續詳細介紹):
為資料設計儲存方式,content provider提供兩種方式:
- 檔案資料
資料儲存在檔案中,例如圖片,視訊,音訊等。這些檔案儲存在私有的空間,provider可以提供外部程式訪問。 - 結構化的資料
這樣的資料通常儲存在資料庫,陣列,或者相似的結構中,當然可以把這些資料以相容的方式儲存在表中。表中的一行表示一個實體(記錄),一列表示實體相關屬性的取值。通常這些資料儲存在SQLite資料庫中,當然也可以採用其他永久儲存資料的方式。
- 檔案資料
需要繼承抽象類ContentProvider,並且覆蓋必要的方法。這個類是我們的資料和其他程式互動的介面。
- 定義provider的許可權(authority),內容URIs和列名。若程式想要處理intents,則還必須定義intent的action,extra data和flags。同樣需要定義其它程式想要訪問該provider必須請求的許可(permission)。通常可以考慮把這些值定義為常量並定義在另一個類中。
- 新增額外的資訊。
下面詳細講解以上步驟。
設計資料儲存
在提供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/table1
和com.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訪問資料。
query()
檢索資料,返回Cursor物件。insert()
插入一行資料,返回新插入行的URI。update()
更新存在的某行,返回更新的行號。delete()
刪除存在的某行,返回刪除的行號。getType()
返回對應URI的MIME型別。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_PERMISSION
和FLAG_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。