1. 程式人生 > >Android 如何自定義一個ContentProvider

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,如下:

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等。

       這篇文章就分享到這裡啦,有疑問可以留言,亦可糾錯,亦可補充,互相學習...^_^