Android 如何自定義一個ContentProvider
一,寫在前面
我們知道Android有四大元件,ContentProvider是其中之一,顧名思義:內容提供者。什麼是內容提供者呢?一個抽象類,可以暴露應用的資料給其他應用。應用裡的資料通常說的是資料庫,事實上普通的檔案,甚至是記憶體中的物件,也可以作為內容提供者暴露的資料形式。為什麼要使用內容提供者呢?從上面定義就知道,內容提供者可以實現應用間的資料訪問,一般是暴露表格形式的資料庫中的資料。內容提供者的實現機制是什麼呢?由於是實現應用間的資料通訊,自然也是兩個程序間的通訊,其內部實現機制是Binder機制。那麼,內容提供者也是實現程序間通訊的一種方式。
事實上在開發中,很少需要自己寫一個ContentProvider,一般都是去訪問其他應用的ContentProvider。本篇文章之所以去研究如何自己寫一個ContentProvider,也是為了更好的在開發中理解:如何訪問其他應用的內容提供者。
二,實現一個ContentProvider
接下來介紹如何自己去實現一個內容提供者,大致分三步進行:
1,繼承抽象類ContentProvider,重寫onCreate,CUDR,getType六個方法;
2,註冊可以訪問內容提供者的uri
3,清單檔案中配置provider
第一步,onCreate()方法中,獲取SQLiteDatabase物件;CUDR方法通過對uri進行判斷,做相應的增刪改查資料的操作;getType方法是返回uri對應的MIME型別。
第二步,建立靜態程式碼塊,static{...code},在類載入的時候註冊可以訪問內容提供者的uri,使用類UriMatcher的addURI(...)完成。
第三步,註冊內容提供者,加入authorities屬性,對外暴露該應用的內容提供者。
直接上程式碼,應用B的MyContentProvider,如下:
DbOpenHelper程式碼如下:public class MyContentProvider extends ContentProvider { private DbOpenHelper helper; private SQLiteDatabase db; private static UriMatcher uriMatcher; public static final String AUTHORITY = "com.example.mycontentprovider.wang"; public static final int CODE_PERSON = 0; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(AUTHORITY, "person", CODE_PERSON); } @Override public boolean onCreate() { helper = DbOpenHelper.getInstance(getContext()); db = helper.getWritableDatabase(); //在資料庫裡新增一些資料 initData(); return true; } public void initData() { for (int i = 0; i < 5; i++) { ContentValues values = new ContentValues(); values.put("name", "kobe" + (i + 1)); values.put("age", 21 + i); db.insert("person", null, values); } } @Override public String getType(Uri uri) { return null; } public String getTableName(Uri uri) { if (uriMatcher.match(uri) == CODE_PERSON) { return "person"; } else { //... } return null; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { String tableName = getTableName(uri); if (tableName == null) { throw new IllegalArgumentException("uri has not been added by urimatcher"); } Cursor cursor = db.query(tableName, projection, selection, selectionArgs, null, null, null); return cursor; } @Override public Uri insert(Uri uri, ContentValues values) { String tableName = getTableName(uri); if (tableName == null) { throw new IllegalArgumentException("uri has not been added by urimatcher"); } db.insert(tableName, null, values); //資料庫中資料發生改變時,呼叫 getContext().getContentResolver().notifyChange(uri, null); return uri; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { String tableName = getTableName(uri); if (tableName == null) { throw new IllegalArgumentException("uri has not been added by urimatcher"); } int row = db.delete(tableName, selection, selectionArgs); if (row > 0) { getContext().getContentResolver().notifyChange(uri, null); } return row; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { String tableName = getTableName(uri); if (tableName == null) { throw new IllegalArgumentException("uri has not been added by urimatcher"); } int row = db.update(tableName, values, selection, selectionArgs); if (row > 0) { getContext().getContentResolver().notifyChange(uri, null); } return row; } }
public class DbOpenHelper extends SQLiteOpenHelper {
public DbOpenHelper(Context context, String name, CursorFactory factory,
int version) {
super(context, name, factory, version);
}
private static DbOpenHelper helper;
public static synchronized DbOpenHelper getInstance(Context context) {
if (helper == null) {
//建立資料庫
helper = new DbOpenHelper(context, "my_provider.db", null, 1);
}
return helper;
}
//建立表
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "create table person (_id integer primary key autoincrement, name Text, age integer)";
db.execSQL(sql);
}
//資料庫升級時,回撥該方法
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
在MyContentProvider$onCreate方法中,通過一個抽象幫助類SQLiteOpenHelper的子類例項,呼叫getWritableDatabase()獲取SQLiteDatabase例項。先簡單介紹下SQLiteOpenHelper,DbOpenHelper中我們提供一個getInstance的方法,用於獲得SQLiteOpenHelper的一個子類例項,並採用單例設計模式;onCreate方法:建立資料庫的表,且可以建立多個表;onUpgrade方法:在資料庫版本發生改變時,該方法被回撥,可以加入修改表的操作的程式碼。在MyContentProvider$onCreate方法中獲取了SQLiteDatabase例項就可以操作資料庫,下面分析第二步的註冊uri。
註冊uri的目的就是確定哪些URI可以訪問應用的資料,通常這些uri是由其他應用傳遞過來的,在後面訪問uri的模組中會有所瞭解。UriMatcher可以用於註冊uri,看起來就像一個容器,可以儲存uri,還可以判斷容器中是否有某一個uri。事實上,UriMatcher內部維護了一個ArrayList集合。檢視UriMatcher的建構函式,程式碼如下:
public UriMatcher(int code)
{
mCode = code;
mWhich = -1;
mChildren = new ArrayList<UriMatcher>();
mText = null;
}
由此可見UriMatcher並不是一個什麼陌生的東西,就是學習Java時接觸到的ArrayList集合,只是將新增uri,判斷uri的操作做了相應的封裝。addURI(String authority,String path, int code),authority,path後面會講到;code:與uri一一對應的int值,後面在判斷uri是否新增到UriMatcher時,是先將該uri轉化為code,再進行判斷。
接下里分析CUDR操作,我們重寫了這樣四個方法:query,insert,delete,update,這個四個方法的引數都是想訪問該應用的其他使用者傳遞過來的,重點看uri。那麼這個uri是如何構成的呢?uri = scheme + authorities + path。先看這樣一個uri,
uri = "content://com.example.mycontentprovider.wang/a/b/c",
scheme:"content://";
authorities:com.example.mycontentprovider.wang;authorities就是在清單檔案中配置的authorities屬性的值,唯一標識該應用的內容提供者。
path:/a/b/c;path裡面常常放的是一些表名,欄位資訊,確定訪問該資料庫中哪個表的哪些資料,具體是訪問哪些資料還要看CUDR對該uri做了怎樣的操作。
在getTableName方法中,我們呼叫uriMatcher.match(uri)獲取uri對應的code,如果該code沒有註冊過,則丟擲異常IllegalArgumentException。也就是說,在其他應用訪問本應用的內容提供者時,如果uri“不合法”,那麼會丟擲IllegalArgumentException異常。
然後呼叫SQLiteDatabase的query,insert,delete,update四個方法進行增刪改查資料,值得一提的是,在增加,刪除,修改資料後,需要呼叫內容解決者ContentResolver的notifyChange(uri,observer),通知資料發生改變。getType方法返回uri請求檔案的MIME型別,這裡返回null;
清單檔案中註冊provider程式碼如下:
<provider
android:name="com.example.mycontentprovider.provider.MyContentProvider"
android:authorities="com.example.mycontentprovider.wang"
android:exported="true" >
</provider>
authorities(也稱,授權)屬性必須指定相應的值,唯一標識該內容提供者,每個內容提供者的authorities的值都不同,它是訪問的uri的一部分。
exported屬性:若沒有intent-filter,則預設false,不可訪問;若有intent-filter,則預設true,可以訪問。亦可手動設定
還可以新增許可權屬性,有興趣的哥們可以自己去研究。以上就是自己寫一個內容提供者的過程,分三步完成。下面展示另一個應用A,如何訪問該應用的ContentProvider。
三,訪問ContentProvider
應用A的程式碼,xml佈局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<Button
android:id="@+id/btn_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="新增一條name為Tom,age為21的資料"/>
<Button
android:id="@+id/btn_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="刪除name為Tom的資料"/>
<Button
android:id="@+id/btn_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="更改最後一條資料的name為paul"/>
<Button
android:id="@+id/btn_query"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查詢所有資料"/>
</LinearLayout>
實體類Person程式碼如下:
package com.example.mcontentprovider.domain;
public class Person {
public int _id;
public String name;
public int age;
public Person() {
super();
}
public Person(int _id, String name, int age) {
super();
this._id = _id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
MainActivity程式碼如下:public class MainActivity extends Activity implements OnClickListener {
private Button btn_add;
private Button btn_deleteAll;
private Button btn_query;
private Button btn_update;
private ContentResolver cr;
private static final String AUTHORITIES = "com.example.mycontentprovider.wang";
private MyContentObserver observer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
cr = getContentResolver();
observer = new MyContentObserver(new Handler());
cr.registerContentObserver(Uri.parse(uri), false, observer);
initView();
}
public void initView() {
btn_add = (Button) findViewById(R.id.btn_add);
btn_deleteAll = (Button) findViewById(R.id.btn_delete);
btn_query = (Button) findViewById(R.id.btn_query);
btn_update = (Button) findViewById(R.id.btn_update);
btn_add.setOnClickListener(this);
btn_deleteAll.setOnClickListener(this);
btn_query.setOnClickListener(this);
btn_update.setOnClickListener(this);
}
private String uri = "content://" + AUTHORITIES + "/person";
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_add:
new Thread(){
public void run() {
//休眠3秒,模擬非同步任務
SystemClock.sleep(3000);
add();
};
}.start();
break;
case R.id.btn_delete:
Log.e("MainActivity", "刪除名字為Tom的資料");
cr.delete(Uri.parse(uri), "name = ?", new String[]{"Tom"});
break;
case R.id.btn_query:
Cursor cursor = cr.query(Uri.parse(uri), null, null, null, null);
ArrayList<Person> persons = new ArrayList<Person>();
while (cursor.moveToNext()) {
int _id = cursor.getInt(0);
String name = cursor.getString(1);
int age = cursor.getInt(2);
persons.add(new Person(_id, name, age));
}
Log.e("MainActivity", persons.toString());
break;
case R.id.btn_update:
Log.e("MainActivity", "更改最後一條資料的name為paul");
ContentValues values2 = new ContentValues();
values2.put("name", "paul");
//獲取資料庫的行數
Cursor cursor2 = cr.query(Uri.parse(uri), null, null, null, null);
int count = cursor2.getCount();
cr.update(Uri.parse(uri), values2, "_id = ?", new String[]{count + ""});
break;
default:
break;
}
}
private void add() {
Log.e("MainActivity", "新增一條name為Tom,age為21的資料");
ContentValues values = new ContentValues();
values.put("name", "Tom");
values.put("age", 21);
cr.insert(Uri.parse(uri), values);
}
private class MyContentObserver extends ContentObserver {
public MyContentObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
Toast.makeText(getApplicationContext(), "資料改變啦!!!", 0).show();
super.onChange(selfChange);
}
}
}
在應用A中,我們設定uri = "content://" + AUTHORITIES + "/person",增刪改查的操作對應都是該uri。事實上,只要內容提供者註冊了的uri都可以訪問,這裡暫且讓uri都相同。有興趣的哥們可以嘗試一下,若uri不合法,確實會丟擲IllegalArgumentException異常。在實際開發中,最重要的是尋找到需要的uri,然後進行CUDR操作,如何進行CUDR操作不是本篇重點,不做講解。
注意到程式碼裡新增資料時,這裡建立了一個執行緒,使執行緒休眠了3s,用於模擬新增大量資料時的非同步操作。同時註冊了一個內容觀察者用於監聽資料變化,cr.registerContentObserver(Uri.parse(uri), false, observer)。第一個引數:監聽的uri。第二個引數:若為true,表示以該uri字串為開頭的uri都可以監聽;若為false,表示只能監聽該uri。第三個引數:ContentObserver子類例項,資料發生改變時回撥onChange方法。
執行點選操作,檢視log。
查詢;
新增->查詢;(在點選新增按鈕後,過了3秒左右,彈出toast,顯示"資料改變啦!!!")
刪除->查詢;
更改->查詢;
log如下:
這裡解釋下,在新增資料時,為何模擬非同步操作。有這樣一個場景:當資料新增進內容提供者的資料庫中後,才可以執行某一個操作。那麼onChange方法被回撥時,就是一個很好的時機去執行某一個操作。
可能有的哥們要問:在應用A中呼叫了ContentResolver的CUDR方法,那麼怎麼應用B中資料庫的資料為何能變化呢?表面上可以這樣理解:應用A在呼叫ContentResolver的CUDR方法時,會使應用B中對應的CUDR方法被呼叫,而uri則是應用A傳遞給應用B的。而為何“會使應用B中對應的CUDR方法被呼叫”,但是是Binder機制實現的。包括被回撥的onChange方法也是Binder機制才能實現的,試想資料增刪改查操作是在應用B完成的,為何在應用B中呼叫notifyChange方法通知資料改變後,應用A的onChange方法能被回撥。
侃了這麼多,拿程式碼來點一下,檢視ContentResolver$notifyChange原始碼如下:
public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork,
int userHandle) {
try {
getContentService().notifyChange(
uri, observer == null ? null : observer.getContentObserver(),
observer != null && observer.deliverSelfNotifications(), syncToNetwork,
userHandle);
} catch (RemoteException e) {
}
}
繼續檢視ContentResolver$getContentService方法:
public static IContentService getContentService() {
if (sContentService != null) {
return sContentService;
}
IBinder b = ServiceManager.getService(CONTENT_SERVICE_NAME);
if (false) Log.v("ContentService", "default service binder = " + b);
sContentService = IContentService.Stub.asInterface(b);
if (false) Log.v("ContentService", "default service = " + sContentService);
return sContentService;
}
sContentService不就是代理物件麼,呼叫代理物件的notifyChange(...)方法:內部會呼叫transact方法向服務發起請求;然後onTransact(...)被呼叫,會呼叫IContentService介面的notifyChange方法完成通訊。介面IContentService中方法的重寫是在extends IContentService.Stub的類中,也就是ContentService。
四,另外
好了,上面只是簡單點了一下,說明ContentProvider暴露資料給其他應用訪問,內部就是Binder機制原理實現的。常用程序間通訊方式有:AIDL,ContentProvider,Messenger等。
這篇文章就分享到這裡啦,有疑問可以留言,亦可糾錯,亦可補充,互相學習...^_^