標籤:android blog http os 使用 io strong 檔案 資料
資料來源組件ContentProvider與其他組件不同,資料來源組件並不包括特定的功能邏輯。它只是負責為應用提供資料訪問的介面。Android內建的許多資料都是使用ContentProvider形式,供開發人員調用的(如視頻,音頻,圖片,通訊錄等)。如果把第三方應用比作一個黑盒子的話,ContentProvider就像是從裡面延伸出來的管道,從這個管道,應用可以把一些資料共用出來,我們也可以往裡面輸送資料。但是裡面怎麼處理資料我們看不到,也管不著。並且這個管道是有規範標準的,不是它規定的資料你塞不進這個管道。
一、ContentProvider的特徵
- 我們為什麼使用ContentProvider?像上幾篇寫的部落格中就有好幾種方法可以跨應用來讀取資料,但ContentProvider的特點不僅僅如此。首先它作為Android中四大組件之一,(我們都知道組件的資訊會被android統一管理),提供資料的跨進程無縫隙訪問,並會在進程中提供本機快取。跨進程調用是需要時間和資源消耗的,而通過緩衝可以有效提高效率。再則ContentProvider規定了資料訪問結構,嚴謹不容易發生錯誤。然後,應用調用介面進行操作時,是一個同步的過程,也就是說,所有對資料來源組件對象中的資料操作都是在訊息佇列中串列執行的,我們開發人員就不需要考慮複雜的並發情形。最後,資料來源組件中資料存放區的方式沒有任何的限制,可以通過資料庫、檔案等任意方式實現。
- 通過什麼方式找到想要的ContentProvider?它是通過URI進行定位。URI,就是全域統一定位標誌,通過一個結構化的字串,唯一標識資料來源的地址資訊,而每個資料來源組件都有一個唯一的URI標識。
ContentProvider的scheme已經由Android所規定, scheme為:content://
主機名稱(或叫Authority)用於唯一標識這個ContentProvider,外部調用者可以根據這個標識來找到它。
路徑(path)可以用來表示我們要操作的資料,路徑的構建應根據業務而定,如下:
要操作person表中id為10的記錄,可以構建這樣的路徑:/person/10
要操作person表中id為10的記錄的name欄位, person/10/name
要操作person表中的所有記錄,可以構建這樣的路徑:/person
要操作xxx表中的記錄,可以構建這樣的路徑:/xxx
當然要操作的資料不一定來自資料庫,也可以是檔案、xml或網路等其他儲存方式,如下:
要操作xml檔案中person節點下的name節點,可以構建這樣的路徑:/person/name
如果要把一個字串轉換成Uri,可以使用Uri類中的parse()方法,如下:
Uri uri = Uri.parse("content://com.ljq.provider.personprovider/person")
二、ContentProvider的執行個體
我們還是通過一個執行個體來瞭解它吧。利用ContentProvider來對第三方的資料庫進行操作。
- 首先我們建一個DBHelper的類繼承SQLiteOpenHelper
package com.example.database;import android.content.Context;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteOpenHelper;import android.database.sqlite.SQLiteDatabase.CursorFactory;public class DBHelper extends SQLiteOpenHelper{ private static final int VERSION=1; /** * 在SQLiteOpenHelper的子類當中,必須有該建構函式 * @param context 內容物件 * @param name 資料庫名稱 * @param factory * @param version 當前資料庫的版本,值必須是整數並且是遞增的狀態 */ public DBHelper(Context context,String name,CursorFactory factory,int version){ super(context,name,factory,version); } public DBHelper(Context context, String name, int version){ this(context,name,null,version); } public DBHelper(Context context, String name){ this(context,name,VERSION); } @Override public void onCreate(SQLiteDatabase db) { // 資料庫首次構造時,會調用該函數,可以在這裡構造表、索引,等等 System.out.println("create a database"); //execSQL用於執行SQL語句 db.execSQL("create table notebook(_id integer primary key autoincrement,title varchar(20),content text,time long)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 如果給定的當前資料庫版本高於已有資料庫版本,調用該函數 System.out.println("upgrade a database"); } }這一步沒什麼好解釋的,不懂的可以看一看我寫的上一篇關於資料庫操作的博文。
- 接下來我們就要建立一個MyProvider的類繼承ContentProvider
package com.example.database;import android.content.ContentProvider;import android.content.ContentUris;import android.content.ContentValues;import android.content.UriMatcher;import android.database.Cursor;import android.database.sqlite.SQLiteDatabase;import android.net.Uri;public class MyProvider extends ContentProvider { private DBHelper dh = null;// 資料庫管理對象 private SQLiteDatabase db;//擷取其中的資料庫 //UriMatcher:Creates the root node of the URI tree. //按照官方解釋,UriMatcher是一顆Uri的樹,然後利用addURI()方法往裡面添加枝幹,通過match()函數來尋找枝幹。 private static final UriMatcher MATCHER = new UriMatcher( UriMatcher.NO_MATCH); //設定匹配碼 private static final int NOTEBOOK = 1; private static final int NOTEBOOKS = 2; static { //添加枝幹,並給它們加上唯一的匹配碼,以方便尋找 //如果match()方法匹配content://com.example.database/notebook路徑,返回匹配碼為1 MATCHER.addURI("com.example.database", "notebook", NOTEBOOKS); //如果match()方法匹配content://com.example.database/notebook/#路徑,返回匹配碼為2 //其中#號為萬用字元。 MATCHER.addURI("com.example.database", "notebook/#", NOTEBOOK); } @Override public boolean onCreate() { // 建立ContentProvider對象時會調用這個函數 dh = new DBHelper(this.getContext(),"note.db");// 資料庫操作對象 db = dh.getReadableDatabase(); return false; } /** * 查詢,返回Cursor **/ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { //通過match函數,擷取匹配碼 switch (MATCHER.match(uri)) { case NOTEBOOKS: //返回資料庫操作的結果 return db.query("notebook", projection, selection, selectionArgs, null, null, sortOrder); case NOTEBOOK: //因為添加 了id,所以要把id加到where條件中 long id = ContentUris.parseId(uri); String where = "_id=" + id; if (selection != null && !"".equals(selection)) { where = selection + " and " + where; } return db.query("notebook", projection, where, selectionArgs, null, null, sortOrder); default: throw new IllegalArgumentException("Unkwon Uri:" + uri.toString()); } } //擷取Uri的類型 @Override public String getType(Uri uri) { // TODO Auto-generated method stub switch (MATCHER.match(uri)) { case NOTEBOOKS: return "com.example.Database.all/notebook"; case NOTEBOOK: return "com.example.Database.item/notebook"; default: throw new IllegalArgumentException("Unkwon Uri:" + uri.toString()); } } //插入資料 @Override public Uri insert(Uri uri, ContentValues values) { // TODO Auto-generated method stub switch (MATCHER.match(uri)) { case NOTEBOOKS: //調用資料庫的插入功能 // 特別說一下第二個參數是當title欄位為空白時,將自動插入一個NULL。 long rowid = db.insert("notebook", "title", values); Uri insertUri = ContentUris.withAppendedId(uri, rowid);// 得到代表新增記錄的Uri this.getContext().getContentResolver().notifyChange(uri, null); return insertUri; default: throw new IllegalArgumentException("Unkwon Uri:" + uri.toString()); } } //刪除資料 @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // TODO Auto-generated method stub int count; switch (MATCHER.match(uri)) { case NOTEBOOKS: count = db.delete("notebook", selection, selectionArgs); return count; case NOTEBOOK: long id = ContentUris.parseId(uri); String where = "_id=" + id; if (selection != null && !"".equals(selection)) { where = selection + " and " + where; } count = db.delete("notebook", where, selectionArgs); return count; default: throw new IllegalArgumentException("Unkwon Uri:" + uri.toString()); } } //更新資料 @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO Auto-generated method stub int count = 0; switch (MATCHER.match(uri)) { case NOTEBOOKS: count = db.update("notebook", values, selection, selectionArgs); return count; case NOTEBOOK: long id = ContentUris.parseId(uri); String where = "_id=" + id; if (selection != null && !"".equals(selection)) { where = selection + " and " + where; } count = db.update("notebook", values, where, selectionArgs); return count; default: throw new IllegalArgumentException("Unkwon Uri:" + uri.toString()); } }}因為Uri代表了要操作的資料,所以我們經常需要解析Uri,並從Uri中擷取資料。Android系統提供了兩個用於操作Uri的工具類,分別為UriMatcher和ContentUris 。掌握它們的使用,會便於我們的開發工作。
看上去這個類很像我上次寫的DBManager類吧。其實這可以算是一個很簡單的資料操作類,關鍵地方就在於它放在了ContentProvider這個“容器”上,讓第三方應用也能訪問到己方的資料。所以想要吃透這個組件,只要透徹理解什麼是Uri,怎麼操作Uri就八九不離十了。
- 最後,不要忘記在設定檔中為ContentProvider註冊,因為這也是一個組件,所以無法避免了~
<provider android:name=".MyProvider" android:authorities="com.example.database" />
前面的是你的類名,後面則是關鍵地方,它是要寫在Uri中的,所以不要弄錯了。
到此,一個可以供其他應用訪問的工程就建好了,接下來我們來寫個測試工程來檢驗效果吧。
三、調用ContentProvider
在使用其他應用為你提供的ContentProvider時,你必須要知道的有兩點:(1)它的authorities值,在我這裡的是“com.example.database”;(2)資料檔案的結構,比如我這裡要使用的是資料庫中的booknote表,它裡面有著(_id,title,content,time)這些欄位。只有知道了這些你才能操作ContentProvider。
- 好的,我們先建立一個工程,設定一下布局檔案,效果如下
activity_main.xml
item.xml
- 接下來在MainActivity添加代碼
import android.view.View;import android.view.View.OnClickListener;import android.widget.AdapterView;import android.widget.AdapterView.OnItemClickListener;import android.widget.Button;import android.widget.EditText;import android.widget.ListView;import android.widget.SimpleCursorAdapter;import android.widget.Toast;import android.app.Activity;import android.content.ContentResolver;import android.content.ContentValues;import android.database.Cursor;import android.net.Uri;import android.os.Bundle;public class MainActivity extends Activity implements OnClickListener { private ListView listView; private SimpleCursorAdapter adapter; private Button button_query, button_insert, button_delete, button_update; private EditText editText_title, editText_content; private int CurItem; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); editText_title = (EditText) this.findViewById(R.id.editText1); editText_content = (EditText) this.findViewById(R.id.editText2); listView = (ListView) this.findViewById(R.id.listView1); listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { ListView lView = (ListView) parent; Cursor data = (Cursor) lView.getItemAtPosition(position); int _id = data.getInt(data.getColumnIndex("_id")); Toast.makeText(MainActivity.this, _id + "", Toast.LENGTH_SHORT) .show(); CurItem = _id; editText_title.setText(data.getString(data.getColumnIndex("title"))); editText_content.setText(data.getString(data.getColumnIndex("content"))); } }); button_query = (Button) this.findViewById(R.id.button1); button_query.setOnClickListener(this); button_insert = (Button) this.findViewById(R.id.button2); button_insert.setOnClickListener(this); button_delete = (Button) this.findViewById(R.id.button3); button_delete.setOnClickListener(this); button_update = (Button) this.findViewById(R.id.button4); button_update.setOnClickListener(this); } @Override public void onClick(View v) { //ContentResolver它是ContentProvider提供的一個介面,它能夠調用ContentProvider裡面的所有方法。 ContentResolver contentResolver; // TODO Auto-generated method stub switch (v.getId()) { case R.id.button1: contentResolver = getContentResolver(); //Uri.parse()能將字串轉換成Uri格式。 Uri selectUri = Uri.parse("content://com.example.database/notebook"); Cursor cursor = contentResolver.query(selectUri, null, null, null, null); adapter = new SimpleCursorAdapter(this, R.layout.item, cursor, new String[] { "_id", "title", "content", "time" }, new int[] { R.id.id, R.id.title, R.id.content, R.id.time }, 1); listView.setAdapter(adapter); break; case R.id.button2: contentResolver = getContentResolver(); Uri insertUri = Uri .parse("content://com.example.database/notebook"); ContentValues values = new ContentValues(); values.put("title", editText_title.getText().toString()); values.put("content", editText_content.getText().toString()); values.put("time", System.currentTimeMillis()); Uri uri = contentResolver.insert(insertUri, values); Toast.makeText(this, uri.toString() + "添加完成", Toast.LENGTH_SHORT) .show(); break; case R.id.button3: contentResolver = getContentResolver(); Uri deleteUri = Uri .parse("content://com.example.database/notebook/"+CurItem); int d = contentResolver.delete(deleteUri, null,null); Toast.makeText(this, CurItem+"刪除完成", Toast.LENGTH_SHORT) .show(); break; case R.id.button4: contentResolver = getContentResolver(); Uri updateUri = Uri .parse("content://com.example.database/notebook/"+CurItem); ContentValues updatevalues = new ContentValues(); updatevalues.put("title", editText_title.getText().toString()); updatevalues.put("content", editText_content.getText().toString()); updatevalues.put("time", System.currentTimeMillis()); int u = contentResolver.update(updateUri, updatevalues,null,null); Toast.makeText(this, CurItem+"更新完成", Toast.LENGTH_SHORT) .show(); break; } }}兩個應用之間的流程圖大概就是這樣了(手挫,不要嫌棄~)
- 最後,將兩個應用安裝好,開啟實踐一下。那麼我們看看運行結果吧
正常運行。那麼今天就到此結束,收工了~
四、結束語
理論上來說,資料來源組件並沒有所謂的生命週期,因為資料來源組件的狀態並不作為判定進程優先順序的依據。所以系統回收進程資源時,並不會將資料來源組件的銷毀事件告訴開發人員。但構造ContentProvider組件時還是會調用onCreate()函數。所以,不要在資料來源組件中部署延遲寫入等寫最佳化策略,當被系統默默回收時,一些未持久化的資料會丟失。一旦資料來源組件被構造出來,就會保持長期啟動並執行狀態至其所在的進程被系統回收。所以,也不要在資料來源組件中緩衝過多的資料,以免佔用記憶體空間。
【Android的從零單排開發日記】——Android四大組件之ContentProvider