Android 系统主要提供3种方式去实现数据的持久化功能,即

  • 文件存储
  • SharePreferences 存储
  • 数据库存储

1 文件存储

文件存储是 Android 中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件当中,因而它比较 适合用于存储一些简单的文本数据或二进制数据

1.1 存储数据到文件中

Context 类中提供 openFileOutput 方法,可以用于将数据存储到指定的文件中,该方法接受两个参数,分别是:

  • 文件名,指定的文件名不能包含路径,因为所有的文件都是默认存储到 /data/data/<package name>/files/ 目录下
  • 第二个参数是文件操作模式,主要有两个模式分别是 MODE_PRIVATEMODE_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 类的例子代码如下,注意重写的 onCreateonUpgrade 方法:

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() 方法可以获取数据库对象 SQLiteDatabaseSQLiteDatabase 提供了一些内置方法用于增删改查数据库,不过习惯上使用 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 类型的默认值为 0boolean 类型的默认值为 false

4.4 删除数据

使用 LitePal 删除数据的方式也与更新数据的方式相类似,第一种就是直接调用 已存储对象delete() 方法,或者调用 DataSupportdeleteAll() 静态方法,该方法的例子代码如下:

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");