Android 四大元件之" ContentProvider "

cryAllen發表於2016-09-20

前言

ContentProvider作為Android的四大元件之一,是屬於需要掌握的基礎知識,可能在我們的應用中,對於Activity和Service這兩個元件用的很常見,瞭解的也很多,但是對ContentProvider所知卻甚少,所以有必要去整理歸納下其中的內容,講講為什麼要用ContentProvider這個元件、ContentProvider是什麼、ContentProvider用法如何,讓大家對ContentProvider有個整體上的理解,方便以後在開發過程中如果忘記了可以及時回顧。

目錄

  • 為什麼要用ContentProvider
  • ContentProvider是什麼
  • ContentProvider用法
  • 小結

為什麼要用ContentProvider

我們都知道在Android中有資料持久化技術,常見的幾種方式比如:括檔案儲存、SharedPreferences 存 儲、以及資料庫儲存。但這幾個儲存方式有個共性,那就是隻能在應用內訪問儲存的資料,如果有需要共享的資料呢,就不能對外提供了,雖然SharedPreferences 儲存中提供了MODE_WORLD_READABLE 和MODE_WORLD_WRITEABLE這兩種操作模式,但這兩種模式在Android 4.2的版本已經被廢棄了。

那該如何實現跨程式的資料共享呢?此時,就引出了ContentProvider內容提供器,或許你會問,我們為什麼要實現跨程式的資料共享,很簡單,如果我們想在基礎系統上進行二次開發,想引用Android系統本身的資料,就需要一些程式的資料共享,比如,你想做基於通訊錄的二次開發,基於簡訊系統的二次開發,就需要用Android系統提供的資料,因為這些基礎資料本身被封裝到系統內,你也不太可能自己去設定吧,如果這些資料都不允許第三方的程式進行 訪問的話,恐怕很多應用的功能都要大打折扣了。

ContentProvider是什麼

內容提供器(ContentProvider)主要用於在不同的應用程式之間實現資料共享的功能, 它提供了一套完整的機制,允許一個程式訪問另一個程式中的資料,同時還能保證被訪資料 的安全性。目前,使用內容提供器是 Android實現跨程式共享資料的標準方式。

ContentProvider的用法一般有兩種,一種是使用現有的ContentProdiver來讀取和操作相應程式中的資料,另一種是建立自己的ContentProvider給我們程式的資料提供外部訪問介面。

ContentProvider用法

  • 利用現有的ContentProvider來讀取和操作
  • 自己建立ContentProvider提供資料

利用現有的ContentProvider來讀取和操作

對於每一個應用程式來說,如果想要訪問內容提供器中共享的資料,就一定要藉助 ContentResolve 類,可以通過 Context 中的 getContentResolver()方法獲取到該類的例項。 ContentResolver 中提供了一系列的方法用於對資料進行 CRUD 操作,其中 insert()方法用於 新增資料,update()方法用於更新資料,delete()方法用於刪除資料,query()方法用於查詢數 據。

有沒有似曾相識的感覺?沒錯,SQLiteDatabase中也是使用的這幾個方法來進行 CRUD 操作的,只不過它們在方法引數上稍微有一些區別。 不同於 SQLiteDatabase,ContentResolver中的增刪改查方法都是不接收表名引數的,而 是使用一個 Uri引數代替,這個引數被稱為內容 URI。。內容 URI給內容提供器中的資料建立 了唯一識別符號,它主要由兩部分組成,許可權(authority)和路徑(path)。許可權是用於對不同 的應用程式做區分的,一般為了避免衝突,都會採用程式包名的方式來進行命名。比如某個 程式的包名是 com.example.app,那麼該程式對應的許可權就可以命名為 com.example.app. provider。路徑則是用於對同一應用程式中不同的表做區分的,通常都會新增到許可權的後面。 比如某個程式的資料庫裡存在兩張表,table1和 table2,這時就可以將路徑分別命名為/table1 和/table2,然後把許可權和路徑進行組合,內容 URI就變成了 com.example.app.provider/table1 和 com.example.app.provider/table2。

內容 URI最標準的格式寫法:

content://com.example.app.provider/table1 
content://com.example.app.provider/table2 

在得到了內容 URI字串之後,我們還需要將它解析成 Uri物件才可以作為引數傳入。 解析的方法也相當簡單,程式碼如下所示:

Uri uri = Uri.parse("content://com.example.app.provider/table1") 

只需要呼叫 Uri.parse()方法,就可以將內容 URI字串解析成 Uri物件了。
現在我們就可以使用這個 Uri物件來查詢 table1表中的資料了,程式碼如下所示:

Cursor cursor = getContentResolver().query(  uri,   projection,   selection,   selectionArgs, sortOrder);

