1. 程式人生 > >解讀Android之ContentProvider(1)CRUD操作

解讀Android之ContentProvider(1)CRUD操作

本文翻譯自android官方文件,結合自己測試,整理如下。

Content providers能夠管理結構化的資料集,封裝資料,並且能夠提供資料安全的機制。Content providers是一種標準的介面,能夠跨程序資料共享。中文可以被稱為內容提供器。

當我們想從content providers中獲取資料時,我們可以使用ContentResolver物件為客戶端訪問該providers。ContentResolver物件能夠和Content Provider例項進行通訊,該例項是繼承抽象類ContentProvider類的一個子類的例項。Content Provider接收來自客戶端(ContentResolver物件)的請求資料,執行請求動作,返回請求結果。

若我們不打算和其他應用程式進行共享資料,則我們沒有必要建立自己的content provider。若有這種打算的話,需要提供content provider,以便提供個性化的查詢建議。同時,若你想從你的程式中複製貼上複雜的資料或檔案到其他程式的話,我們也應該提供content provider。

Android系統自身也包括管理視訊,音訊,圖片,聯絡人等的content providers。我們的應用程式可以在滿足條件的情況下使用這些content providers。

Content Provider基礎

Content Provider是android應用程式的一部分,同樣能夠提供和資料互動的UI。然而,content providers主要用於被其它程式使用的。providers和providers客戶端為資料提供了一個一致的標準的介面,可以處理跨程序通訊和安全訪問資料。

本章節中主要描述以下內容:

  • content provider運作機制。
  • 在通過contentprovider訪問資料時,我們可以使用的API。
  • 在contentprovider中我們可以使用的API。
  • 其它關於方便操作provider的API。

下面將以讀取聯絡人provider講解各個部分。

概述

content provider可以通過一個或多個表將資料暴露給其他應用程式,該表類似關係資料庫中的表。表中的一行表示一條資料記錄,每一列表示該記錄的一個型別取值。這個不用過多描述。

注意: provider不需要提供主鍵,但是若想和ListView關聯時,必須提供一個名為_ID的主鍵。下面將有詳細的介紹。

訪問provider

應用程式通過抽象類ContentResovler物件訪問content provider中的資料,該ContentResovler物件和ContentProvider物件有同名的方法,能夠對持久化儲存資料進行CRUD(create,retrieve,update,and delete)。

在客戶端程式程序中的ContentResovler物件和在擁有provider的程式中的ContentProvider物件自動處理跨程序通訊。ContentProvider物件也作為資料集和資料的外部表現的抽象層。

當然,若要訪問系統provider(通常自定義的provider也需要設定許可),必須要有相應的許可(permissions)。下面有詳細介紹。例如讀取聯絡人provider需要新增下列許可:
<uses-permission android:name="android.permission.READ_CONTACTS" />
而若想要寫內容的話則需要:
<uses-permission android:name="android.permission.WRITE_CONTACTS" />

若要查詢provider表中的資料,我們可以呼叫ContentResolver.query(),然後該方法就會呼叫ContentProvider實現類物件的query()方法。例如下面檢視聯絡人資訊程式碼:


 public void forResult(View v){
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_PICK);
        intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
        if(intent.resolveActivity(getPackageManager()) != null){
            startActivityForResult(intent,REQUEST_SELECT_CONTACT);
        } else
            Toast.makeText(this,"沒有滿足條件的activity",Toast.LENGTH_LONG).show();
    }

上面程式碼是呼叫系統聯絡人程式,當我們點選某個聯絡人之後,系統聯絡人程式就會銷燬並將帶有聯絡人URI的Intent傳遞給我們的程式,我們能在onActivityResult()中處理該Intent物件,程式碼如下:


 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(REQUEST_SELECT_CONTACT == requestCode && RESULT_FIRST_USER == requestCode){
            // 獲取被點選的聯絡人URI
            Uri uri = data.getData();
            Cursor cursor = getContentResolver().query(uri,null,null,null,null);
            // 判斷是否讀取成功
            if(cursor != null){
                int indexName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
                while(cursor.moveToNext()){
                    tv_name.setText("姓名為:" + cursor.getString(indexName));
                }
            } else{
                Toast.makeText(this,"讀取聯絡人失敗,",Toast.LENGTH_LONG).show();
            }
        } else{
            Toast.makeText(this,"讀取聯絡人失敗,requestCode = " + requestCode + ",resultCode = " + resultCode,Toast.LENGTH_LONG).show();

        }
    }

關於返回的Cursor物件下面有詳細的講解,暫時不管。下面的表格中展示了query()方法中的引數如何匹配SQL SELECT語句:

