Android ContentProvider組件全面介紹
前言
ContentProvider雖然與Activity、Service、BroadcastReceiver齊名為Android四大組件。但如果你不是特別開發一款與其他APP有數據交互的應用,它的使用頻率遠沒有另外三者高。甚至有些需要使用得地方,有些開發者因為對ContentProvider整體作用和使用方法一知半解,所以選擇去找相關代碼復制粘貼,稍作改動,而無法自己獨立完成ContentProvider功能的開發。此篇希望能全面介紹下ContentProvider,從ContentProvider在框架中所充當的角色,到ContentResolver的使用,到URI的概念,再到數據共享的方法和權限管理,一步步的讓大家對ContentProvider有個全面的認識。
ContentProvider的角色
ContentProvider一般為存儲和獲取數據提供統一的接口,可以在不同的應用程序之間共享數據。
之所以使用ContentProvider,主要有以下幾個理由:
1,ContentProvider提供了對底層數據存儲方式的抽象。比如下圖中,底層使用了SQLite數據庫,在用了ContentProvider封裝后,即使你把數據庫換成MongoDB,也不會對上層數據使用層代碼產生影響。
ContentProvider角色
2,Android框架中的一些類需要ContentProvider類型數據。如果你想讓你的數據可以使用在如SyncAdapter, Loader, CursorAdapter等類上,那么你就需要為你的數據做一層ContentProvider封裝。
3,第三個原因也是最主要的原因,是ContentProvider為應用間的數據交互提供了一個安全的環境。它準許你把自己的應用數據根據需求開放給其他應用進行增、刪、改、查,而不用擔心直接開放數據庫權限而帶來的安全問題。
我們知道了ContentProvider是對數據層的封裝后,那么大家可能會問我們要如何對ContentProvider進行增,刪,改,查的操作呢?下面我們來介紹一個新的類ContentResolver,我們可以通過它,來對不同的ContentProvider進行操作。
ContentResolver
有些人可能會疑惑,為什么我們不直接訪問Provider,而是又在上面加了一層ContentResolver來進行對其的操作,這樣豈不是更復雜了嗎?其實不然,大家要知道一臺手機中可不是只有一個Provider內容,它可能安裝了很多含有Provider的應用,比如聯系人應用,日歷應用,字典應用等等。有如此多的Provider,如果你開發一款應用要使用其中多個,如果讓你去了解每個ContentProvider的不同實現,豈不是要頭都大了。所以Android為我們提供了ContentResolver來統一管理與不同ContentProvider間的操作。
ContentResolver角色
在Context.java的源碼中有一段
/** Return a ContentResolver instance for your application's package. */
public abstract ContentResolver getContentResolver();
所以我們可以通過在所有繼承Context的類中通過調用getContentResolver()
來獲得ContentResolver
。
可能又有童鞋會問,那ContentResolver是如何來區別不同的ContentProvider的呢?這就涉及到URI(Uniform Resource Identifier)問題,對URI是什么還不明白的童鞋請自行Google。
ContentProvider中的URI
ContentProvider中的URI有固定格式,如下圖:
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";
}
}</code></pre>
從上面代碼我們可以看到,我們創建了一個
content://me.pengtao.contentprovidertest/test
的uri,并且開了一個靜態方法,用以在有新數據產生時根據id生成新的uri。下面介紹下如何把此uri映射到數據庫表中。
實作
首先我們創建一個自己的TestProvider
繼承ContentProvider
。默認該Provider需要實現如下六個方法,onCreate()
, query(Uri, String[], String, String[], String)
,insert(Uri, ContentValues)
, update(Uri, ContentValues, String, String[])
, delete(Uri, String, String[])
, getType(Uri)
,方法的具體介紹可以參考
http://developer.android.com/reference/android/content/ContentProvider.html
下面我們以實現insert和query方法為例
private final static int TEST = 100;
static UriMatcher buildUriMatcher() {
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
final String authority = TestContract.CONTENT_AUTHORITY;
matcher.addURI(authority, TestContract.PATH_TEST, TEST);
return matcher;
}
@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
Cursor cursor = null;
switch ( buildUriMatcher().match(uri)) {
case TEST:
cursor = db.query(TestContract.TestEntry.TABLE_NAME, projection, selection, selectionArgs, sortOrder, null, null);
break;
}
return cursor;
}
@Nullable
@Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
Uri returnUri;
long _id;
switch ( buildUriMatcher().match(uri)) {
case TEST:
_id = db.insert(TestContract.TestEntry.TABLE_NAME, null, values);
if ( _id > 0 )
returnUri = TestContract.TestEntry.buildUri(_id);
else
throw new android.database.SQLException("Failed to insert row into " + uri);
break;
default:
throw new android.database.SQLException("Unknown uri: " + uri);
}
return returnUri;
}</code></pre>
此例中我們可以看到,我們根據path的不同,來區別對不同的數據庫表進行操作,從而完成uri與具體數據庫間的映射關系。
因為ContentProvider作為四大組件之一,所以還需要在AndroidManifest.xml中注冊一下。
<provider
android:authorities="me.pengtao.contentprovidertest"
android:name=".provider.TestProvider" />
然后你就可以使用getContentResolver()
方法來對該ContentProvider進行操作了,ContentResolver對應ContentProvider也有insert,query,delete等方法,詳情請參考:
http://developer.android.com/reference/android/content/ContentResolver.html
此處因為我們只實現了ContentProvider的query和insert的方法,所以我們可以進行插入和查詢處理。如下我們可以在某個Activity中進行如下操作,先插入一個數據peng
,然后再從從表中讀取第一行數據中的第二個字段的值。
ContentValues contentValues = new ContentValues();
contentValues.put(TestContract.TestEntry.COLUMN_NAME, "peng");
contentValues.put(TestContract.TestEntry._ID, System.currentTimeMillis());
getContentResolver().insert(TestContract.TestEntry.CONTENT_URI, contentValues);
Cursor cursor = getContentResolver().query(TestContract.TestEntry.CONTENT_URI, null, null, null, null);
try {
Log.e("ContentProviderTest", "total data number = " + cursor.getCount());
cursor.moveToFirst();
Log.e("ContentProviderTest", "total data number = " + cursor.getString(1));
} finally {
cursor.close();
}</code></pre>
數據共享
以上例子中創建的ContentProvider只能在本應用內訪問,那如何讓其他應用也可以訪問此應用中的數據呢,一種方法是向此應用設置一個android:sharedUserId
,然后需要訪問此數據的應用也設置同一個sharedUserId
,具有同樣的sharedUserId的應用間可以共享數據。
但此種方法不夠安全,也無法做到對不同數據進行不同讀寫權限的管理,下面我們就來詳細介紹下ContentProvider中的數據共享規則。
首先我們先介紹下,共享數據所涉及到的幾個重要標簽:
android:exported
設置此provider是否可以被其他應用使用。
android:readPermission
該provider的讀權限的標識
android:writePermission
該provider的寫權限標識
android:permission
provider讀寫權限標識
android:grantUriPermissions
臨時權限標識,true時,意味著該provider下所有數據均可被臨時使用;false時,則反之,但可以通過設置<grant-uri-permission>
標簽來指定哪些路徑可以被臨時使用。這么說可能還是不容易理解,我們舉個例子,比如你開發了一個郵箱應用,其中含有附件需要第三方應用打開,但第三方應用又沒有向你申請該附件的讀權限,但如果你設置了此標簽,則可以在start第三方應用時,傳入FLAG_GRANT_READ_URI_PERMISSION
或FLAG_GRANT_WRITE_URI_PERMISSION
來讓第三方應用臨時具有讀寫該數據的權限。
知道了這些標簽用法后,讓我們改寫下AndroidManifest.xml,讓ContentProvider可以被其他應用查詢。
聲明一個permission
<permission android:name="me.pengtao.READ" android:protectionLevel="normal"/>
然后改變provider標簽為
<provider
android:authorities="me.pengtao.contentprovidertest"
android:name=".provider.TestProvider"
android:readPermission="me.pengtao.READ"
android:exported="true">
</provider>
則在其他應用中可以使用以下權限來對TestProvider進行訪問。
<uses-permission android:name="me.pengtao.READ"/>
有人可能又想問,如果我的provider里面包含了不同的數據表,我希望對不同的數據表有不同的權限操作,要如何做呢?Android為這種場景提供了provider的子標簽<path-permission>
,path-permission包括了以下幾個標簽。
<path-permission android:path="string"
android:pathPrefix="string"
android:pathPattern="string"
android:permission="string"
android:readPermission="string"
android:writePermission="string" />
可以對不同path設置不同的權限規則,具體如何設定我這里就不做詳細介紹了,可以參考
http://developer.android.com/guide/topics/manifest/path-permission-element.html
相關代碼
ContentProviderTest
https://github.com/CPPAlien/ContentProviderTest
ContentResolverTest
https://github.com/CPPAlien/ContentResolverTest
注:ContentResolverTest是讀取ContentProviderTest中的數據來顯示,所以需要先安裝ContentProviderTest。
文/CPPAlien(簡書)