一,寫在前面
我們知道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,如下:
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;
}
}
DbOpenHelper代碼如下:
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等。
這篇文章就分享到這裏啦,有疑問可以留言,亦可糾錯,亦可補充,互相學習...^_^