引數 對應SQL部分 描述
Uri FROM table_name 指定查詢應用程式下的table_name表
mProjection select column1,column2… 指定查詢的列名
mSelectionClause WHERE column1 = value 指定查詢的where約束條件
mSelectionArgs - 為where中的佔位符提供取值
mSortOrder ORDER BY column1,column2… 指定查詢結果的排序方式

內容URIs

內容URI給內容提供器中的資料建立一種唯一的識別符號,主要包括許可權(aythority)和路徑(path)兩部分。許可權是標識provider名稱,用作區分應用程式的provider,可以在manifest配置檔案中的<provider>標籤中設定,通常以包名作為字首;路徑則是表的名稱,用於區分程式中不同的表。這樣就形成了內容URI。例如一個provider的許可權為:com.sywyg.provider,一個表名為:table1。則最終的URI字串為:content://com.sywyg.provider/table1。其中content://為協議,表示該URI為內容URI。在得到URI字串之後,可以通過,下面的方法將其解析為Uri物件:
Uri uri = Uri.parse("content://com.sywyg.provider/table1");

大多數providers可以通過指定ID訪問某一單行例如(訪問第二行):
Uri uri = ContentUris.withAppendedId("content://com.sywyg.provider/table1",2);

注意:Uri和Uri.Builder類能夠方便的通過字串構造標準格式的Uri。而ContentUris類則能夠方便地向URI中新增id(或從URI中解析id),例如上面的例子。

從provider中檢索資料

這部分將描述如何從provider中檢索資料。

為了方便描述,將ContentResolver.query()查詢方法寫在UI執行緒中,但是在實際中,應該在子執行緒中進行非同步查詢。想要在子執行緒中實現的方法之一是使用CursorLoader類(目前還未整理)。這一部分在官方文件的activity下面的Loader有詳細的講解,目前還未整理。

從provider中檢索資料需要完成以下兩步:

  1. 請求provider的訪問許可(permission);
  2. 傳送給provider查詢請求。

請求許可

若要檢索/查詢資料,需要有read access許可,我們不能在程式執行時設定許可。因此,必須在manifest配置檔案中指定該許可,通過<uses-permission>標籤使用要訪問的provider定義的許可。
例如我們想要獲取聯絡人資訊,則可以在我們的程式中manifest檔案中:

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

構建查詢語句

通過上一步設定許可之後,我們就可以對provider中的表進行查詢了。通過ContentResolver物件的query()方法設定具體的查詢語句,上面已經詳細地講過query()的使用。

處理惡意輸入

在操作資料時,小心SQL注入。例如輸入一個query()方法中的引數mSelectionClause設定為:
String mSelectionClause = "var = " + mUserInput;
這樣的話,就有可能導致惡意的SQL攻擊,例如mUserInput被賦值為`”nothing;DROP TABLE *;”,那麼provider就有可能把所有的表都刪除。

因此,我們應該通過佔位符來給約束條件賦值,通過這種方式,佔位符給出的值只作為查詢的條件,而不會進行連線到SQL語句中。例如上面的輸入可以通過下面這種方式:


String mSelectionClause =  "var = ?";
String[] selectionArgs = {""};
selectionArgs[0] = mUserInput;

其中,selectionArgs為query()的第四個引數。

即使provider不依賴SQL資料庫,上述通過佔位符指定的方式也是可以的。

顯示查詢結果

query()方法返回一個Cursor物件,這樣我們就可以從Cursor中讀取查詢結果了。provider可能會限制訪問特定的列,因此,可能導致訪問不了特定的列。若查詢結果為空的話Cursor中的方法getCount()為0。若查詢出現錯誤,結果會依賴於特定的provider,有的可能返回null,有的丟擲異常。

由於Cursor是一行一行的,因此通過SimpleCursorAdapter將資料顯示在ListView上是一個不錯的選擇。若要在ListView上顯示,表中必須要有id列,ListView通過id檢索。因此,通常來說providers需要提供id列。

從查詢結果中獲取資料

我們知道query()返回一個Cursor物件,我們可以從該物件中獲取想要的資料。通過移動遊標的方法遍歷Cursor的所有行,示例程式碼如下:


// 確定列號,即要資訊的列
int index = mCursor.getColumnIndex("column1");

/*
 * Only executes if the cursor is valid. 
 * 只有當cursor非空才執行
 */

if (mCursor != null) {
    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you will get an
     * exception.
     * 移動到下一行,行號是從-1開始的,因此第一次移動到第0行。
     */
    while (mCursor.moveToNext()) {

        // 讀取index列中的資料
        newWord = mCursor.getString(index);

        // Insert code here to process the retrieved word.
        ...
        // end of while loop
      }
    mCursor.close();
  } else {
    // Insert code here to report an error if the cursor is null or the provider threw an exception.
  }
}

moveToNext()方法將游標移動到下一行,然後通過getXXX(int)就能獲取對應列的行取值(是不是像迭代器Iterator的用法,,,,)。Cursor中有一系列的getXXX(int)方法用於返回列中對應的值(列中是該型別的值),例如上面的getString()。同樣可以通過getType()返回某列中MIME型別。Cursor中還有其他getXXX()方法,例如上面的getColumnIndex("column1")獲取指定列的索引。

Content Provider許可(permission)

provider指定的許可在使用該provider的程式中必須要宣告。許可能夠保證使用者知道程式想要獲取哪種資料。若不設定許可的話,外部程式不能使用該provider。然而和provider同一個程式中的元件可以任意獲取,即使在有許可的情況下。

可以在manifest配置檔案中通過`設定許可。在android安裝程式時,使用者必須保證該程式請求的所有許可都授權,否則不能安裝該程式。

