Android 四大元件之 ContentProvider

Keven發表於2019-04-08

讀前思考

學習一門技術或者看一篇文章最好的方式就是帶著問題去學習,這樣才能在過程中有茅塞頓開、燈火闌珊的感覺,記憶也會更深刻。

  1. ContentProvider 是什麼?
  2. ContentProvider 如何使用?
  3. ContentProvider 和其他通訊方式比有什麼區別與優點?
  4. 什麼是 URI ?怎麼書寫?有什麼含義?

認識 URI

通用資源標誌符(Universal Resource Identifier, 簡稱 "URI")
Uri 代表要操作的資料,Android 上可用的每種資源、影像、視訊片段等都可以用 Uri 來表示。Uri 唯一標識每種資源。
Uri一般由三部分組成: 1.訪問資源的命名機制。 2.存放資源的主機名。 3.資源自身的名稱,由路徑表示。
android 的 Uri 由以下三部分組成:
"content://"、資料的路徑、標示 ID(可選)

一個標準的內容 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. #:表示匹配任意長度的數字

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

content://com.example.app.provider/*
複製程式碼

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

content://com.example.app.provider/table1/#
複製程式碼

什麼是 ContentProvider ?

作為四大元件之一,ContentProvider 主要負責儲存和共享資料。與檔案儲存、SharedPreferences 儲存、SQLite 資料庫儲存這幾種資料儲存方法不同的是,後幾者儲存下的資料只能被該應用程式使用,而前者可以讓不同應用程式之間進行資料共享,它還可以選擇只對哪一部分資料進行共享,從而保證程式中的隱私資料不會有洩漏風險。

ContentProvider 有兩種形式

  1. 可以使用現有的內容提供者來讀取和操作相應程式中的資料。
  2. 也可以建立自己的內容提供者給這個程式的資料提供外部訪問介面。

從系統提供的 Provider 訪問資料

例子:讀取聯絡人的電話

1.清單檔案中新增讀取許可權

<uses-permission android:name="android.permission.READ_CONTACTS" />
複製程式碼
  1. 針對 6.0+ 系統動態許可權申請
//判斷是否有讀取聯絡人許可權
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, 1);
} else {
    readContacts();
}
複製程式碼
  1. 重寫 onRequestPermissionsResult( ) 方法獲取許可權申請返回
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case 1:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                readContacts();
            } else {
                Toast.makeText(this, "獲取許可權失敗!", Toast.LENGTH_SHORT);
            }
            break;
        default:
            break;
        }
    }
複製程式碼
  1. 獲取手機通訊錄
private void readContacts() {
    List contactsList=null;
    Cursor cursor=null;
    try {
        contactsList=new ArrayList();
        //查詢聯絡人資料,使用了getContentResolver().query方法來查詢系統的聯絡人的資料
        //CONTENT_URI就是一個封裝好的Uri,是已經解析過得常量
        cursor=getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
        //對cursor進行遍歷,取出姓名和電話號碼
        if (cursor!=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
                    ));
                //把取出的兩類資料進行拼接,中間加換行符,然後新增到listview中
                contactsList.add(displayName+"\n"+number);
                LogUtils.i("姓名:"+displayName+"\n"+"電話:"+number);
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //記得關掉cursor
            if (cursor!=null){
                cursor.close();
            }
        }
    }
複製程式碼
  1. 最後的結果輸出
com.keven.jianshu I/TAG: 姓名:孫**
    電話:+86157****429
com.keven.jianshu I/TAG: 姓名:井**
    電話:+861836****299
com.keven.jianshu I/TAG: 姓名:張**
    電話:+861866****830
com.keven.jianshu I/TAG: 姓名:釘**
    電話:0105****898
com.keven.jianshu I/TAG: 姓名:彩**
    電話:0108****604
複製程式碼

建立自己的 Provider

  1. 自定義類繼承 ContentProvider,重寫六個方法