那麼我們可以看看getContentResolver()這個方法跟Sql部分的對應關係吧
兩者關係

查詢完成後返回的仍然是一個 Cursor 物件,這時我們就可以將資料從 Cursor 物件中逐 個讀取出來了。讀取的思路仍然是通過移動遊標的位置來遍歷 Cursor的所有行,然後再取出每一行中相應列的資料,程式碼如下所示:

if (cursor != null) {  
  while (cursor.moveToNext()) {   
    String column1 = cursor.getString(cursor.getColumnIndex("column1"));  
    int column2 = cursor.getInt(cursor.getColumnIndex("column2"));  
  } 
  cursor.close(); 
}

剩下的增加、修改、刪除操作就簡單多了。

我們先來看看如何向 table1表中新增一條資料,程式碼如下所示:

sql lite ContentValues values = new ContentValues(); values.put("column1", "text"); values.put("column2", 1); getContentResolver().insert(uri, values);

可以看到,仍然是將待新增的資料組裝到 ContentValues 中,然後呼叫 ContentResolver。

在table1中更新一條資料

sql lite ContentValues values = new ContentValues(); values.put("column1", ""); getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[] {"text", "1"});

在table1中刪除一條資料

sql lite getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });

整體來說,我們可以利用現有的ContentProvider來獲取資料,比如讀取聯絡人的資訊:

