持久化技术
数据持久化就是讲那些内存中的瞬时数据保存到储存设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是属于瞬时状态的,儿保存在存储设备中的数据上处于持久状态的,持久化技术提供了一种可以让数据在瞬时状态和持久状态之间转换的机制。
Android系统中主要提供了3种方式用于简单地实现数据持久化功能,即文件存储、SharedPreference
存储以及数据库存储。当然,除了这三种之外,还可以将数据保存在手机的SD卡之中,不过使用文件、SharedPreference
存储以及数据库存储会更加简单一些,而且更加安全。
文件存储
文件存储是Android中最基本的一种数据存储方式,它不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合用于存储一些简单的文本数据或者二进制数据。
如果想要使用文件存储的方式来保存一些较为复杂的文本数据,就需要定义一套自己的格式规范,这样就可以方便之后将数据从文件中重新解析出来。
将数据存储到文件中
Context
类中提供了一个openFileOutput
方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数,第一个参数的文件名,在文件创建的时候使用的就是这个名称,注意这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data/
目录下的。第二个参数是文件的操作模式,主要有两种模式可以选:MODE_PRIVATE
和MODE_APPEND
。其中MODE_PRIVATE
是默认的操作模式,写入的内容会覆盖原文件的内容,而MODE_APPEND
则表示如果该文件已经存在,就往文件里面追加内容,不存在就创建新文件。
openFileOutput
方法返回的是一个FileOutputStream
对象,得到了这个对象之后就可以使用Java
流的方式将数据写入到文件中了。
public void save(){
String data = "Data to save";
FileOutputStream out = null;
BufferedWriter writer = null;
try{
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(data);
} catch (IOException e){
e.printStackTrace();
} finally {
try {
if (writer != null)
writer.close();
}catch (IOException e) {
e.printStackTrace();
}
}
}
这里通过openFileOutput
方法能够得到一个FileOutputStream
对象,然后再借助它构建出一个OutputStreamWriter
对象,接着再构建一个BufferedWriter
对象,这样就可以通过BufferedWriter
来将文本内容写入到文件中了。
下面创建一个完整的例子:
//FilePersistenceTest.MainActivity
public class MainActivity extends AppCompatActivity {
private EditText edit;
private Button confirm;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit = (EditText) findViewById(R.id.edit);
confirm = (Button) findViewById(R.id.Upload);
confirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String inputText = edit.getText().toString();
save(inputText);
}
});
}
public void save(String inputText){
FileOutputStream out = null;
BufferedWriter writer = null;
try{
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(inputText);
} catch (IOException e){
e.printStackTrace();
} finally {
try{
if (writer != null)
writer.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}
从文件中读取数据
类似于将数据存储到文件中,Context
类中还提供了一个openFileInput
方法,用于从文件中读取数据。这个方法要比openFileOutput
简单一些,只接受一个参数,即要读取的文件名,然后系统会自动到/data/data/
目录下去加载这个文件,并返回一个FileInputStream
对象,得到了这个对象之后再通过Java
流的方式就可以将数据读取出来了。
public String load(){
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try{
in = openFileInput("data");
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while((line = reader.readLine()) != null)
content.append(line);
} catch (IOException e){
e.printStackTrace();
} finally {
if (reader != null){
try {
reader.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
return content.toString();
}
在这段代码中,首先通过openFileInput
方法获取到了一个FileInputStream
对象,然后借助它有构建出了一个InputStreamReader
对象,再构造一个BufferedReader
。这样我们就可以通过BufferedReader
进行一行一行地读取,把文件中所有的文本内容全部读取出来,并且存放在一个StringBuilder
对象中,最后将读取到的内容返回即可。
然后我们来完善一下上面的例子。
在onCreate
方法中加入下面的代码:
String inputText = load();
if (!TextUtils.isEmpty(inputText)){
edit.setText(inputText);
edit.setSelection(inputText.length());
Toast.makeText(this, "Restoring succeeded", Toast.LENGTH_LONG).show();
}
注意,上述代码在对字符串进行非空判断的时候用到了TextUtils.isEmpty
方法,这是个非常好用的方法,可以一次进行两种空值的判断。当传入的字符串为null
或者等于空字符串的时候,这个方法都会返回true
,从而使得我们不需要先单独判断这两种空值在使用逻辑运算符连接起来了。
SharedPreferences存储
不同于文件的储存方式,SharedPreferences
是使用键值对的方式来存储数据的。也就是说,当保存一条数据的时候,需要给这条数据提供一个队员的键,这样在读取数据的时候可以通过这个键把对应的值取出来。而且SharedPreferences
还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型。
将数据存储到SharedPreferences中
要想使用SharedPreferences
,需要先获取SharedPreferences
对象。
Android中主要提供了三种方法用于得到SharedPreferences
对象。
-
Context
类中的getSharedPreferences
方法
此方法接受两个参数,第一个参数用于指定SharedPreferences
文件的名称,如果文件不存在则创建一个。文件都存放在/data/data/
目录下。第二个参数用于指定操作模式,目前只有/shared_prefs/ MODE_PRIVATE
一种模式可选。它是默认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences
文件进行读写。 -
Acitvity
类中的getPreferences
方法
这个方法和上面的很相似,不过只接受一个操作模式参数,它会自动使用当前活动的类名来作为SharedPreferences
的文件名。 -
PreferenceManager
类中的getDefaultSharedPreferences
方法
这是一个静态方法,它接收一个Context
参数,并自动使用当前应用程序的报名作为前缀来命名SharedPreferences
文件。
得到了SharedPreferences
对象之后,就可以开始向SharedPreferences
文件中存储数据了。
- 调用
SharedPreferences
对象的edit
方法来获取一个SharedPreferences.Editor
对象。 - 向
SharedPreferences.Editor
对象中添加数据,比如添加一个布尔型数据就使用putBoolean
方法,添加一个字符串则使用putString
方法。 - 调用
apply
方法将添加的数据提交,从而完成数据存储操作。
实践
这里用一个按钮来作为演示。
confirm = (Button) findViewById(R.id.Upload);
confirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
editor.putString("name ", "Tom");
editor.putInt("age", 28);
editor.putBoolean("married", false);
editor.apply();
}
});
点击事件通过getSharedPreferences
方法指定SharedPreferences
的文件名为data
,并得到了SharedPreferences.Editor
对象。接着向这个对象添加了三条不同类型的数据,最后apply
提交。
运行之后,我们能发现,生成了一个data.xml
文件。
从SharedPreferences中读取数据
从SharedPreferences
中存储数据非常简单,读取数据也很简单。SharedPreferences
对象提供了一系列的get
方法,用于对存储的数据进行读取,每种get
方法都对应了SharedPreferences.Editor
中的一种put
方法。
get
方法接收两个参数,第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表示当传入的键找不到对于的值时会以什么样的默认值进行返回。
我们还是来实践一下。
在onCreate
方法中添加下面的代码,效果和之前相同。
SharedPreferences pref = getSharedPreferences("data", MODE_PRIVATE);
String inputText = pref.getString("input", "");
if (!TextUtils.isEmpty(inputText)){
edit.setText(inputText);
edit.setSelection(inputText.length());
Toast.makeText(this, "Restoring succeeded", Toast.LENGTH_LONG).show();
}
SQLite数据库存储
创建数据库
Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper
帮助类,借助这个类可以很简单地对数据库进行创建和升级。
首先,SQLiteOpenHelper
是一个抽象类,这意味着我们想要使用它的话就需要创建一个自己的帮助类来继承它。SQLiteOpenHelper
中有两个抽象方法,分别是onCreate
和onUpgrade
,我们必须在自己的帮助类里重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。
SQLiteOpenHelp
中还有两个非常重要的实例方法:getReadableDatabase
和getWritableDatabase
。这两个方法都可以创建或者打开一个现有的数据库,并返回一个可对数据库进行读写的对象。不同的是,当数据库不可写入的时候,getReadableDatabase
方法返回的对象会用只读的方式打开数据库,而getWritableDatabase
会出现异常。
SQLiteOpenHelper
中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。这个构造方法中接收4个参数,第一个是Context
,第二个是数据库名,第三个参数允许我们在查询数据的时候返回一个自定义的Cursor
,一般都是传入null
,第四个参数表示当前数据库的版本号,可以用于升级数据库。
构建数据库实例后,就可以调用getReadableDatabase
和getWritableDatabase
创建数据库了。数据库文件会存放在/data/data/
目录下。此时,重写的onCreate
方法也会得到执行。所以通常会在这里处理一些创建表的逻辑。
接下来还是通过例子来直观地体会一下。
我们创建一个名为BookStore.db
的数据库,然后在这个数据库中新建一张Book
表,表中有id(主键)
、作者、价格、页数、书名等。
创建数据库表当然还是需要建表语句的。
create table Book(
id interger primary key autoincrement,
author text,
price real,
pages integer,
name text)
SQLite的数据类型很简单,integer
表示整型,real
表示浮点型,text
表示文本类型,blob
表示二进制类型。另外,上述建表语句中我们还使用了primary key
将id
设置为主键,并且用autocrement
关键字表示id
列是自增长的。
然后需要在代码中去执行这条SQL语句,才能完成创建表的操作。
新建一个MyDatabaseHelper
类。
public class MyDatebaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book("
+ "id integer primary key autoincrement, "
+ "author text, "
+ "price real, "
+ "name text)";
private Context mContext;
public MyDatebaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version){
super(context, name, factory, version);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db){
db.execSQL(CREATE_BOOK);
Toast.makeText(mContext, "Create succeed", Toast.LENGTH_LONG).show();
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
}
}
我们把建表语句定义成一个字符串常量,然后在onCreate
方法中有调用了SQLiteDatabase
的execSQL
方法去执行这条建表语句,并弹出一个Toast
提示创建成功,这样就可以保证在数据库创建完成的时候还能同时创建Book
表。
再修改一下activity_main.xml
。
添加了一个用于建库的按钮。
最后修改一下主活动。
dbHelper = new MyDatebaseHelper(this, "BookStore.db", null, 1);
Button button_createdb = (Button) findViewById(R.id.create_Database);
button_createdb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dbHelper.getWritableDatabase();
}
});
至此,建库的代码就完成了。
建库之后,如果才能证实已经创建成功?如果还是使用File Explorer
,最多只能看到database
目录下多了一个文件,Book
表是无法通过File Explorer
看到的。因此这次我们准备换一种查看方式,用adb shell
来对数据库和表的创建情况进行检查。
关于
adb shell
的内容下次补全。
升级数据库
之前我们重写了一个空方法onUpgrade
方法是用于对数据库进行升级的,它在整个数据库的管理工作中起着非常重要的作用。
目前DatabaseTest
项目中已经有了一张Book
表用于存放书的各种详细数据,如果我们想要再添加一张Catagory
表用于记录图书的分类,怎么做呢?
Category
表中有id(主键)
、分类名和分类代码这几列,那么建表语句就可以写成:
create table Category(
id integer primary key autoincrement,
category_name text,
category_code integer)
接下来我们将这条建表语句添加到MyDatabaseHelper
中,然后修改一下onUpgrade
方法。
public static final String CREATE_CATEGORY = "create table Category ("
+ "id integer primary key autoincrement, "
+ "category_name text, "
+ "category_code integer)";
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
db.execSQL("drop table if exists Book");
db.execSQL("drop teble if exists Category");
onCreate(db);
}
当我们调用onUpgrade
方法时,如果Book
和Category
已经存在了,就会将原先存在的表删除,然后在onCreate
方法中创建一个新的表。
然后修改一下主活动中创建表格的语句,就可以调用onUpgrade
方法了。
dbHelper = new MyDatebaseHelper(
this, "BookStore.db", null, 2
);
添加数据
接下来学习一下如何对表中的数据进行操作。
其实我们能对数据进行的操作无非四种,CRUD
。即添加、查询、更新、删除。每种操作又对于了一种SQL命令,添加insert
,查询select
,更新update
,删除delete
。Android为了照顾开发者的水平,提供了一系列的辅助性方法,使得在Android中即使不编写SQL语句,也能轻松完成所有CRUD操作。
前面我们知道,getReadableDatabase
和getWriteableDatabase
方法是可以用于创建和升级数据库的,不仅如此,两个方法还都会返回一个SQLiteDatabase
对象,借助这个对象就可以对数据进行操作了。
首先是添加数据。
SQLiteDatabase
提供了一个insert
方法。它接收三个参数,第一个是表名,第二个用于在未指定添加数据的情况下给某些可为空的列自动赋值为null,第三个是一个ContentValues
对象,它提供了一系列的put
方法重载,用于向ContentValues
中添加数据,只需要将表中的每个列名已经相应的数据传入即可。
我们来实践一下。
首先我在主活动的布局中添加了一个按钮,然后在主活动中给按钮添加了一个监听器。
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", "The Da Vinci Code");
values.put("author", "Dan Brown");
values.put("pages", 454);
values.put("price", 16.96);
db.insert("Book", null, values);
values.clear();
values.put("name", "The Lost Symbol");
values.put("author", "Dan Brown");
values.put("pages", 510);
values.put("price", 19.95);
db.insert("Book", null, values)
}
});
我们赋值的时候对id这列没有赋值,因为在创建表的时候,已经将主键设置为自增长了。
更新数据
SQLiteDatabase
中同样提供了一个很好的updata
方法,用于对数据进行更新,这个方法接受4个参数,第一个参数同样是表名,第二个参数是ContentValues
对象,第三个、第四个参数用于约束更新某一行或者某几行的数据,不指定的话就是默认更新所有行。
Button updataData = (Button) findViewById(R.id.update_data);
updataData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("price", 10.99);
db.update("Book", values, "name = ?", new String[]{ "The Da Vinci Code" });
}
});
删除数据
依然是用SQLiteDatabase
中的delete
方法。方法接受三个参数,第一个是表名,第二、三个用于约束删除某一行的数据。
Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete("Book", "pages > ?", new String[] {"500"});
}
});
查询数据
查询数据是CRUD中最复杂的一种操作。
SQLiteDatabase
中有一个query
方法用于对数据进行查询,这个方法的参数十分复杂,最短的一个方法重载也需要传入七个参数。
第一个参数自然还是表名,第二个参数用于指定查询哪几列,三四个参数用于约束查询某一行或者某几行的数据,第五个参数用于指定需要去group by
的列,第六个参数用于对group by
之后的数据进一步过滤,第七个参数用于指定查询结果的排序方式。
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if(cursor.moveToFirst()){
do {
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("DatabaseTest", "book name is "+name);
Log.d("Main.Activity", "book author is"+author);
Log.d("Main Activity", "book pages is "+pages);
Log.d("Main Activity", "book price is"+price);
}while (cursor.moveToNext());
}
cursor.close();
}
});
使用SQL操作数据库
Android提供了很多方便的API来操作数据库,不过也提供了直接使用SQL来操作数据库的方法。只要使用execSQL
方法,其中传入你需要的SQL语句即可。