【Android的从零单排开发日记】——Android四大组件之ContentProvider
数据源组件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.xmlitem.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()函数。所以,不要在数据源组件中部署延迟写入等写优化策略,当被系统默默回收时,一些未持久化的数据会丢失。一旦数据源组件被构造出来,就会保持长期运行的状态至其所在的进程被系统回收。所以,也不要在数据源组件中缓存过多的数据,以免占用内存空间。