考虑下面几种情况:
在用户使用应用时,上述情况经常会出现,此时就要求应用具有保存这些数据并在以后读取的能力,也就是所谓的数据持久化。
Android系统为开发人员提供了三种实现数据持久化的方式,分别是保存到SharedPreferences、保存到SQLite数据库以及保存到文件。
使用SharedPreference是保存较小的键值对的最佳方法。每个SharedPreference对象指向一个保存键值对的shared preference文件,并为读写不同类型的键值对提供了方便的方法。
Context类提供了创建或访问shared preferences文件的方法:
mode一般都设置成Context.MODE_PRIVATE,表示只有创建该文件的应用才能访问它。‘
获得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文件中的键值对非常容易,只要先获得SharedPreferences对象,之后调用各类getXXX()方法获取键值对即可。示例如下:
SharedPreferences preferences = getPreferences(Context.MODE_PRIVATE);
int highScore = preferences.getInt("HighScore",0);
需要注意的一点是,所有的get方法都需要传入一个默认值,它会在找不到相应值的时候作为结果返回。
对于较大量的、结构化的数据,通常的做法是将它们保存到数据库中。Android系统提供了对SQLite数据库的支持。
创建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类可以这么解读:
注意:Age类实现了BaseColumns接口,这使得它能够继承到一个名为_ID的主键,这是让数据库和Android系统中的一些框架(如Cursor)相容的关键。
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()等,可以在需要时选择性地实现。
当使用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语句的组织方式大致相同,如下:
方法返回一个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的File对象:
下面尝试打印这两个目录的地址:
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是手机提供的公用存储区域,未root的手机打开文件管理等应用,看到的目录就是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的形式存储起来。Environment类提供了访问公用存储区域的File对象的方法:
下面做一个简单测试:
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及以上版本,这两个权限被视作危险权限,只能够在运行时动态获取。
有些文件虽然被保存到了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