public class MainActivity extends Activity {
    ListView contactsView;
    ArrayAdapter<String> adapter;
    List<String> contactsList = new ArrayList<String>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        contactsView = (ListView) findViewById(R.id.contacts_view);
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactsList);
        contactsView.setAdapter(adapter);
        readContacts();
    }

    private void readContacts() {
        Cursor cursor = null;
        try {
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null, null, null, null);
            while (cursor.moveToNext()) {
                String displayName = cursor.getString(cursor
                    .getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                String number = cursor.getString(cursor
                        .getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                contactsList.add(displayName + "\n" + number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }
}

同時需要要注意的是在註冊清單中加上讀取聯絡人的許可權,表示允許應用訪問聯絡人資訊。

<uses-permission android:name="android.permission.READ_CONTACTS" />

自己建立ContentProvider提供資料

如果想要實現跨程式共享資料的功能,官方推薦的方式就是使用內容 提供器,可以通過新建一個類去繼承 ContentProvider的方式來建立一個自己的內容提供器。 ContentProvider類中有六個抽象方法,我們在使用子類繼承它的時候,需要將這六個方法全部重寫。

比如新寫一個MyProvider類,需要繼承ContentProvider。

public class MyProvider extends ContentProvider {

    @Override
    public boolean onCreate() {
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder) {
        return null;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }
}

在這六個方法中,相信大多數你都已經非常熟悉了。

  1. onCreate() 初始化內容提供器的時候呼叫。通常會在這裡完成對資料庫的建立和升級等操作, 返回 true 表示內容提供器初始化成功,返回 false 則表示失敗。注意,只有當存在 ContentResolver嘗試訪問我們程式中的資料時,內容提供器才會被初始化。
  2. query() 從內容提供器中查詢資料。使用 uri引數來確定查詢哪張表,projection引數用於確 定查詢哪些列,selection和 selectionArgs引數用於約束查詢哪些行,sortOrder引數用於 對結果進行排序,查詢的結果存放在 Cursor物件中返回。
  3. insert() 向內容提供器中新增一條資料。使用 uri 引數來確定要新增到的表,待新增的資料 儲存在 values引數中。新增完成後,返回一個用於表示這條新記錄的 URI。
  4. update() 更新內容提供器中已有的資料。使用 uri 引數來確定更新哪一張表中的資料,新數 據儲存在 values引數中,selection和 selectionArgs引數用於約束更新哪些行,受影響的 行數將作為返回值返回。
  5. delete() 從內容提供器中刪除資料。使用 uri 引數來確定刪除哪一張表中的資料,selection和 selectionArgs引數用於約束刪除哪些行,被刪除的行數將作為返回值返回。
  6. getType() 根據傳入的內容 URI來返回相應的 MIME型別。

可以看到,幾乎每一個方法都會帶有Uri這個引數,這個引數也正是呼叫 ContentResolver 的增刪改查方法時傳遞過來的。而現在我們需要對傳入的 Uri引數進行解析,從中分析出 呼叫方期望訪問的表和資料。 回顧一下,一個標準的內容 URI寫法是這樣的:

content://com.example.app.provider/table1 

這就表示呼叫方期望訪問的是 com.example.app 這個應用的 table1 表中的資料。除此之 外,我們還可以在這個內容 URI的後面加上一個 id,如下所示:

content://com.example.app.provider/table1/1

這就表示呼叫方期望訪問的是 com.example.app這個應用的 table1表中 id 為 1的資料。

內容 URI的格式主要就只有以上兩種,以路徑結尾就表示期望訪問該表中所有的資料, 以 id 結尾就表示期望訪問該表中擁有相應 id 的資料。我們可以使用萬用字元的方式來分別匹 配這兩種格式的內容 URI,規則如下。

  1. "*":表示匹配任意長度的任意字元
  2. "#" :表示匹配任意長度的數字

content://com.example.app.provider/* 表示一個能夠匹配任意表的內容 URI格式content://com.example.app.provider/table1/# 表示一個能夠匹配 table1表中任意一行資料的內容 URI格式

此時,我們需要引出一個UriMatcher這個類,這個類就可以輕鬆地實現匹配內容URI的功能。UriMatcher 中提供了一個 addURI()方法,這個方法接收三個引數,可以分別把許可權、路徑和一個自定義 程式碼傳進去。這樣,當呼叫 UriMatcher 的 match()方法時,就可以將一個 Uri 物件傳入,返回值是某個能夠匹配這個 Uri物件所對應的自定義程式碼,利用這個程式碼,我們就可以判斷出呼叫方期望訪問的是哪張表中的資料了。

    public static final int TABLE1_DIR = 0;  
    public static final int TABLE1_ITEM = 1;  
    public static final int TABLE2_DIR = 2; 
    public static final int TABLE2_ITEM = 3; 
    
    private static UriMatcher uriMatcher;  
    static {   
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 
        uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);  
        uriMatcher.addURI("com.example.app.provider ", "table1/#", TABLE1_ITEM); 
        uriMatcher.addURI("com.example.app.provider ", "table2", TABLE2_ITEM);  
        uriMatcher.addURI("com.example.app.provider ", "table2/#", TABLE2_ITEM);
    }  

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder) {
        switch (uriMatcher.match(uri)) {  
            case TABLE1_DIR:    //查詢table1表中的所有資料   
                break;   
            case TABLE1_ITEM:   //查詢table1表中的單條資料    
                break;   
            case TABLE2_DIR:    //查詢table2表中的所有資料    
                break;  
            case TABLE2_ITEM:   //查詢table2表中的單條資料    
                break;  
            default:   
                break;  
          } 
        return null;
    }

可以看到,MyProvider 中新增了四個整型常量,其中 TABLE1_DIR 表示訪問 table1 表 中的所有資料,TABLE1_ITEM 表示訪問 table1 表中的單條資料,TABLE2_DIR 表示訪問 table2 表中的所有資料,TABLE2_ITEM 表示訪問 table2 表中的單條資料。接著在靜態程式碼塊裡我們建立了 UriMatcher 的例項,並呼叫 addURI()方法,將期望匹配的內容 URI 格式傳 遞進去,注意這裡傳入的路徑引數是可以使用萬用字元的。然後當 query()方法被呼叫的時候, 就會通過 UriMatcher的 match()方法對傳入的 Uri 物件進行匹配,如果發現 UriMatcher 中某 個內容 URI格式成功匹配了該 Uri物件,則會返回相應的自定義程式碼,然後我們就可以判斷 出呼叫方期望訪問的到底是什麼資料了。

除此之外,還有一個方法你會比較陌生,即 getType()方法。它是所有的內容提供器都必 須提供的一個方法,用於獲取 Uri物件所對應的 MIME型別。一個內容 URI所對應的 MIME 字串主要由三部分組分,Android對這三個部分做了如下格式規定。

  1. 必須以 vnd開頭。
  2. 如果內容 URI 以路徑結尾,則後接 android.cursor.dir/,如果內容 URI 以 id 結尾, 則後接 android.cursor.item/。
  3. 最後接上 vnd..
    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
            case CATEGORY_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
            case CATEGORY_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
        }
        return null;
    }

到這裡,一個完整的內容提供器就建立完成了,現在任何一個應用程式都可以使用 ContentResolver來訪問我們程式中的資料。那麼前面所提到的,如何才能保證隱私資料不會 洩漏出去呢?其實多虧了內容提供器的良好機制,這個問題在不知不覺中已經被解決了。因 為所有的 CRUD 操作都一定要匹配到相應的內容 URI 格式才能進行的,而我們當然不可能 向 UriMatcher中新增隱私資料的 URI,所以這部分資料根本無法被外部程式訪問到,安全問 題也就不存在了。

下面比如我們自己建立一個內容提供器

public class DatabaseProvider extends ContentProvider {

    public static final int BOOK_DIR = 0;

    public static final int BOOK_ITEM = 1;

    public static final int CATEGORY_DIR = 2;

    public static final int CATEGORY_ITEM = 3;

