Android知識體系總結2020之Android部分ContentProvider篇

初級~中級

1.內容提供者是什麼?

  內容提供者(Content Provider)主要用於在不同的應用程序之間實現數據共享的功能,它提供了一套完整的機制,允許一個程序訪問另一個程序中的數據,同時還能保證被訪數據的安全性。目前,使用內容提供者是Android實現跨程序共享數據的標準方式。
  不同於文件存儲和SharedPreferences存儲中的兩種全局可讀可寫操作模式,內容提供者可以選擇只對哪一部分數據進行共享,從而保證我們程序中的隱私數據不會泄露的風險。

2.內容提供者的使用&系統提供哪些內容提供者

2.1 內容提供者的使用

我們一般用內容提供者都是用來查詢數據的:

Cursor cursor = getContentResolver().query(final Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal)

  • uri,指定查詢某一個程序下的某一張表
  • projection,指定查詢的列名
  • selection,指定查詢條件,相當於sql語句中where後面的條件
  • selectionArgs,給selection中的佔位符提供具體的值
  • orderBy,指定查詢結果排序方式
  • cancellationSignal,取消正在進行操作的信號量

  寫過SQLite代碼的你一定對此方法非常熟悉吧!等你看完後面ContentProvider原理機制的時候,一定會恍然大悟吧!

  想要訪問內容提供者中共享的數據,就一定要藉助CotentResolver類,可以通過Context中的getContentResolver()方法獲取該類的實例。ContentResolver中提供了一系列的方法用於對數據進行CRUD(增刪改查)操作,其中insert()方法用於添加數據,update()方法用於數據的更新,delete()方法用於數據的刪除,query()方法用於數據的查詢。這好像SQLite數據庫操作有木有?
  不同於SQLiteDatabase,ContentResolver中的增刪改查方法都是不接收表名參數的,而是使用一個Uri的參數代替,這個參數被稱作內容URI。內容URI給內容提供者中的數據建立了唯一的標識符,它主要由兩部分組成:authority和path。authority是用於對不同的應用程序做區分的,一般爲了避免衝突,都會採用程序包名的方式來進行命名。比如某個程序的包名爲com.example.app,那麼該程序對應的authority就可以命名爲com.example.app.provider。path則是用於對同一應用程序中不同的表做區分的,通常都會添加到authority的後面。比如某個程序的數據庫裏存在兩張表:table1和table2,這時就可以將path分別命名爲/table1和/table2,然後把authority和path進行組合,內容的URI就變成了com.example.app.provider/table1和com.example.app.provider/table2。不過目前還是很難辨認出這兩個字符串就是兩個內容URI,我們還需要在字符串的頭部加上協議聲明。因此,內容URI最標準的格式寫法如下:

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

  在得到內容URI字符串之後,我們還需要將它解析成Uri對象纔可以作爲參數傳入。解析的方法也相當簡單,代碼如下所示:

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

  只需要調用Uri的靜態方法parse()就可以把內容URI字符串解析成URI對象。
  現在,我們可以通過這個Uri對象來查詢table1表中的數據了。代碼如下所示:

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

  query()方法接收的參數跟SQLiteDatabase中的query()方法接收的參數很像,但總體來說這個稍微簡單一些,畢竟這是在訪問其他程序中的數據,沒必要構建複雜的查詢語句。下標對內容提供者中的query的接收的參數進行了詳細的解釋:

  查詢完成仍然會返回一個Cursor對象,這時我們就可以將數據從Cursor對象中逐個讀取出來了。讀取的思路仍然是對這個Cursor對象進行遍歷,然後一條一條的取出數據即可,代碼如下:

if(cursor != null){//注意這裏一定要進行一次判空,因爲有可能你要查詢的表根本不存在
    while(cursor.moveToNext()){
        String column1 = cursor.getString(cursor.getColumnIndex("column1"));
        int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
    }
}

  查詢都會了,那麼剩下的增加,刪除,修改自然也不在話下了,代碼如下所示:

//增加數據
ContentValues values = new ContentValues();
values.put("Column1","text");
values.put("Column2","1");
getContextResolver.insert(uri,values);

//刪除數據
getContextResolver.delete(uri,"column2 = ?",new String[]{ "1" });

//更新數據
ContentValues values = new ContentValues();
values.put("Column1","改數據");
getContextResolver.update(uri,values,"column1 =  ? and column2 = ?",new String[]{"text","1"});

3.如何創建屬於自己應用的內容提供者?

  前面已經提到過,如果要想實現跨程序共享數據的功能,官方推薦的方式就是使用內容提供器,可以新建一個類去繼承ContentProvider類的方式來創建一個自己的內容提供器。ContentProvider類有6個抽象方法,我們在使用子類繼承它的時候,需要將這6個方法全部重寫。新建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 Uri insert(Uri uri, ContentValues values) {
        return null;
    }//添加

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

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

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

  在這6個方法中,相信出來增刪改查的方法你知道之外,剩下兩個方法你可能不知道,下面就對這些方法進行一一介紹:

  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,規則如下:

  • *:表示匹配任意長度的任意字符。
  • #:表示匹配任意長度的任意數字。

  所以,一個能夠匹配任意表的內容URI格式就可以寫成:

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

  而一個能夠匹配table表中任意一行數據的內容URI格式就可以寫成:

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

  接着,我們再借助UriMatcher這個類就可以輕鬆地實現匹配內容URI的功能。UriMatcher中提供了一個addURI()方法,這個方法接收三個參數,可以分別把authority,path和一個自定義代碼傳進去,這個自定義代碼其實就是一個final的int類型的具值。這樣,當調用UriMatcher的match()方法時,就可以將一個Uri對象傳入,返回值是某個能夠匹配這個Uri對象所對應的自定義代碼,利用這個代碼,我們就可以判斷出調用方期望訪問的是哪張表中的數據了。修改上述的MyProvider代碼如下所示:

public class MyProvider extends ContentProvider {

    public static fianl int TABLE1_DIR = 0;

    public static fianl int TABLE1_ITEM = 1;

    public static fianl int TABLE2_DIR = 2;

    public static fianl 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_DIR);
        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;
             
        }

        
        return null;

    }//查詢

    ...
}

  上述代碼只是以query()方法爲例做了個示範,其實insert()方法,update(),delete()方法的實現也是跟query()方法是差不多的,它們都會攜帶Uri這個參數,然後同樣利用UriMatcher的match()方法判斷出調用方期望訪問的是哪張表,再對該表中的數據進行相應的操作就可以了。
  除此之外,還有個方法你比較陌生,這個方法就是getType()方法。它是所有的內容提供器都必須提供的一個方法,用於獲取Uri對象所對應的MIME類型。一個內容URI所對應的MIME字符串主要由3部分組成,Android對這3個部分做了如下格式規定:

  • 必須以vnd開頭
  • 如果內容URI以路徑結尾,則後接android.cursor.dir/,如果內容URI以id結尾,則後接android.cursor.item/。
  • 最後接vnd.< authority >.< path >

  所以,對於content://com.example.app.provider/table1這個內容URI,它所對應的MIME類型就可以寫成:

vnd.android.cursor.dir/vnd.com.example.app.provider.table1 

  對於content://com.example.app.provider/table1/1這個內容URI,它所對應的MIME類型就可以寫成:

vnd.android.cursor.item/vnd.com.example.app.provider.table1 

  現在,我們可以繼續完善MyProvider類中的內容,這次實現getType()方法的邏輯,代碼如下:

public class MyProvider extends ContentProvider{

    ...
  
    @Override
    public String getType(Uri uri){
        switch(uriMatcher.match(uri)){
            case TABLE1_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
                break;

             case TABLE1_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider.table1 "
                break;

             case TABLE2_DIR:
                 return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"
                break;

             case TABLE2_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider.table2 "
                break;
        }
    } 

    ...

}

  到這裏,一個完整的內容提供器就創建完成了,現在任何一個應用程序都可以使用
ContentResolver 來訪問我們程序中的數據。那麼前面所提到的,如何才能保證隱私數據不會泄漏出去呢?其實多虧了內容提供器的良好機制,這個問題在不知不覺中已經被解決了。因爲所有的 CRUD 操作都一定要匹配到相應的內容 URI 格式才能進行的,而我們當然不可能向UriMatcher中添加隱私數據的URI,所以這部分數據根本無法被外部程序訪問到,安全問題也就不存在了。
  好了,創建內容提供器的步驟你也已經清楚了,下面就來實戰一下,真正體驗一回跨程
序數據共享的功能。

還沒有結束呢?我們都知道4大組件都需要在AndroidManifest.xml文件中進行註冊,既然完成的內容提供器寫好了,那麼下一步就是去AndroidManifest.xml文件中進行註冊,然後這個內容提供器就可以使用了。我們就來拿一個例子進行講解,一個標準的內容提供器的封裝代碼如下所示:

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;//這裏一定返回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;

     }
}

AndroidManifest.xml文件中對內容提供器進行註冊,標籤爲< provider >…< /provider >

<manifest xmlns:android="http://schemas.android.com/apk/res/android"  
          package="com.example.databasetest"  
          android:versionCode="1"  
          android:versionName="1.0" >  
          ……  
          <application  
              android:allowBackup="true"  
              android:icon="@drawable/ic_launcher"  
              android:label="@string/app_name"  
              android:theme="@style/AppTheme" >  
              ……  
              <provider  
                  android:name="com.example.databasetest.DatabaseProvider"  
                  android:authorities="com.example.databasetest.provider" >  
              </provider>  
          </application>  
</manifest>  

  這樣一個完整的內容提供器的創建流程就講完了。

高級

1.ContentPrivoder的原理機制

  其實內容提供者可跨程序訪問,這可以認爲是一種進程間通信的方式,其實它原理核心就是Binder。

  https://blog.csdn.net/tianmi1988/article/details/51077378

2.有些輪子或者SDK不需要初始化,why?

https://www.jianshu.com/p/5c0570263dfd

面試題(檢測自己學的怎麼樣)

  • 1.什麼是內容提供者?(校招&實習)
  • 2.說說如何創建自己應用的內容提供者 & 使用場景。(校招&實習)
  • 3.說說ContentProvider的原理。
  • 4.ContentProvider,ContentResolver,ContentObserver之間的關係?
  • 5.說說ContentProvider的權限管理。

注意:文章末尾面試題來自於筆者自己總結,想求答案或者交流,start 筆者GitHub項目AndroidFaceInterview

更新時間:2020-01-17

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