很多Android应用都需要保存数据,哪怕仅仅是程序保存活动状态来防止程序暂停时信息丢失的保存。多数大型应用同样还需要保存用户的配置信息,还有一些应用需要通过文件和数据库来管理应用中大量的信息。本课程将向你讲述Android中数据存储的基本原则,包括:
1、通过Shared Preferences文件以键值对的方式来保存应用中一些简单的配置;
2、在Android文件系统保存任意文件;
3、通过Sqlite数据库管理应用配置。
通过键值对保存配置
如果你应用中只是些相对轻量级的用户配置,你可以通过SharedPreferences以键值对的方式来保存数据。SharedPreferences是一个通过提供简单的读写方法来管理这些键值对的类。每个SharedPreferences文件都是通过framework层来关闭,可以是私有的,也可以将其共享。
这次课程将向你讲述如何通过SharedPreferences API来存储和取回你应用中简单的数据信息。
注意:SharedPreferences只提供通过键值对来保存信息的方法。
获取SharedPrefereces的句柄
你可以通过如下两个方法中的某个方法来创建一个新的Shared preferences文件或者访问一个已经存在的文件。
1、getSharedPreferences()—如果你应用中有多个Shared Preferences文件需要保存,这个方法很适合你,第一个参数为你给这个文件指定的ID,你可以通过你应用的上下文来调用它;
2、getPreferences()—如果你的应用中只需要保存一个Shared Preferences文件,这个方法很适合你,由于整个应用就只有这么一个Shared Preferences文件,所以你不需要为其指定名称,系统在你创建时会为其默认一个名称的。
比如,这是在Fragment中执行的一段代码,这是一个以私有模式访问应用中一个id为R.string.preference_file_key的shared preferences实例。
Context context = getActivity();
SharedPreferences sharedPref = context.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE);
在你应用中的每个shared preferences的名称是独一无二的,你可以通过将应用包名反转的方式来为其命名,比如”com.example.myapp.PREFERENCE_FILE_KEY”.
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
注意:如果你以MODE_WORLD_READABLE或者MODE_WORLD_WRITEABLE方式来创建shared preferences文件的话,系统中其它的应用也是可以访问它的。
保存Shared Preferences配置信息
为了能够成功地通过Shared Preferences文件来保存配置信息,你需要通过调用SharedPreferences的edit()方法来创建一个SharedPreferences.Editor实例。
然后通过putInt()、putString()等方法以键值对的方式来传递你需要保存的信息,最后记得调用commit()方法提交传递的数据,比如:
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(getString(R.string.saved_high_score), newHighScore);
editor.commit();
从Shared Preferences中读取配置信息
你需要通过调用SharedPreferences的getInt()、getString()等方法来取出之前保存到应用中的信息,你还可以在这些方法的第二个参数中指定一个默认值来避免由于系统还没保存配置而导致的获取信息异常的情况,比如:
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
int defaultValue = getResources().getInteger(R.string.saved_high_score_default);
long highScore = sharedPref.getInt(getString(R.string.saved_high_score), defaultValue);
保存文件
Android是和其它基于磁盘文件系统的平台一样来使用文件系统的,本课程将向你讲述如何通过File API来对Android系统进行读写操作。
一个File实例适合于按照顺序流的方式来读写大量数据,比如,保存图片或者任何与网络交互的操作。
本课将向你描述如何在你的app中通过基于文件操作的方式来保存和恢复应用中的数据,本课假设你已经熟悉Linux的文件系统并且对java的输入/输出接口有所了解。
选择内部或者外部存储设备
所有的Android设备都有两个存储区域,内部和外部存储,它们的命名来源于早起的Android,它和搭载其它系统设备中的内置非易失性存储设备(内存)加上一个比如SD卡的可移动存储媒体(外存)是一个道理。但是很多设备都将内置存储设备氛围两块(内存和外存),所以在这些设备中即使没有可移动存储媒体,设备中同样存储两个存储空间,并且有API来决定到底是外存还是可移动存储设备,下面列举了每种存储设备的特征:
内部存储设备:
1、它永远可用;
2、只有你自己的应用才可以在这里保存信息;
3、当用户卸载掉你的应用时,系统会把与你应用相关的文件移除。
当你希望所有的应用都可以访问你的应用数据信息时,内存是一个非常适于保存信息的地方。
外部存储设备:
1、它并不是一直可用的,当用户通过USB将设备的文件系统挂载或者是从设备中移除后,它将处于不可用状态;
2、任何人都可以访问它,所以保存在该设备上的数据是不可控的;
3、只有将数据保存在通过getExternalFileDir()获取应用路径的目录下,才会在应用被卸载时自动删除,其它路径下的数据在卸载应用时系统是不会自动将其删除的。
外部存储设备非常适合于存储那些允许任何人访问的数据。
小提示:虽然应用默认是安装在内部存储设备中,但是你可以在AndroidManifest.xml清单文件中通过androidLinstallLocation属性来指定你应用的安装位置。多数人通常会在应用比较大并且系统挂在了外部存储设备的情况下如该种操作。
获取外部存储的权限
你必须在清单文件中申请到”WRITE_EXTERNAL_STORAGE”权限,你才可以往外部存储设备中写数据:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
注意:虽然现在所有的应用都可以在不申明文件访问权限的情况下去读取外部存储设备的数据,但是这将在今后的Android API版本中有所改变,有可能需要你申请外部存储设备访问权限后你才可以读取外设中的数据,所以为了能够让你的应用保持灵活性,最好还是在清单文件中加入”READ_EXTERNAL_STORAGE”文件访问权限:
<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...
</manifest>
然而,当你在清单文件中申请了外部存储设备写权限后,你的应用同样可以在不申请读权限的情况下去访问外存中的数据。
如果你是将数据保存在内存中,你不需要申请任何与文件相关的权限,你的应用默认状态下就有内部存储设备访问权限。
在内部存储设备中保存文件
当你在内部存储设备中保存数据时,你可以通过File类中如下的某个方法来访问相应的目录:
1、getFilesDir()
返回你应用的文件目录路径;
2、getCacheDir()
返回你应用的缓存目录路径,记得在缓存不需要使用时将其删掉以免占用内存,并且为缓存指定一个固定大小的容量(比如1MB)。当系统内存不足时,系统会在不报警告的情况下删掉应用中的缓存文件。
你可以通过File的文件读写方法在上面介绍的两个目录中为应用保存数据,比如:
File file = new File(context.getFilesDir(), filename);
你可以调用File类的openFileOutput()方法来获取一个输出流,并在内部存储目录中保存属于,比如,下例所示为如何通过输出流来保存一些文本:
String filename = "myfile";
String string = "Hello world!";
FileOutputStream outputStream;
try {
outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
outputStream.write(string.getBytes());
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
亦或是你需要缓存某些文件,这个时候你就可以用creteTempFile()方法来达到目标了。比如,下面的方法是从URL中提取文件名,然后在内存缓存目录中创建以这个名字为文件名的文件:
public File getTempFile(Context context, String url) {
File file;
try {
String fileName = Uri.parse(url).getLastPathSegment();
file = File.createTempFile(fileName, null, context.getCacheDir());
catch (IOException e) {
// Error while creating file
}
return file;
}
注意:你应用的内存存储目录是根据应用的包名来指定的,从技术角度来讲,如果在你将这些文件设置为可读后,其它应用是可以无障碍访问的,但前提是其它应用要知道你应用的报名和文件名。上述的情况只有在你专门指定文件可读和可写后才会出现,所以如果你是不希望你保存的文本被其它应用访问,建议在你应用的内部存储文件设置为MODE_PRIVATE。
在外部存储设备中保存文件
因为外部存储设备可能会由于通过USB将文件系统挂在在PC端或者是被移除当前设备。所以在你使用外部存储设备之前记得检查其是否可用。你可以通过getExternalStorageState()方法来查询外部存储设备的状态,如果返回值为MEDIA_MOUNTED,你就可以像使用内部存储设备一样使用它了。比如,你可以按照下面所提供的方法来判断外设:
/* 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;
}
尽管外部存储设备中的数据可以被用户或者其它应用修改,但它同样有两种类型的文件可以保持:
公共文件
可以被任何人或者任何程序访问,当用户卸载掉你的应用后,这些文件依然存在。比如,通过你应用拍摄的照片或者下载的文件。
私有文件
只属于你应用的文件,系统会在你应用被卸载掉后将其删除。从技术角度上来讲用户和其它应用也是可以访问这些文件的,比如通过你应用下载的资源文件或者是一些临时媒体文件。
如果你想在外部存储设备中保存公用文件,通过getExternalStoragePublicDirectory()来获取外部存储设备的文件实例,这个方法带有一个设置文件类型的参数,比如DIRECTORY_MUSIC或者DIRECTORY_PICTURES,比如:
public File getAlbumStorageDir(String albumName) {
// Get the directory for the user's public pictures directory.
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
如果你想再外部存储设备中保存你应用的私有文件,你可以通过getExternalFilesDir()来获取应用的外部存储目录,这些被创建的文件会在你应用被卸载后自动删除。
比如,下面的方法可用于创建一个相册目录:
public File getAlbumStorageDir(Context context, String albumName) {
// Get the directory for the app's private pictures directory.
File file = new File(context.getExternalFilesDir(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
如果外设中没有你之前定义的子目录,你可以通过调用带null参数的getExternalFilesDir()方法来返回应用的外设根目录。
查询剩余空间
如果你能预先知道你需要保存多少数据,你可以通过调用getFreeSpace或者getTotalSpace方法来避免在程序运行时出现IOException。它们分别用于返回设备的可用空间和总空间,通过这种方式可以避免文件操作时出现异常。
然而,当存储设备剩余空间比你需要保存数据需要的空间少或者是设备已用空间超过总存储容量的90%时,系统将不会执行程序指定的文件操作。
注意:并不是在操作文件时必须要做这种处理,你也可以通过正确地处理代码来避免有可能出现的文件操作异常。
删除一个文件
当一个文件不再需要时你应该将其删除。删文件最直接的方式是调用文件句柄的delete()方法。
myFile.delete();
如果文件是保存在内部存储设置被,你同样可以通过应用上下文来执行文件删除操作。
myContext.deleteFile(fileName);
注意:在你应用被卸载时,系统会删除如下文件:
1、所有保存在内部存储设备的文件;
2、所有保存在路径为getExternalFilesDir()下的文件。
记住,你应该手动删除应用中的缓存文件。
保存数据到数据库中
对于重复或者结构性强的数据,将其保存到数据库中是比较理想的做法,比如联系人信息。在开始本课程之前,假定你有一定的SQLite数据库基础,在Android上使用数据库,你需要使用到Android提供的android.database.sqlite包。
定义一个构架和契约
SQL数据库中的主要原则之一就是构架(schema):一个关于这个数据库是如何组织的一个正式的声明。构架反映在创建SQL数据库的语句中。你可能会发现可以很容易地创建一个同伴类(companion class),同伴类也就是我们所说的合约类(contract class),其中明确规定了你的构架的布局,以一种系统且自说明的方式。
一个合约类(contract class)是定义URI常量、表名和列名的容器,合约类允许你在同一个包的其他类中使用这些常量。这就允许了你在一个地方改变列名,而同时把它传播到代码的其它地方去。组织合约类的一个好方法是:把对于你的整个数据库来说是全局的那些定义放在类的根级上;然后对于每一个表(table)创建一个内部类,列举其列。
注意:通过实现BaseColumns 接口,你的内部类要继承一个基本的关键字域叫做_ID,一些Android的类比如cursor adaptors将需要它。
例如,下面的代码段定义了一个表的表名和列名:
public final class FeedReaderContract {
// To prevent someone from accidentally instantiating the contract class,
// give it an empty constructor.
public FeedReaderContract() {}
/* Inner class that defines the table contents */
public static abstract class FeedEntry implements BaseColumns {
public static final String TABLE_NAME = "entry";
public static final String COLUMN_NAME_ENTRY_ID = "entryid";
public static final String COLUMN_NAME_TITLE = "title";
public static final String COLUMN_NAME_SUBTITLE = "subtitle";
...
}
}
使用SQL帮助类创建一个数据库
一旦你定义好你的数据库结构后,你就应该实现一些方法来创建和组织数据库和表。下面是一些典型的创建和删除表的代码段:
private static final String TEXT_TYPE = " TEXT";
private static final String COMMA_SEP = ",";
private static final String SQL_CREATE_ENTRIES =
"CREATE TABLE " + FeedEntry.TABLE_NAME + " (" +
FeedEntry._ID + " INTEGER PRIMARY KEY," +
FeedEntry.COLUMN_NAME_ENTRY_ID + TEXT_TYPE + COMMA_SEP +
FeedEntry.COLUMN_NAME_TITLE + TEXT_TYPE + COMMA_SEP +
... // Any other options for the CREATE command
" )";
private static final String SQL_DELETE_ENTRIES =
"DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;
和你将文本保存到内置存储设备一样,Android将你应用的数据库文件也是保存到你应用的私有磁盘空间里的,这样就可以保证你的数据是安全的,因为默认情况下其它应用是不可以访问这个路径的。
关于数据库有一个非常有用的帮助类SQLiteOpenHelper,当你使用这个类来获取你数据库的引用时,系统仅在需要时执行可能长时间的操作:创建和更新数据库,而不是在应用启动的时候执行。你需要做的仅仅是调用 getWritableDatabase()或getReadableDatabase()方法。
注意:因为它是长时间处于运行状态的,请确保你在后台线程里面调用getWritableDatabase()或者getReadableDatabase()。比如通过同步AsyncTask或者IntentServidce。
为了在操作数据库的时候使用SQLiteOpenHelper,你需要创建一个复写onCreate()、onUpgrade()和onOpen()回调方法的Helper之类。你或许系统也实现onDowngrade()回调,但这不是必须的。
下面是一个复写SQLiteOpenHelper类的具体实现:
public class FeedReaderDbHelper 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 = "FeedReader.db";
public FeedReaderDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_ENTRIES);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// This database is only a cache for online data, so its upgrade policy is
// to simply to discard the data and start over
db.execSQL(SQL_DELETE_ENTRIES);
onCreate(db);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
}
你需要实例化这个类才能正常地通过它访问数据库:
FeedReaderDbHelper mDbHelper = new FeedReaderDbHelper(getContext());
将信息存入数据库中
通过ContentValues对象将需要插入的数据组织,然后调用SQLiteDatabase的insert方法就可以将数据插入到数据库中了:
// Gets the data repository in write mode
SQLiteDatabase db = mDbHelper.getWritableDatabase();
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_ENTRY_ID, id);
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_CONTENT, content);
// Insert the new row, returning the primary key value of the new row
long newRowId;
newRowId = db.insert(
FeedEntry.TABLE_NAME,
FeedEntry.COLUMN_NAME_NULLABLE,
values);
Insert()方法的第一个参数是表名,第二个参数是列名,可以为空。
从数据库中读取信息
使用SQLiteDatabase的query()方法可以从数据库中读取信息,你要做的就是向其传递选择标准和想要的列。这个方法结合了insert()和update()方法的元素,只不过它的列的表指定了你想取出的数据,而不是插入的数据。查询到的结果将以游标Cursor对象返回给你:
SQLiteDatabase db = mDbHelper.getReadableDatabase();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
FeedEntry._ID,
FeedEntry.COLUMN_NAME_TITLE,
FeedEntry.COLUMN_NAME_UPDATED,
...
};
// How you want the results sorted in the resulting Cursor
String sortOrder =
FeedEntry.COLUMN_NAME_UPDATED + " DESC";
Cursor c = db.query(
FeedEntry.TABLE_NAME, // The table to query
projection, // The columns to return
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
sortOrder // The sort order
);
想要看一个Cursor中的一行,可以使用 Cursor 类中的各种move方法中的一个,当你开始读取值时你必须先调用它。一般情况下,你应该先调用moveToFirst()方法,它将把读取位置指向结果中的第一项。对每一行,你可以通过Cursor类的get方法读取各列内容,比如 getString()或 getLong()。对每一个get方法,你必须指定你想要的列的索引,你可以通过 getColumnIndex() 或 getColumnIndexOrThrow()方法来获得索引。比如:
cursor.moveToFirst();
long itemId = cursor.getLong(
cursor.getColumnIndexOrThrow(FeedEntry._ID)
);
从数据库中删除信息
要删除表中的行,你需要提供一定的选择标准来确定要删除的行。数据库的API提供了一种机制,用于创建选择标准,防止SQL注入,该机制将选择标准分为一个选择从句和一些选择参数。这个从句定义了需要查看的列,也允许你结合列的测试。这些参数是和从句绑定的需要测试的值。因为结果不像常规的SQL语句那样处理,所以它是防SQL注入的。
// Define 'where' part of query.
String selection = FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
// Specify arguments in placeholder order.
String[] selectionArgs = { String.valueOf(rowId) };
// Issue SQL statement.
db.delete(table_name, selection, selectionArgs);
更新数据库
当你需要修改数据库中的某一项信息时,你需要使用SQLiteDatabase的update()方法。
更新数据表结合了insert()方法中的content values语义和 delete()方法中的where语义。
SQLiteDatabase db = mDbHelper.getReadableDatabase();
// New value for one column
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
// Which row to update, based on the ID
String selection = FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selectionArgs = { String.valueOf(rowId) };
int count = db.update(
FeedReaderDbHelper.FeedEntry.TABLE_NAME,
values,
selection,
selectionArgs);
原文链接:Saving Data