Android实现数据持久化的三种方法

考虑下面几种情况:

  • 用户对应用的亮度、音量、字体颜色等进行了配置;
  • 用户在游戏过程中打出了新的记录;
  • 用户使用应用下载了一些图片、音乐等大文件;

在用户使用应用时,上述情况经常会出现,此时就要求应用具有保存这些数据并在以后读取的能力,也就是所谓的数据持久化。
Android系统为开发人员提供了三种实现数据持久化的方式,分别是保存到SharedPreferences、保存到SQLite数据库以及保存到文件。

保存到SharedPreferences

使用SharedPreference是保存较小的键值对的最佳方法。每个SharedPreference对象指向一个保存键值对的shared preference文件,并为读写不同类型的键值对提供了方便的方法。

获取SharedPreferences对象

Context类提供了创建或访问shared preferences文件的方法:

  • getSharedPreferences(String name, int mode):当需要多个shared preferences文件时使用。通过name指定shared preferences文件的名称。
  • getPreferences(int mode):当只需一个shared preferences文件时,可以使用这个方法获得默认的shared preferences文件。

mode一般都设置成Context.MODE_PRIVATE,表示只有创建该文件的应用才能访问它。‘

向shared preferences文件中写入

获得SharedPreferences对象后,就可以对其进行写入了。一般步骤如下:
1. 调用SharedPreferences的实例方法edit()创建一个SharedPreferences.Editor对象;
2. 通过Editor对象的putXXX()方法传递键值对;
3. 调用Editor对象的apply()方法(在后台进行数据存储)或者commit()方法(阻塞当前线程并立即进行数据存储)提交。

示例如下:

SharedPreferences preferences = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putInt("HighScore",100);
editor.apply();

从shared preferences文件中读取

读取shared preferences文件中的键值对非常容易,只要先获得SharedPreferences对象,之后调用各类getXXX()方法获取键值对即可。示例如下:

SharedPreferences preferences = getPreferences(Context.MODE_PRIVATE);
int highScore = preferences.getInt("HighScore",0);

需要注意的一点是,所有的get方法都需要传入一个默认值,它会在找不到相应值的时候作为结果返回。

保存到SQLite数据库

对于较大量的、结构化的数据,通常的做法是将它们保存到数据库中。Android系统提供了对SQLite数据库的支持。

创建Contract类

创建Contract类这个步骤不是绝对必须的,但它是Android类库中组织数据库结构的一个规范做法:每个Contract类代表一个数据库,其中的每一个内部类代表一张表。下面是一个例子:

public static final class MyDbContract {
    private MyDbContract(){

    }

    public static final class Age implements BaseColumns {
        private Age(){

        }

        public static final String TABLE_NAME = "NameToAge";
        public static final String COLUMN_NAME_USER_NAME = "name";
        public static final String COLUMN_NAME_USER_AGE = "age";
    }
}

上面的Contract类可以这么解读:

  • 数据库名称为MyDb;
  • 有一张名为NameToAge的表,表由name列和age列组成。

注意:Age类实现了BaseColumns接口,这使得它能够继承到一个名为_ID的主键,这是让数据库和Android系统中的一些框架(如Cursor)相容的关键。

实现SQLiteOpenHelper类

SQLiteOpenHelper是Android系统提供的用于管理数据库的创建与访问的类。通过这个类,Android系统会将数据库保存到应用的私密空间,保证数据不会被其他应用访问到。
下面是实现SQLiteOpenHelper的一个例子:

public class MyDbHelper extends SQLiteOpenHelper {
    // If you change the database schema, you must increment the database version.
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "MyDb.db";

    private static final String SQL_CREATE_TABLE_AGE =
            "CREATE TABLE " + MyDbContract.Age.TABLE_NAME + " ("
            + MyDbContract.Age._ID + " INTEGER PRIMARY KEY" + ","
            + MyDbContract.Age.COLUMN_NAME_USER_NAME + " TEXT" + ","
            + MyDbContract.Age.COLUMN_NAME_USER_AGE + " INTEGER"
            + ")";