    public static final String AUTHORITY = "com.example.databasetest.provider";

    private static UriMatcher uriMatcher;

    private MyDatabaseHelper dbHelper;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
    }

    @Override
    public boolean onCreate() {
        dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = null;
        switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
            break;
        case BOOK_ITEM:
            String bookId = uri.getPathSegments().get(1);
            cursor = db.query("Book", projection, "id = ?", new String[] { bookId }, null, null,sortOrder);
            break;
        case CATEGORY_DIR:
            cursor = db.query("Category", projection, selection, selectionArgs, null, null,
                    sortOrder);
            break;
        case CATEGORY_ITEM:
            String categoryId = uri.getPathSegments().get(1);
            cursor = db.query("Category", projection, "id = ?", new String[] { categoryId }, null,null, sortOrder);
            break;
        default:
            break;
        }
        return cursor;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Uri uriReturn = null;
        switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
        case BOOK_ITEM:
            long newBookId = db.insert("Book", null, values);
            uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
            break;
        case CATEGORY_DIR:
        case CATEGORY_ITEM:
            long newCategoryId = db.insert("Category", null, values);
            uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
            break;
        default:
            break;
        }
        return uriReturn;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int updatedRows = 0;
        switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            updatedRows = db.update("Book", values, selection, selectionArgs);
            break;
        case BOOK_ITEM:
            String bookId = uri.getPathSegments().get(1);
            updatedRows = db.update("Book", values, "id = ?", new String[] { bookId });
            break;
        case CATEGORY_DIR:
            updatedRows = db.update("Category", values, selection, selectionArgs);
            break;
        case CATEGORY_ITEM:
            String categoryId = uri.getPathSegments().get(1);
            updatedRows = db.update("Category", values, "id = ?", new String[] { categoryId });
            break;
        default:
            break;
        }
        return updatedRows;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int deletedRows = 0;
        switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            deletedRows = db.delete("Book", selection, selectionArgs);
            break;
        case BOOK_ITEM:
            String bookId = uri.getPathSegments().get(1);
            deletedRows = db.delete("Book", "id = ?", new String[] { bookId });
            break;
        case CATEGORY_DIR:
            deletedRows = db.delete("Category", selection, selectionArgs);
            break;
        case CATEGORY_ITEM:
            String categoryId = uri.getPathSegments().get(1);
            deletedRows = db.delete("Category", "id = ?", new String[] { categoryId });
            break;
        default:
            break;
        }
        return deletedRows;
    }

    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book";
        case BOOK_ITEM:
            return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book";
        case CATEGORY_DIR:
            return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category";
        case CATEGORY_ITEM:
            return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category";
        }
        return null;
    }
}

這裡結合了SQLiteDatabase資料庫的操作,把想把共享的資料開放出去。

不過同樣需要注意的是,需要在清單中註冊。

 <provider
    android:name="com.example.databasetest.DatabaseProvider"
    android:authorities="com.example.databasetest.provider" >
 </provider>

小結

所以總的來說,ContentProvider是四大元件之一,這也是我們需要掌握的基礎知識之一,從通過分析為何需要ContentProvider,以及ContentProvider是什麼,並最後是如何使用,對這一個過程是不是清楚很多,當然這些都是基本用法,如果有興趣的話,可以具體去看原始碼,瞭解其中實現的原理,很多時候我們要做到知其然而之所以然,這樣使用起來了,以後有什麼問題,可以迅速定位到其中原因。

閱讀擴充套件

源於對掌握的Android開發基礎點進行整理,羅列下已經總結的文章,從中可以看到技術積累的過程。
1,Android系統簡介
2,ProGuard程式碼混淆
3,講講Handler+Looper+MessageQueue關係
4,Android圖片載入庫理解
5,談談Android執行時許可權理解
6,EventBus初理解
7,Android 常見工具類
8,對於Fragment的一些理解
9,Android 四大元件之 " Activity "
10,Android 四大元件之" Service "
11,Android 四大元件之“ BroadcastReceiver "
12,Android 四大元件之" ContentProvider "
13,講講 Android 事件攔截機制
14,Android 動畫的理解
15,Android 生命週期和啟動模式
16,Android IPC 機制
17,View 的事件體系
18,View 的工作原理
19,理解 Window 和 WindowManager
20,Activity 啟動過程分析
21,Service 啟動過程分析
22,Android 效能優化
23,Android 訊息機制
24,Android Bitmap相關
25,Android 執行緒和執行緒池
26,Android 中的 Drawable 和動畫
27,RecylerView 中的裝飾者模式
28,Android 觸控事件機制
29,Android 事件機制應用
30,Cordova 框架的一些理解
31,有關 Android 外掛化思考
32,開發人員必備技能——單元測試

相關文章