Android ContentProvider組件全面介紹

前言

ContentProvider雖然與Activity、Service、BroadcastReceiver齊名為Android四大組件。但如果你不是特別開發一款與其他APP有數據交互的應用,它的使用頻率遠沒有另外三者高。甚至有些需要使用得地方,有些開發者因為對ContentProvider整體作用和使用方法一知半解,所以選擇去找相關代碼復制粘貼,稍作改動,而無法自己獨立完成ContentProvider功能的開發。此篇希望能全面介紹下ContentProvider,從ContentProvider在框架中所充當的角色,到ContentResolver的使用,到URI的概念,再到數據共享的方法和權限管理,一步步的讓大家對ContentProvider有個全面的認識。

ContentProvider的角色

ContentProvider一般為存儲和獲取數據提供統一的接口,可以在不同的應用程序之間共享數據。

之所以使用ContentProvider,主要有以下幾個理由:
1,ContentProvider提供了對底層數據存儲方式的抽象。比如下圖中,底層使用了SQLite數據庫,在用了ContentProvider封裝后,即使你把數據庫換成MongoDB,也不會對上層數據使用層代碼產生影響。

最全的ContentProvider介紹

ContentProvider角色

2,Android框架中的一些類需要ContentProvider類型數據。如果你想讓你的數據可以使用在如SyncAdapter, Loader, CursorAdapter等類上,那么你就需要為你的數據做一層ContentProvider封裝。

3,第三個原因也是最主要的原因,是ContentProvider為應用間的數據交互提供了一個安全的環境。它準許你把自己的應用數據根據需求開放給其他應用進行增、刪、改、查,而不用擔心直接開放數據庫權限而帶來的安全問題。

我們知道了ContentProvider是對數據層的封裝后,那么大家可能會問我們要如何對ContentProvider進行增,刪,改,查的操作呢?下面我們來介紹一個新的類ContentResolver,我們可以通過它,來對不同的ContentProvider進行操作。

ContentResolver

有些人可能會疑惑,為什么我們不直接訪問Provider,而是又在上面加了一層ContentResolver來進行對其的操作,這樣豈不是更復雜了嗎?其實不然,大家要知道一臺手機中可不是只有一個Provider內容,它可能安裝了很多含有Provider的應用,比如聯系人應用,日歷應用,字典應用等等。有如此多的Provider,如果你開發一款應用要使用其中多個,如果讓你去了解每個ContentProvider的不同實現,豈不是要頭都大了。所以Android為我們提供了ContentResolver來統一管理與不同ContentProvider間的操作。

最全的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有固定格式,如下圖:

最全的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";
}

}</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_PERMISSIONFLAG_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(簡書)
 

 本文由用戶 ElijahArsen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!