public class Part1dMyProvider extends ContentProvider {
    /**
     * 初始化內容提供器的時候呼叫。通常會在這裡完成對資料庫的建立和升級等操作,
     * 返回 true 表示內容提供器初始化成功,返回 false 則表示失敗。注意,只有
     * 當存在 ContentResolver 嘗試訪問我們程式中的資料時,內容提供器才會被初始化。
     */
    @Override
    public boolean onCreate() {
        return false;
    }
    /**
     * 從內容提供器中查詢資料。使用 uri 引數來確定查詢哪張表,projection 引數用
     * 於確 定查詢哪些列,selection 和 selectionArgs 引數用於約束查詢哪些行,
     * sortOrder 引數用於 對結果進行排序,查詢的結果存放在 Cursor 物件中返回。
     */
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        return null;
    }
    /**
     * 向內容提供器中新增一條資料。使用 uri 引數來確定要新增到的表,待新增的資料
     * 儲存在 values 引數中。新增完成後,返回一個用於表示這條新記錄的 URI。
     */
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }
    /**
     * 更新內容提供器中已有的資料。使用 uri 引數來確定更新哪一張表中的資料,新數
     * 據儲存在 values 引數中,selection 和 selectionArgs 引數用於約束更新哪些行,
     * 受影響的 行數將作為返回值返回。
     */
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }
    /**
     * 從內容提供器中刪除資料。使用 uri 引數來確定刪除哪一張表中的資料,selection
     * 和 selectionArgs 引數用於約束刪除哪些行,被刪除的行數將作為返回值返回。
     */
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }
    /**
     * 根據傳入的內容 URI 來返回相應的 MIME 型別。 可以看到,幾乎每一個方法都會
     * 帶有 Uri 這個引數,這個引數也正是呼叫 ContentResolver的增刪改查方法時傳
     * 遞過來的。而現在,我們需要對傳入的 Uri 引數進行解析,從中分析出 呼叫方
     * 期望訪問的表和資料。
     */
    @Override
    public String getType(Uri uri) {
        return null;
    }
}
複製程式碼
  1. 使用 UriMatcher
public class Part1dMyProvider extends ContentProvider {
    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.keven.jianshu.provider", "table1", TABLE1_DIR);

        uriMatcher.addURI("com.keven.jianshu.provider ", "table1/#", TABLE1_ITEM);

        uriMatcher.addURI("com.keven.jianshu.provider ", "table2", TABLE2_ITEM);

        uriMatcher.addURI("com.keven.jianshu.provider ", "table2/#", TABLE2_ITEM);

    }
    ......
}
複製程式碼
  1. 以 query( ) 方法為例示範(insert()、update()、delete() 實現類似)
@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;
    }
    ……
}
……
}
複製程式碼

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

  1. 必須以 vnd 開頭。
  2. 如果內容 URI 以路徑結尾,則後接 android.cursor.dir/,如果內容 URI 以 id 結尾, 則後接 android.cursor.item/。
  3. 最後接上 vnd..

所以,對於 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
複製程式碼

則我們的自定義 Provider 可以完善為

@Override

public String getType(Uri uri) { 

switch (uriMatcher.match(uri)) { 

    case TABLE1_DIR:
        return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";

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

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

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

    default:
        break;
}
return null;
}
複製程式碼
  1. 在清單檔案中註冊
<provider
    android:authorities="com.keven.jianshu.Part1dMyProvider"
    android:name=".part1.Part1dMyProvider"
    android:exported="true"/>
複製程式碼
  • name:ContentProvider 的全稱類名。
  • authorities:唯一標識了一個 ContentProvider,外部應用通過該屬性值來訪問我們的 ContentProvider。因此該屬性值必須是唯一的,建議在命名時以包名為字首。
  • exported:表明是否允許其他應用呼叫 ContentProvider,true 表示支援,false 表示不支援。預設值根據開發者的屬性設定而會有所不同,如果包含 Intent-Filter 則預設值為 true,否則為 false。

到這裡,一個完整的內容提供器就建立完成了,現在任何一個應用程式都可以使用 ContentResolver 來訪問我們程式中的資料。

文章已經讀到末尾了,不知道最初的幾個問題你都會了嗎?如果不會的話?可以再針對不會的問題進行精讀哦!答案都在文中,相信你肯定可以解決的!

相關文章