Android 系统主要提供3种方式去实现数据的持久化功能,即
- 文件存储
- SharePreferences 存储
- 数据库存储
1 文件存储
文件存储是 Android 中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件当中,因而它比较 适合用于存储一些简单的文本数据或二进制数据。
1.1 存储数据到文件中
Context
类中提供 openFileOutput
方法,可以用于将数据存储到指定的文件中,该方法接受两个参数,分别是:
- 文件名,指定的文件名不能包含路径,因为所有的文件都是默认存储到
/data/data/<package name>/files/
目录下 - 第二个参数是文件操作模式,主要有两个模式分别是
MODE_PRIVATE
和MODE_APPEND
。其中MODE_PRIVATE
模式时默认的操作模式,表示当指定的文件名在目录下已存在时,就覆盖它。而MODE_APPEND
则表示当指定的文件名在目录下已存在时,就往该文件里 追加 内容。
openFileOutput
方法返回的是 FileOutputStream
对象,得到该对象后就可以使用 Java 流的方式将数据写入文件中了,如:
private void saveDate(String data) {
FileOutputStream out = null;
BufferedWriter writer = null;
try {
// 通过 openFileOutput 获取写入流
out = openFileOutput("inputData", MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
writer.write(data);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
finally {
try {
if (writer != null) {
writer.close();
}
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
写入数据后,使用 Android Studio 的 DeviceFileExplorer
工具,即可查看 /data/data/<package name>/files
目录里是否出现相应的数据文件。
1.2 从文件中读取数据
Context
类中还提供了一个 openFileInput
方法,用于从文件中读取数据,它只接受一个参数,即要读取文件的文件名,然后系统会自动到 /data/data/<package name>/files
目录下加载这个文件,并返回一个 FileInputSteam
对象,此时可以通过 Java 流的方式将其读取出来。
代码如下:
private StringBuilder getData(String fileName) {
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder result = new StringBuilder();
try {
in = openFileInput(fileName);
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while ((line = reader.readLine()) != null) {
result.append(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
finally {
if (reader != null) {
try {
reader.close();
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
2 SharedPreferences 存储
不同于文件的存储方式,SharedPreferences 是使用 键值对 的方式来存储数据的,同时 SharedPreferences 还支持多种不同的数据类型存储,即如果存储的数据类型是整型,那么读取数据的时候依旧为整型,SharedPreferences
文件是使用 XML 文件来对数据进行管理。
2.1 存储数据到 SharedPreferences 中
要使用 SharedPreferences,就需要先获取 SharedPreferences
对象,这里主要有三种方法用于得到 SharedPreferences
对象。
Context
类中的getSharedPreferences
方法,如同上述的openFileOutput
方法,getSharedPreferences
方法同样接收两个参数,第一个参数用于指定SharedPreferences
文件的名称,如果指定的文件不存在则会创建一个,而SharePreferences
文件都是存放在/data/data/<package name>/shared_prefs/
目录下,第二个参数用于指定操作模式,默认的只有MODE_PRIVATE
这个模式可选,其它的都已经废弃了。Activity
类中的getPreferences
方法很相似,不过它只接受一个操作模式参数,因为它自动的将当前活动的类名作为SharedPreferences
的文件名。PreferenceManager
类中的getDefualtSharedPreferences
方法,这是一个静态方法,接受一个Context
参数,并自动使用 当前程序的包名作为前缀 命名SharePreferences
文件。
获取到 SharePreferences
对象后,就可以开始存储数据了,主要可以分为三步实现:
- 使用
SharedPreferences
对象的edit
方法获取SharedPreferences.Editor
对象 - 向
SharedPreferences.Editor
对象中添加数据,比如putBoolean
方法用于添加一个布尔型类型的数据 - 调用
apply
方法将添加的数据提交,从而完成数据存储操作
例子如下:
button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取 SharedPreferences 对象
SharedPreferences sharedPreferences = getSharedPreferences("data", MODE_PRIVATE);
// 获取 SharedPreferences.Editor 对象
SharedPreferences.Editor editor = sharedPreferences.edit();
// 存储参数
editor.putString("testKey", "HelloWorld");
// 提交申请
editor.apply();
}
});
在调用
apply
方法之前,调用SharedPreferences.Eidtor
对象的clear
方法,可以清除已经插入的数据
2.2 从 SharedPreferences 中读取数据
读取 SharedPreferences 数据很简单,就是先获取对应的 SharedPreferences
对象,最后通过调用诸如 getString
方法来获取,这些方法都具有两个参数,第一个为键值名称,第二个为默认值,如:
button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取 SharedPreferences 对象
SharedPreferences sharedPreferences = getSharedPreferences("data", MODE_PRIVATE);
String data = sharedPreferences.getString("testKey", "");
Toast.makeText(MainActivity.this, data, Toast.LENGTH_LONG).show();
}
});
3 SQLite 数据库
Android 系统有内置数据库 SQLite,SQLite 是一款轻量级的关系型数据库,它支持标准的 SQL 语法,同时无需设置用户和密码就可以使用。
SQLite 数据库的数据类型只有以下几种:
integer
:整型real
:浮点text
:文本类型blob
:二进制类型
3.1 创建数据库
Android 提供了 SQLiteOpenHelper
类,使用这个类可以对数据库进行创建和升级。
SQLiteOpenHelper
是一个抽象类,即想要使用它就需要创建一个类去继承它,它有两个抽象方法,分别是:
onCreate()
:用于创建数据库,当调用实例方法getWritabeDatabase
或者getReadableDatabase
方法查询到没有对应名的数据库时就会执行这个方法onUpgrade()
:用于升级数据库
SQLiteOpenHelper
类有两个构造方法可供重写,一般使用以下的构造方法:
public SQLiteOpenHelper(Context context, String nameOfDataSet, Cursor cursor, int version)
其中:
context
:上下文nameOfDataSet
:数据库名cursor
:允许在查询数据的时候返回一个自定义的Cursor
,一般传入null
version
:当前数据库的版本号,可用于对数据库进行升级操作
构建出 SQLiteOpenHelper
实例后,再调用它的实例方法 getReadbleDatabase()
或 getWritableDatabase()
即可创建数据库了:
getReadbleDatabase()
:创建或打开一个现有数据库,并返回一个可对数据库进行读写操作的对象getWritableDatabase()
:创建或打开一个现有数据库,并返回一个可对数据库进行读写操作的对象
二者不同在于,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()
方法返回的对象将以只读的方式去打开数据库,而 getWritableDatabase()
方法则抛出异常。
数据库文件会存放在
/data/data/<package name>/databases/
目录下
继承 SQLiteOpenHelper
类的例子代码如下,注意重写的 onCreate
和 onUpgrade
方法:
public class MyDatabaseHelper extends SQLiteOpenHelper {
private Context myContext;
// primary key 主键
// autoincrement 自增长
private String CREATE_MsgOfStudent = "create table MsgOfStudent ("
+ "id integer primary key autoincrement, "
+ "name text,"
+ "age integer)";
/**
* 构造函数
* @param context 上下文
* @param name 数据库名
* @param factory
* @param version 数据库版本号
*/
public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
myContext = context;
}
/**
* 创建数据库,当调用实例方法 getWritabeDatabase 或者 getReadableDatabase 方法查询到没有对应名的数据库时就会执行这个方法
* @param db 数据库实例
*/
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_MsgOfStudent);
Toast.makeText(myContext, "初始化数据库结构", Toast.LENGTH_LONG).show();
}
/**
* 升级数据库,当数据库的版本号更新比原来的大时,就会执行这个方法
* @param db 数据库实例
* @param oldVersion 旧版本号
* @param newVersion 新版本号
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 删除旧数据库格式
db.execSQL("drop table if exists MsgOfStudent");
// 重新执行创建数据库
onCreate(db);
}
}
3.2 增删改查操作
在实例化 SQLiteHelper
对象之后,通过调用 getReadableDatabase()
方法或者 getWritableDatadase()
方法可以获取数据库对象 SQLiteDatabase
,SQLiteDatabase
提供了一些内置方法用于增删改查数据库,不过习惯上使用 SQL
语句,所以就不做多介绍。
添加数据的方法如下:
db.execSQL("insert into MsgOfStudent (name, age) valuse (?, ?)", new String[] {"seiei", "20"});
更新数据的方法如下:
db.execSQL("update MsgOfStudent set age = ? whrer name = ?", new String[] {"18", "seiei"});
删除数据的方法如下:
db.execSQL("delete from MsgOfStudent whrer name = ?", new String[] {"seiei"});
查询数据的方法如下:
db.rawQuery("select * from MsgOfStudent");
查询方法返回的是一个 Cursor
对象,通过调用 Cursor
对象的一些方法获得真实的数据,例子代码如下:
MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(this, "seiei.db", null, 2);
// 获取数据库实例
SQLiteDatabase db = myDatabaseHelper.getReadableDatabase();
// 获取 Cursor 对象
Cursor cursor = db.rawQuery("select * from MsgOfStudent", null);
// 调用 Cursor 对象的 moveToFirst 方法可以将数据的指针移到第一行
if (cursor.moveToFirst()) {
// 使用 do while 历遍 Cursor
do {
String name = cursor.getString(cursor.getColumnIndex("name"));
int age = cursor.getInt(cursor.getColumnIndex("age"));
Log.d("读取数据库信息", "学生信息:姓名是 " + name + ",年龄是 " + age + "。");
} while (cursor.moveToNext());
}
// 关闭 Cursor
cursor.close();
4 LitePal
LitePal 是一款开源的 Android 数据库框架,它采用对象关系映射 ORM 的模式,在 app/build.gradle
文件中添加对应引用即可。如:
implementation 'org.litepal.android:core:1.4.1'
接下来就需要配置 litepal.xml
文件,该文件需要在 app/src/main/assets
目录下,具体内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<!-- litepa 配置文件 -->
<litepal>
<!-- 指定数据库名称,不用添加后缀 .db -->
<dbname value="seiei"></dbname>
<!-- 数据库版本,想要更新数据库,设置版本号加一即可,无需做其它操作 -->
<version value="2"></version>
<list>
</list>
</litepal>
同时还需要 AndroidManifest.xml
文件中配置 Android.name
属性,如:
...
<!-- 设置 android:name,这样 LitePal 的所有功能才能正常工作 -->
<application
android:name="org.litepal.LitePalApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
</application>
此时调用
LitePal
的静态方法getDatabase
可以获取到对应的SQLiteDatabase
类的实例对象
4.1 创建和升级数据库
LitePal 采用的是对象关系映射 ORM 模式,即只需创建对象而不需要再和 SQL 语句打交道了。这里创建映射关系也很简单,只需要在 litepal.xml
文件中设置 <mapping>
标签声明要配置的映射模型类,litepal.xml
文件配置如下:
<?xml version="1.0" encoding="utf-8" ?>
<!-- litepa 配置文件 -->
<litepal>
<!-- 数据库名称,不用添加后缀 .db -->
<dbname value="seiei"></dbname>
<!-- 数据库版本,想要更新数据库,设置版本号加一即可,无需做其它操作 -->
<version value="2"></version>
<list>
<!-- 配置映射模型类 -->
<mapping class="top.seiei.aboutbroadcast.bean.MsgOfStudent"></mapping>
</list>
</litepal>
而映射模型类的创建也很简单,就如同普通的 Java bean,只不过还需要继承 DataSupport
类,如:
public class MsgOfStudent extends DataSupport {
private int id;
private int age;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
4.2 添加数据
注意在进行数据库操作的时候,一定要让映射模型类继承 DataSupport
类。
添加数据到数据库只需要将配置好了的映射模型类实例调用 save
方法即可,如:
MsgOfStudent msgOfStudent = new MsgOfStudent();
msgOfStudent.setName("Nemo");
msgOfStudent.setAge(18);
msgOfStudent.save();
4.3 更新数据
LitePal 的更新数据,就需要获取相应的 已存储的对象。所有在学习更新数据就需要先了解 已存储的对象 的概念,对于 LitePal 来说,对象是否已存储就是根据调用 model.isSaved()
方法的结果来判断,而实际上只有两种情况下,model.isSaved()
方法会返回 true
,分别是:
- 该
model
对象已经调用过save()
方法添加过数据 - 该
model
对象是通过 LitePal 提供的查询 API 得到的
在获取了相应的已存储对象之后,重新设置需要修改的属性再调用 save()
方法即可。
4.3.1 updateAll
这种更新方法只能对已存储的对象进行操作,这里还有另一种灵巧的更新方式 model
对象的 updateAll()
方法,在 updateAll
方法中可以指定一个条件约束,如果不指定条件就表示更新所有数据,例子代码:
MsgOfStudent msgOfStudent = new MsgOfStudent();
// 更新数据
msgOfStudent.setAge(25);
msgOfStudent.updateAll("name = ? and classId = ?", "Taka", "3");
调用 updateAll
这里要注意一点,就是默认值的问题,比如以上面的代码为例,msgOfStudent
并没有设置 name
属性,即它的 name
属性默认值为 null
,从而调用 updateAll
方法之后,就不会更新该数据的 name
,但假如当前就需要更新 name
属性为 null
需要这么做呢?
此时就需要使用 setToDefault
方法,例子代码如下:
MsgOfStudent msgOfStudent = new MsgOfStudent();
// 更新数据
msgOfStudent.setToDefault("age"); // 表示将年龄设置为 0
msgOfStudent.updateAll("name = ? and classId = ?", "Taka", "3");
int
类型的默认值为0
,boolean
类型的默认值为false
4.4 删除数据
使用 LitePal 删除数据的方式也与更新数据的方式相类似,第一种就是直接调用 已存储对象 的 delete()
方法,或者调用 DataSupport
的 deleteAll()
静态方法,该方法的例子代码如下:
DataSupport.deleteAll(MsgOfStudent.class, "name = ?", "Taka");
该方法的第一个参数就是指定删除哪个表中的数据,后面的参数用于指定约束条件。
4.5 查询数据
LitePal 提供很多查询 API,以下举例说明
查询所有数据 findAll
:
List<MsgOfStudent> students = DataSupport.findAll(MsgOfStudent.class);
指定只查询某些属性的数据 select
:
List<MsgOfStudent> students = DataSupport.select("name", "age").find(MsgOfStudent.class);
指定查询的约束条件 where
:
List<MsgOfStudent> students = DataSupport.where("age > ?", "15").finde(MsgOfStudent.class);
指定排序方式 order
:
List<MsgOfStudent> students = DataSupport.order("price desc").find(MsgOfStudent.class);
指定查询结果的数量 limit
,下面代码只查询三条数据:
List<MsgOfStudent> students = DataSupport.limit(3).find(MsgOfStudent.class);
指定查询结果的偏移量 offset
,比如下面代码表示只查询表中的第2,3,4条数据
List<MsgOfStudent> students = DataSupport.limit(3).offset(1).find(MsgOfStudent.class);
同时,LitePal 也支持使用原生的 SQL 语句查询数据,返回一个 Cursor
对象,此时就需要像之前一样对 Cursor
对象进行操作获取具体信息,例子如下:
Cursor cursor = DataSupprot.findeBySQL("select * from MsgOfStudent");