插入/更新/刪除資料

插入資料

通過ContentResolver.insert()方法能夠對provider中的表進行插入資料,返回該插入行的URI。例如:


// 接收插入返回值
Uri mNewUri;
// 將要插入的資料儲存在ContentValues(內部是hashMap實現)
ContentValues mNewValues = new ContentValues();
// 設定每一列的值,引數為列和值
mNewValues.put("column1", "example");
// 插入到某個表中,引數為Uri和插入的ContentValues
mNewUri = getContentResolver().insert(uri,mNewValues);

通過ContentValues物件設定要插入表中的值,該物件中設定的值的型別不需要和實際列中的值的型別一致。若不想設定某列,可以通過putNull("column2")設定該列為null。對於provider中的id不需要指定值,該值作為主鍵自動新增。

對於insert()方法返回的Uri格式如下:
content://authority/table/行號id
通過這個Uri就能訪問該行,可以通過ContentUris.parseId(uri)獲得id(就是擷取帶有id的uri的最後部分)。

更新資料

通過ContentResolver.update()可以對資料進行更新,若想清除值,只需設定為null。

簡單的程式碼如下;


// 同樣使用ContentValues設定更新的資料
ContentValues mUpdateValues = new ContentValues();
// 定義約束條件,更新第column1列值為example的行。。。。。
String mSelectionClause = "column1 = ?";
String[] mSelectionArgs = {"example"};
// 接收修改行。
int mRowsUpdated = 0;
mUpdateValues.putNull("column2");
mRowsUpdated = getContentResolver().update(
    uri,
    mUpdateValues,
    mSelectionClause,
    mSelectionArgs
);

update()中的四個引數和前兩個和insert()的引數一樣,後兩個是約束條件及條件取值。

刪除資料

通過ContentResolver.delete()可以刪除資料,該方法接收三個引數:Uri,mSelectionClause,mSelectionArgs。和update()方法中的引數相比只少了一個ContentValues物件。

上述CRUD拼成簡單的SQL語句為:

  • 查詢:select * from table1 where 範圍
  • 插入:insert into table1(column1,column2) values(value1,value2)
  • 更新:update table1 set column1=value1 where 範圍
  • 刪除:delete from table1 where 範圍

Content Provider的資料型別

Content Provider提供的型別如下:

  • integer
  • long integer(long)
  • floating point
  • long floating point(double)
  • text

其它的資料型別,provider使用長度為64KB位元組陣列BLOB(Binary Large OBject)儲存資料。BLOB是資料庫中用來儲存二進位制檔案的欄位型別。

provider同樣支援MIME資料型別,用於表示定義的URIs。下面有詳細講解。

其它訪問provider的形式

有三種可選的訪問形式:

  • Batch訪問:批處理形式
    可以通過ContentProviderOperation類實現一組訪問,使用ContentResolver.applyBatch()操作。
  • 非同步查詢
    你需要在另一個執行緒中進行資料處理。可以使用CursorLoader物件實現。
  • 通過intents訪問
    儘管不能直接地傳送intent給provider,但是我們可以將intent傳送給provider所在的應用程式,這種方式是最佳的實現修改provider資料的方式。

下面詳細介紹Batch和通過intents方式,至於非同步查詢,將在Activity中的Loaders文件中介紹。

Batch訪問

當需要插入大量的行資料或者插入到多個表中時,Batch訪問就非常有用。

可以通過ContentProviderOperation類實現一組訪問,使用ContentResolver.applyBatch()操作。

例如下面進行一組插入操作:


// 建立一組ContentProviderOperation物件,每一個物件代表一次CRUD操作
ArrayList<ContentProviderOperation> ops =
          new ArrayList<ContentProviderOperation>();

 int rawContactInsertIndex = ops.size();
 // 通過ContentProviderOperation類中的Builder類的build()方法建立ContentProviderOperation物件
 // 沒記錯的話這應該是建造者模式
 ops.add(ContentProviderOperation.newInsert(uri)
          .withValue("column1", "value1")
          .withValue("column2", "value2")
          .build());

 getContentResolver().applyBatch(authority,ops);

