前言
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;
}
}
在這六個方法中,相信大多數你都已經非常熟悉了。
- onCreate() 初始化內容提供器的時候呼叫。通常會在這裡完成對資料庫的建立和升級等操作, 返回 true 表示內容提供器初始化成功,返回 false 則表示失敗。注意,只有當存在 ContentResolver嘗試訪問我們程式中的資料時,內容提供器才會被初始化。
- query() 從內容提供器中查詢資料。使用 uri引數來確定查詢哪張表,projection引數用於確 定查詢哪些列,selection和 selectionArgs引數用於約束查詢哪些行,sortOrder引數用於 對結果進行排序,查詢的結果存放在 Cursor物件中返回。
- insert() 向內容提供器中新增一條資料。使用 uri 引數來確定要新增到的表,待新增的資料 儲存在 values引數中。新增完成後,返回一個用於表示這條新記錄的 URI。
- update() 更新內容提供器中已有的資料。使用 uri 引數來確定更新哪一張表中的資料,新數 據儲存在 values引數中,selection和 selectionArgs引數用於約束更新哪些行,受影響的 行數將作為返回值返回。
- delete() 從內容提供器中刪除資料。使用 uri 引數來確定刪除哪一張表中的資料,selection和 selectionArgs引數用於約束刪除哪些行,被刪除的行數將作為返回值返回。
- 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,規則如下。
- "*":表示匹配任意長度的任意字元
- "#" :表示匹配任意長度的數字
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對這三個部分做了如下格式規定。
- 必須以 vnd開頭。
- 如果內容 URI 以路徑結尾,則後接 android.cursor.dir/,如果內容 URI 以 id 結尾, 則後接 android.cursor.item/。
- 最後接上 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,開發人員必備技能——單元測試