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


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




          

   

         



 



       

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章