地址: http://www.jianshu.com/p/f5ec75a9cfea
前言
ContentProvider雖然與Activity、Service、BroadcastReceiver齊名爲Android四大組件。但如果你不是特別開發一款與其他APP有數據交互的應用,它的使用頻率遠沒有另外三者高。進而有些開發者可能在做過幾個成熟應用後,對ContentProvider的理解還是不夠深入,無法獨立完成ContentProvider功能的開發。網上博客對這一塊的內容介紹的也是比較複雜,不適合初學者研究學習,此篇希望能全面介紹下ContentProvider,從ContentProvider在框架中所充當的角色,到ContentResolver的使用,到URI的概念,再到數據共享的方法和權限管理,一步步的讓大家對ContentProvider有個全面的認識。
ContentProvider的角色
ContentProvider一般爲存儲和獲取數據提供統一的接口,可以在不同的應用程序之間共享數據。
之所以使用ContentProvider,主要有以下幾個理由:
1,ContentProvider提供了對底層數據存儲方式的抽象。比如下圖中,底層使用了SQLite數據庫,在用了ContentProvider封裝後,即使你把數據庫換成MongoDB,也不會對上層數據使用層代碼產生影響。
2,Android框架中的一些類需要ContentProvider類型數據。如果你想讓你的數據可以使用在如SyncAdapter, Loader, CursorAdapter等類上,那麼你就需要爲你的數據做一層ContentProvider封裝。
3,第三個原因也是最主要的原因,是ContentProvider爲應用間的數據交互提供了一個安全的環境。它准許你把自己的應用數據根據需求開放給其他應用進行增、刪、改、查,而不用擔心直接開放數據庫權限而帶來的安全問題。
我們知道了ContentProvider是對數據層的封裝後,那麼大家可能會問我們要如何對ContentProvider進行增,刪,改,查的操作呢?下面我們來介紹一個新的類ContentResolver,我們可以通過它,來對不同的ContentProvider進行操作。
ContentResolver
有些人可能會疑惑,爲什麼我們不直接訪問Provider,而是又在上面加了一層ContentResolver來進行對其的操作,這樣豈不是更復雜了嗎?其實不然,大家要知道一臺手機中可不是隻有一個Provider內容,它可能安裝了很多含有Provider的應用,比如聯繫人應用,日曆應用,字典應用等等。有如此多的Provider,如果你開發一款應用要使用其中多個,如果讓你去了解每個ContentProvider的不同實現,豈不是要頭都大了。所以Android爲我們提供了ContentResolver來統一管理與不同ContentProvider間的操作。
在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有固定格式,如下圖:
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。下面介紹下如何把此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;
}
此例中我們可以看到,我們根據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();
}
數據共享
以上例子中創建的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。