Android开发日记(七)——Android四大组件之ContentProvider

本系列原本写于博客园,现移植到自己的博客上并重新编辑。

一、ContentProvider的特征

我们为什么使用ContentProvider?

像上几篇写的博客中就有好几种方法可以跨应用来读取数据,但ContentProvider的特点不仅仅如此。首先它作为Android中四大组件之一,(我们都知道组件的信息会被android统一管理),提供数据的跨进程无缝隙访问,并会在进程中提供本地缓存。跨进程调用是需要时间和资源消耗的,而通过缓存可以有效的提高效率。再则ContentProvider规定了数据访问结构,严谨不容易发生错误。然后,应用调用接口进行操作时,是一个同步的过程,也就是说,所有对数据源组件对象中的数据操作都是在消息队列中串行执行的,我们开发者就不需要考虑复杂的并发情形。最后,数据源组件中数据存储的方式没有任何的限制,可以通过数据库、文件等任意方式实现。

通过什么方式找到想要的ContentProvider?

它是通过URI进行定位。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

首先我们建一个DBHelper的类继承SQLiteOpenHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

接下来我们就要新建一个MyProvider的类继承ContentProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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注册,因为这也是一个组件,所以无法避免了~

1
<provider android:name=".MyProvider" android:authorities="com.example.database" />

前面的是你的类名,后面则是关键地方,它是要写在Uri中的,所以不要弄错了。
到此,一个可以供其他应用访问的工程就建好了,接下来我们来写个测试工程来检验效果吧。

三、调用ContentProvider

在使用其他应用为你提供的ContentProvider时,你必须要知道的有两点:(1)它的authorities值,在我这里的是“com.example.database”;(2)数据文件的结构,比如我这里要使用的是数据库中的booknote表,它里面有着(_id,title,content,time)这些字段。只有知道了这些你才能操作ContentProvider。

设置布局文件

好的,我们先新建一个工程,设置一下布局文件
布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<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" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:orientation="horizontal" >

<LinearLayout
android:layout_width="120dp"
android:layout_height="match_parent"
android:orientation="vertical" >

<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="45dp"
android:text="Title:"
android:textAlignment="center"
android:textSize="25sp" />

<TextView
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="45dp"
android:text="Content:"
android:textAlignment="center"
android:textSize="25sp" />
</LinearLayout>

<LinearLayout
android:layout_width="200dp"
android:layout_height="match_parent"
android:orientation="vertical" >

<EditText
android:id="@+id/editText1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="25sp" >

<requestFocus />
</EditText>

<EditText
android:id="@+id/editText2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="25sp" />
</LinearLayout>
</LinearLayout>

<ListView
android:id="@+id/listView1"
android:layout_width="match_parent"
android:layout_height="196dp" >
</ListView>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_weight="0.20" >

<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="获取全部信息" />

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="插入一条信息" />

<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除一条信息" />

<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="更新一条信息" />

</LinearLayout>

</LinearLayout>

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<TextView
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="_id"
android:id="@+id/id"
/>
<TextView
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="title"
android:id="@+id/title"
/>

<TextView
android:layout_width="100dip"
android:layout_height="wrap_content"
android:text="content"
android:id="@+id/content"
/>

<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="time"
android:id="@+id/time"
/>

</LinearLayout>

在MainActivity添加代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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设计的巧妙和特征,学会正确的使用Android应用的框架。那么接下来可能会带来的是Android的Intent机制,尽请期待~


参考文章:
(1)ContentProvider和Uri详解
(2)ContentProvider浅析
(3)从头学Android之ContentProvider
下载:ContentProviderDemo下载

文章作者: cpacm
文章链接: http://www.cpacm.net/2015/03/22/Android开发日记(七)——Android四大组件之ContentProvider/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 cpacm
打赏
  • 微信
  • 支付宝

评论