    public MyDbHelper(Context context){
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_TABLE_AGE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

分别说明一下构造器以及各个回调方法:
(1)构造器:构造器中调用了超类的构造器SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version),参数分别为用于创建数据库的context对象、数据库名、用于加工Cursor对象的CursorFactory、数据库版本(一般从1开始)。
(2)onCreate():当首次创建数据库时调用。一般会在这里面创建各个表。在上面的例子中,通过原始SQL语句CREATE TABLE创建了一张表。
(3)onUpgrade():当数据库版本升级时调用。
其他还有一些回调方法,如onDowngrade()等,可以在需要时选择性地实现。

获取SQLiteDatabase实例

当使用SQLiteOpenHelper做一些与数据库有关的操作时,系统会对那些有可能比较耗时的操作(例如创建与更新等)在真正需要的时候才去执行,而不是在app刚启动的时候就执行。
对于开发者而言,需要做的仅仅是执行SQLiteOpenHelper的getWritableDatabase()或者getReadableDatabase()方法获得SQLiteDatabase实例。需要注意的是,由于创建与访问数据库可能会很耗时,因此最好在后台线程中执行这些操作。

添加数据

通过向SQLiteDatabase的insert()方法传递ContentValues对象实现:

private void put(SQLiteDatabase database,String name, int age){
    ContentValues values = new ContentValues();
    values.put(MyDbHelper.MyDbContract.Age.COLUMN_NAME_USER_NAME,name);
    values.put(MyDbHelper.MyDbContract.Age.COLUMN_NAME_USER_AGE,age);

    database.insert(
            MyDbHelper.MyDbContract.Age.TABLE_NAME,
            null,
            values
    );
}

insert()方法的第一个参数是将要插入的表的名称,第二个参数是当values为空时需要被置为Null的列的名称(SQLite不允许插入所有列都为空的行)。

查询数据

通过SQLiteDatabase的query(String table, String[] columns, String selection,
String[] selectionArgs, String groupBy, String having,
String orderBy)方法实现:

Cursor cursor = database.query(
        MyDbHelper.MyDbContract.Age.TABLE_NAME,
        projections,
        null,
        null,
        null,
        null,
        MyDbHelper.MyDbContract.Age._ID + " ASC"
);

query()的参数和原始SQL语句的组织方式大致相同,如下:

  • table:表名,对应SQL中的FROM子句;
  • columns:需要返回的列名,对应SQL中的SELECT子句。
  • selection:选取条件,对应SQL中的WHERE子句。语句中可以使用”?”通配符,具体的值会用下面的selectionArgs中的元素代替。
  • selectionArgs:选取条件参数,用于填充selection中的“?”。举例:假设selection == “name = ?”,selectionArgs == {“Bob” },那么最后selection与selectionArgs拼成的WHERE子句为WHERE name = Bob。
  • groupBy:分组,对应SQL中的GROUP BY子句。
  • having:过滤组,对应SQL中的HAVING子句。
  • orderBy:排序,对应SQL中的ORDER BY子句。
  • limit(可选):返回行数,对应SQL中的LIMIT子句。

方法返回一个Cursor对象,关于Cursor对象的使用可以查阅其文档,这里是一个示例:

while (cursor.moveToNext()){
    Log.i(
            cursor.getString(cursor.getColumnIndex(MyDbHelper.MyDbContract.Age.COLUMN_NAME_USER_NAME)),
            String.valueOf(cursor.getInt(cursor.getColumnIndex(MyDbHelper.MyDbContract.Age.COLUMN_NAME_USER_AGE))));
}

删除数据

通过SQLiteDatabase的delete(String table, String whereClause, String[] whereArgs)方法实现。

更新数据

通过SQLiteDatabase的update(String table, ContentValues values, String whereClause, String[] whereArgs)方法实现。

保存到文件

对于用户下载的图片、音乐等,将其保存到文件中储存起来是最合适的方法。

保存到internal storage

internal storage是每个应用专属的一块存储空间,相当于应用的根目录,对于应用来说总是可用的。当用户卸载应用时,internal storage中的数据会全部被清除掉。默认情况下,无论是用户还是其他应用都无法访问该区域。
可以通过以下两个方法获取指向internal storage的File对象:

  • Context的实例方法getFilesDir(),指向应用的internal目录。
  • Context的实例方法getCacheDir(),指向应用的internal缓存目录。当系统空间不足时,会自动清除该目录下的文件,因此该目录下只应当存储较小的缓存文件。

下面尝试打印这两个目录的地址:

Log.i("path",getFilesDir().toString());
Log.i("path",getCacheDir().toString());

结果为:
10-03 10:38:03.329 26289-26289/com.example.swt369.filepathtest I/path: /data/user/0/com.example.swt369.filepathtest/files
10-03 10:38:03.329 26289-26289/com.example.swt369.filepathtest I/path: /data/user/0/com.example.swt369.filepathtest/cache

此外,Context还提供了一个获取指定internal目录下的文件的FileOutputStream的方法openFileOutput(String name, int mode)。mode一般为Context.MODE_PRIVATE。
想要快速创建缓存文件的话,可以利用File的静态方法createTempFile(String prefix, String suffix, File directory)

保存到external storage

external是手机提供的公用存储区域,未root的手机打开文件管理等应用,看到的目录就是external storage的根目录。应用如果选择将数据保存在这里面,那么这些数据对于用户和其他应用而言都是可见的。

检验external storage是否可用

external storage可能是不可用的(某些手机可能没有内置的SD卡),因此在访问之前需要检验其可用性,方法如下:

 /* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

保存public文件

应用为用户下载的音乐、视频等,即使应用被卸载了也应当被保留,此时就应当将它们以public的形式存储起来。Environment类提供了访问公用存储区域的File对象的方法:

  • Environment.getExternalStorageDirectory():返回指向公用存储区域根目录的File对象。
  • getExternalStoragePublicDirectory(String type):根据type返回指向不同类型的公用存储区域目录的File对象。Environment提供了一组type常量,有DIRECTORY_MUSIC,DIRECTORY_DOWNLOADS,DIRECTORY_DOCUMENTS等。

下面做一个简单测试:

Log.i("path",Environment.getExternalStorageDirectory().toString());
Log.i("path",Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString());

结果为:
10-03 11:06:41.384 7364-7364/? I/path: /storage/emulated/0
10-03 11:06:41.385 7364-7364/? I/path: /storage/emulated/0/Download

注意:访问这块区域是需要获取android.permission.READ_EXTERNAL_STORAGE或者android.permission.WRITE_EXTERNAL_STORAGE权限的。当获取到写权限时,也会同时获取到读权限。在Android 6.0及以上版本,这两个权限被视作危险权限,只能够在运行时动态获取。

保存private文件

有些文件虽然被保存到了external storage中,但是它可能只会被创建它的应用使用,比如QQ、微信缓存的聊天表情、小视频等。Android系统为这样的文件提供了一个存储目录:公用存储区域根目录/Android/data/应用包名/files。这个目录除了外部可访问外,和internal storage的性质非常相似:卸载时会被清除、(自Android 4.4)不需要申请访问权限(仅针对与应用包名匹配的那个目录,如果访问其他应用的目录还是需要申请权限)
可以利用Context的实例方法getExternalFilesDir(@Nullable String type)获取指向该目录的File对象。type常量同样由Environment类提供。示例如下:

Log.i("path",getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString());

结果为:
10-03 11:14:57.947 9412-9412/com.example.swt369.filepathtest I/path: /storage/emulated/0/Android/data/com.example.swt369.filepathtest/files/Download

你可能感兴趣的:(Android,知识储备)