解讀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中檢索資料需要完成以下兩步:
- 請求provider的訪問許可(permission);
- 傳送給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許可來訪問所有的聯絡人,可以讓使用者選擇我們的程式可以讀取哪個聯絡人。需要完成以下步驟:
通過
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);
因為intent匹配聯絡人app的activity的intent過濾器,因此該activity將會顯示在前臺。
- 在這個activity中,使用者選擇一個聯絡人,然後該activity就會呼叫
setResult(resultCode,intent)
設定intent並返回給我們的應用程式。該intent包括:使用者選擇的聯絡人的URI,FLAG_GRANT_READ_URI_PERMISSION
的flags。這些flags授予我們的程式有URI許可,能夠讀取URI指定的聯絡人。最後,該activity呼叫finish()
銷燬。這個過程由聯絡人app完成。 - 我們的activity返回到前臺,系統呼叫
onActivityResult()
方法,該方法接收剛才傳過來的intent。 - 有了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.line1
或vnd.android.cursor.dir/vnd.com.example.train2.line1
對於Line2的第5行URI:
content://com.example.trains/Line2/5
對應的MIME型別為:
vnd.android.cursor.item/vnd.example.line2
或vnd.android.cursor.dir/vnd.com.example.train2.line2