ContentProvider
ContentProvider,顧名思義,內容提供者,爲存儲和獲取數據提供統一的接口,可以在不同的應用程序之間共享數據。
ContentProvider爲應用間的數據交互提供了一個安全的環境。它准許你把自己的應用數據根據需求開放給其他應用進行增、刪、改、查,而不用擔心直接開放數據庫權限而帶來的安全問題。
ContentProvider提供了對底層數據存儲方式的抽象,如下圖中:
底層使用了SQLite數據庫,在用了ContentProvider封裝後,即使你把數據庫換成MongoDB,也不會對上層數據使用層代碼產生影響。
ContentResolver
ContentProvider對數據層進行了封裝,那該如何使用ContentProvider進行增,刪,改,查的操作呢?
在一個應用中直接調用另一個應用的ContentProvider,顯然是不可能的。Android中跨進程,一定需要通過Binder機制。
其實系統就提供給了我們一個叫ContentResolver的東西。ContentResolver,顧名思義,內容解析者。
我們可以在Context的實現類ContextImpl中獲得ApplicationContentResolver的實例,ApplicationContentResolver繼承自ContentResolver(ContentResolver是個抽象類):
private final ApplicationContentResolver mContentResolver;
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
private static final class ApplicationContentResolver extends ContentResolver {
……
}
之後ContentResolver通過層層調用,與系統服務進行交互,最後成功調用ContentProvider。情況可以分爲三種:
Provider進程不存在:當Provider進程不存在時,先創建進程併發布對應的Provider。
Provider未發佈:請求Provider時,Provider進程存在但Provider的記錄對象爲空,則要去發佈對應的Provider。
Provider已發佈:直接可以調用了。
關於源碼的解析,請移步大神Gityuan寫的理解ContentProvider原理。
ContentResolver幫助我們統一管理與不同ContentProvider間的操作,如圖:
ContentProvider中的URI
那ContentResolver是如何找到自己需要的ContentProvider呢?我們通過URI(Uniform Resource Identifier),即一個標識、定位任何資源的字符串。
ContentProvider中的URI有固定格式,如下圖:
- Authority:授權信息,用以區別不同的ContentProvider
- Path:表名,用以區分ContentProvider中不同的數據表
- Id:Id號,用以區別表中的不同數據
URI組裝代碼示例:
public class TestContract {
protected static final String CONTENT_AUTHORITY = "me.pengtao.contentprovidertest";
protected static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
protected static final String PATH_TEST = "test";
public static final class TestEntry implements BaseColumns {
public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon().appendPath(PATH_TEST).build();
protected static Uri buildUri(long id) {
return ContentUris.withAppendedId(CONTENT_URI, id);
}
protected static final String TABLE_NAME = "test";
public static final String COLUMN_NAME = "name";
}
}
從上面代碼我們可以看到,我們創建了一個content://me.pengtao.contentprovidertest/test
的uri,並且開了一個靜態方法,用以在有新數據產生時根據id生成新的uri。
創建ContentProvider
必須方法
抽象類ContentProvider定義了六個抽象方法,您必須將這些方法作爲自己具體子類的一部分加以實現。 所有這些方法(onCreate() 除外)都由一個嘗試訪問您的內容提供程序的客戶端應用調用:
query():
從您的提供程序檢索數據。使用參數選擇要查詢的表、要返回的行和列以及結果的排序順序。 將數據作爲 Cursor 對象返回。
insert():
在您的提供程序中插入一個新行。使用參數選擇目標表並獲取要使用的列值。 返回新插入行的內容 URI。
update():
更新您提供程序中的現有行。使用參數選擇要更新的表和行,並獲取更新後的列值。 返回已更新的行數。
delete():
從您的提供程序中刪除行。使用參數選擇要刪除的表和行。 返回已刪除的行數。
getType():
返回內容 URI 對應的 MIME 類型。實現內容提供程序 MIME 類型部分對此方法做了更詳盡的描述。
onCreate():
初始化您的提供程序。Android 系統會在創建您的提供程序後立即調用此方法。 請注意,ContentResolver 對象嘗試訪問您的提供程序時,系統纔會創建它。
注意:
所有這些方法(onCreate() 除外)都可由多個線程同時調用,因此它們必須是線程安全方法。
避免在 onCreate() 中執行長時間操作。將初始化任務推遲到實際需要時進行。
儘管您必須實現這些方法,但您的代碼只需返回要求的數據類型,無需執行任何其他操作。 例如,您可能想防止其他應用向某些表插入數據。 要實現此目的,您可以忽略 insert() 調用並返回 0。
UriMatcher
爲幫助您選擇對傳入的內容 URI 執行的操作,提供程序 API 加入了實用類 UriMatcher,它會將內容 URI“模式”映射到整型值。您可以在一個 switch 語句中使用這些整型值,爲匹配特定模式的一個或多個內容 URI 選擇所需操作。
例1:
content://com.example.app.provider/table1
一個名爲 table1 的表
例2:
content://com.example.app.provider/table3/1
對應由 table3 中 1 標識的行的內容 URI
例3:
以下代碼段演示了 UriMatcher 中方法的工作方式。 此代碼採用不同方式處理整個表的 URI 與單個行的 URI,它爲表使用的內容 URI 模式是 content://< authority >/< path >
,爲單個行使用的內容 URI 模式則是 content://< authority >/< path >/< id >
。
方法addURI()會將授權和路徑映射到一個整型值,方法match()會返回URI的整型值。switch語句會在查詢整個表與查詢單個記錄之間進行選擇:
public class ExampleProvider extends ContentProvider {
...
// Creates a UriMatcher object.
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
/*
* The calls to addURI() go here, for all of the content URI patterns that the provider
* should recognize. For this snippet, only the calls for table 3 are shown.
*/
/*
* Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
* in the path
*/
sUriMatcher.addURI("com.example.app.provider", "table3", 1);
/*
* Sets the code for a single row to 2. In this case, the "#" wildcard is
* used. "content://com.example.app.provider/table3/3" matches, but
* "content://com.example.app.provider/table3 doesn't.
*/
sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
}
...
// Implements ContentProvider.query()
public Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {
...
/*
* Choose the table to query and a sort order based on the code returned for the incoming
* URI. Here, too, only the statements for table 3 are shown.
*/
switch (sUriMatcher.match(uri)) {
// If the incoming URI was for all of table3
case 1:
if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
break;
// If the incoming URI was for a single row
case 2:
/*
* Because this URI was for a single row, the _ID value part is
* present. Get the last path segment from the URI; this is the _ID value.
* Then, append the value to the WHERE clause for the query
*/
selection = selection + "_ID = " uri.getLastPathSegment();
break;
default:
...
// If the URI is not recognized, you should do some error handling here.
}
// call the code to actually do the query
}
程序權限
即使底層數據爲私有數據,所有應用仍可從您的提供程序讀取數據或向其寫入數據,因爲在默認情況下,您的提供程序未設置權限。 要想改變這種情況,請使用屬性或 < provider > 元素的子元素在您的清單文件中爲您的提供程序設置權限。 您可以設置適用於整個提供程序、特定表甚至特定記錄的權限,或者設置同時適用於這三者的權限。
您可以通過清單文件中的一個或多個 < permission > 元素爲您的提供程序定義權限。要使權限對您的提供程序具有唯一性,請爲 android:name 屬性使用 Java 風格作用域。 例如,將讀取權限命名爲 com.example.app.provider.permission.READ_PROVIDER
。
更多權限設置,請見谷歌官方文檔——創建內容提供程序。
參考:
1.ContentProvider從入門到精通
2.谷歌官方文檔——創建內容提供程序
3.理解ContentProvider原理