使用intents訪問

通過Intent可以間接地訪問content provider。即使在沒有許可的條件下,我們可以通過Intent返回的結果實現間接訪問。

獲得臨時許可

在沒有許可的情況下,我們可以通過傳送intent給另一個有許可的程式,然後返回一個帶URI許可的intent物件。這些指定的URI許可將會一直存在,直到接收許可的activity銷燬。擁有永久許可的程式將授權臨時許可,通過設定intent的flag屬性:

  • Read許可:FLAG_GRANT_READ_URI_PERMISSION
  • Write許可 FLAG_GRANT_WRITE_URI_PERMISSION

注意這些flags不是讀寫provider,只是能訪問URI本身的許可。

使用helper app顯示資料

若我們的應用程式沒有許可,但是我們仍然想通過intent顯示另一個程式的資料。例如日曆程式接收一個ACTION_VIEW型的intent,就能顯示日期或事件。這種可以不用建立自己的UI來顯示日曆資訊。傳送intent的程式不需要是關聯provider的程式。

provider可以在manifest檔案中<provider>標籤屬性<android:grantUriPermission>定義URI許可,同時也可以定義<provdier>子標籤<grant-uri-permission>

例如,我們檢索Contacts Provider中的聯絡人資料,即使沒有READ_CONTACTS許可,也可以做到。可能我們只需要讀取某個聯絡人的資訊,因此不需要請求READ_CONTACTS許可來訪問所有的聯絡人,可以讓使用者選擇我們的程式可以讀取哪個聯絡人。需要完成以下步驟:

  1. 通過startActivityForResult()傳送一個包含ACTION_PICK的action(setAction())和CONTENT_ITEM_TYPE的聯絡人的MIME型別(setType())的intent,程式碼如下:

    
    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_PICK);
    intent.setType(ContactsContract.RawContacts.CONTENT_ITEM_TYPE);
    startActivityForResult(intent,RESULTCODE);
    
  2. 因為intent匹配聯絡人app的activity的intent過濾器,因此該activity將會顯示在前臺。

  3. 在這個activity中,使用者選擇一個聯絡人,然後該activity就會呼叫setResult(resultCode,intent)設定intent並返回給我們的應用程式。該intent包括:使用者選擇的聯絡人的URI,FLAG_GRANT_READ_URI_PERMISSION的flags。這些flags授予我們的程式有URI許可,能夠讀取URI指定的聯絡人。最後,該activity呼叫finish()銷燬。這個過程由聯絡人app完成。
  4. 我們的activity返回到前臺,系統呼叫onActivityResult()方法,該方法接收剛才傳過來的intent。
  5. 有了intent,我們就可以讀取Contacts Provider中的聯絡人了(通過intent.getData()獲取Uri),即使我們沒在manifest檔案中設定讀取許可。

這種方式其實就是上面演示查詢聯絡人資訊的過程。

使用另外一個應用程式

我們可以使用另外一個擁有許可的應用程式操作provider。例如,我們想向日歷中插入一些事件,則可以通過ACTION_INSERT的intent啟動日曆程式,讓日曆程式進行插入。

MIME型別

MIME (Multipurpose Internet Mail Extensions) 是描述資料型別的因特網標準。
Content Providers能夠返回標準的MIME媒體型別,或者自定義MIME型別字串,或者兩者。
MIME型別格式為:

type/subtype

例如知名MIME型別有text/html有text型別和html子型別。
自定義MIME型別字串,也被稱為”vendor-specific”MIME型別,有更復雜的型別和子型別。對於多行來說型別通常是:

vnd.android.cursor.dir

對於單行來說:

vnd.androi.cursor.item

子型別是provider指定的,android內部的providers都有一些簡單的子型別。例如當建立一個聯絡人的電話時,可以使用:

vnd.android.cursor.item/phone_v2

這裡,子型別就是phone_v2。

其它provider都有自定義的子型別,依賴於provider的許可權(autority)和路徑。例如,許可權為com.example.train2,包括表Line1,Line2和Line3。對於表Line1的URI:
content://com.example.trains/Line1
對應的MIME型別為:
vnd.android.cursor.dir/vnd.example.line1vnd.android.cursor.dir/vnd.com.example.train2.line1
對於Line2的第5行URI:
content://com.example.trains/Line2/5
對應的MIME型別為:
vnd.android.cursor.item/vnd.example.line2vnd.android.cursor.dir/vnd.com.example.train2.line2