ContentProvider從入門到精通

地址: 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,也不會對上層數據使用層代碼產生影響。


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";
    }
}

從上面代碼我們可以看到,我們創建了一個
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_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。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章