一. Getting Started
1.4 Saving Data
这里讲的是 Android 应用中主要的数据存储选项,包括:
- 保存简单数据类型的键值对()
- 在 Android 的文件系统中保存任意文件
- 使用 SQLite 管理的数据库
也就是常说的 SharedPreferences、文件储存、数据库储存,当然,Android 中还有更多数据存储方式比如网络储存。
1.4.1 SharedPreferences 储存
SharedPreferences 通常用来保存相对较小的键值集合,比如设置之类的。
获取 SharedPreferences 的句柄
调用以下两种方法可以创建新的 SharedPreferences 文件或访问现有的文件:
- getSharedPreference(String name, int mode) 第一个参数指的是文件名称,第二个为访问模式。使用该方法可以在应用中的任何 Context 调用此方法。
例如在 fragment 中访问 Activity 中的 SharedPreferences 文件:
Context context = getActivity();
SharedPreferences sharedPref = context.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE);
- getPreferences(int mode) 如果您只需 Activity 的一个共享首选项文件,可以使用该方法,会访问 Activity 默认的 SharedPreferences 文件。
只需访问 Activity 的一个 SharedPreferences 文件:
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
Tips: 如果您创建带 MODE_WORLD_READABLE 或 MODE_WORLD_WRITEABLE 的共享首选项文件,那么知道文件标识符的任何其他应用都可以访问您的数据。
写入SharedPreferences
向 SharedPreferences 文件中写入数据,需要先调用 SharedPreferences 的 edit() 方法来创建 SharedPreferences.Editor。
然后通过 Editor 来进行修改,最后调用 commit()
来保存更改。
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(getString(R.string.saved_high_score), newHighScore);
editor.commit();
从 SharedPreferences 读取信息
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);
1.4.2 文件储存
选择内部或外部存储
所有 Android 设备都有两个文件存储区域:“内部”和“外部”存储。所谓的内部储存也就是大多数设备提供的内置非易失性内存,外部储存一般指 SD 卡。
名称 | 内部存储 | 外部存储 |
---|---|---|
可用性 | 它始终可用。 | 它并非始终可用,因为用户可采用 USB 存储设备的形式装载外部存储,并在某些情况下会从设备中将其移除。 |
访问权限 | 只有您的应用可以访问此处保存的文件。 | 它是全局可读的,因此此处保存的文件可能不受您控制地被读取。 |
是否删除 | 当用户卸载您的应用时,系统会从内部存储中移除您的应用的所有文件。 | 当用户卸载您的应用时,只有在您通过 getExternalFilesDir() 将您的应用的文件保存在目录中时,系统才会从此处移除您的应用的文件。 |
App 默认安装在内部存储中,如果想让 App 默认安装在外部存储,可在 AndroidManifest.xml 中指定 android:installLocation 属性。
获取外部存储的权限
读取和写入外部存储权限:
<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...
</manifest>
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
如果您的应用使用 WRITE_EXTERNAL_STORAGE 权限,那么它也隐含读取外部存储的权限。
将文件保存在内部存储中
保存文件到内部存储中可以调用以下两种方法获取作为 File 的相应目录:
- getFilesDir():返回应用内部目录
- getCacheDir():返回应用的缓存文件目录,最好作好限制,如果系统内存不足可能会不进行警告的情况下删除您的缓存文件。
在 App 内部目录创建文件:
File file = new File(context.getFilesDir(), filename);
向文件内写入数据:
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();
}
创建缓存文件调用 createTempFile() 方法,以下是从 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 其他应用不能访问这些文件。
将文件保存在外部存储中
获取外部存储的可读性和可写性:
/* 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;
}
外部储存空间可以被用户和其他应用进行修改,但是仍然可以保存两类文件:
- 公共文件:供其它应用和用户自由使用,当用户卸载 App,这些文件依旧可以使用,例如照片等。
- 私有文件:属于 App 私有文件,在卸载时应该删除。虽然可以由其它应用访问(因为储存在了外部储存空间),但它们实际上不向您的应用之外的用户提供任何输出值。 当用户卸载您的应用时,系统会删除应用外部私有目录中的所有文件。
在外部储存设备上保存公共文件:getExternalStoragePublicDirectory() 方法
public File getAlbumStorageDir(String albumName) {
// 创建文件时指定文件类型
// DIRECTORY_PICTURES:图片 DIRECTORY_MUSIC:音乐 DIRECTORY_PICTURES:铃声
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
在外部储存设备上保存私有文件:getExternalFilesDir() 方法
通过这种方法创建的各个目录将添加至封装应用的所有外部存储文件的父目录,当用户卸载 App 时会删除这些文件。
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;
}
如果没有适合您文件的预定义子目录名称,您可以改为调用 getExternalFilesDir() 并传递 null
。这将返回外部存储上您的应用的专用目录的根目录。
getExternalFilesDir 返回的目录是:/storage/emulated/0/Android/data/com.sky.viewtest/files
切记,getExternalFilesDir() 在用户卸载您的应用时删除的目录内创建目录。如果您正保存的文件应在用户卸载您的应用后仍然可用—比如,当您的应用是照相机并且用户要保留照片时—您应改用 getExternalStoragePublicDirectory()。
无论您对于共享的文件使用 getExternalStoragePublicDirectory() 还是对您的应用专用文件使用 getExternalFilesDir(),您使用诸如 DIRECTORY_PICTURES 的 API 常数提供的目录名称非常重要。这些目录名称可确保系统正确处理文件。 例如,保存在 DIRECTORY_RINGTONES 中的文件由系统媒体扫描程序归类为铃声,而不是音乐。
查询可用空间
- 目前可用空间:getFreeSpace()
- 储存卷总空间:getTotalSpace()
但是要注意要保证有足够多的空间去保存文件,如果空间不足会引起 IOException,可以捕获这个异常来处理接下来的事情。
删除文件
删除文件普遍方法:
myFile.delete();
使用 Context 调用 deleteFile() 来定位和删除文件:
myContext.deleteFile(fileName);
注:当用户卸载您的应用时,Android 系统会删除以下各项:
- 您保存在内部存储中的所有文件
- 您使用 getExternalFilesDir() 保存在外部存储中的所有文件。
但是,您应手动删除使用 getCacheDir() 定期创建的所有缓存文件并且定期删除不再需要的其他文件。
1.4.3 在 SQL 数据库中保存数据
Android 数据库 API 所在包: android.database.sqlite。
定义架构和契约
SQL 数据库的主要原则之一是架构:数据库如何组织的正式声明。 架构体现于您用于创建数据库的 SQL 语句。您会发现它有助于创建伴随类,即契约类,其以一种系统性、自记录的方式明确指定您的架构布局。
契约类是用于定义 URI、表格和列名称的常数的容器。 契约类允许您跨同一软件包中的所有其他类使用相同的常数。 您可以在一个位置更改列名称并使其在您整个代码中传播。
组织契约类的一种良好方法是将对于您的整个数据库而言是全局性的定义放入类的根级别。 然后为枚举其列的每个表格创建内部类。
注:通过实现 BaseColumns 接口,您的内部类可继承名为
_ID
的主键字段,某些 Android 类(比如光标适配器)将需要内部类拥有该字段。 这并非必需项,但可帮助您的数据库与 Android 框架协调工作。
例如,该代码段定义了单个表格的表格名称和列名称:
public final class FeedReaderContract {
// To prevent someone from accidentally instantiating the contract class,
// make the constructor private.
private FeedReaderContract() {}
/* Inner class that defines the table contents */
public static class FeedEntry implements BaseColumns {
public static final String TABLE_NAME = "entry";
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_TITLE + TEXT_TYPE + COMMA_SEP +
FeedEntry.COLUMN_NAME_SUBTITLE + TEXT_TYPE + " )";
private static final String SQL_DELETE_ENTRIES =
"DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;
就像您在设备的内部存储中保存文件那样,Android 将您的数据库保存在私人磁盘空间,即关联的应用。 您的数据是安全的,因为在默认情况下,其他应用无法访问此区域。
SQLiteOpenHelper 类中有一组有用的 API。当您使用此类获取对您数据库的引用时,系统将只在需要之时而不是应用启动过程中执行可能长期运行的操作:创建和更新数据库。 您仅需调用 getWritableDatabase() 或 getReadableDatabase() 即可。
注:由于它们可能长期运行,因此请确保您在后台线程中调用 getWritableDatabase() 或 getReadableDatabase(),比如使用 AsyncTask 或 IntentService。
要使用 SQLiteOpenHelper,请创建一个替换 onCreate()、[onUpgrade()] 和[onOpen()] 回调方法的子类。您可能还希望实现 [onDowngrade()],但这并非必需操作。
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);
}
}
要访问您的数据库,请实例化 SQLiteOpenHelper 的子类:
FeedReaderDbHelper mDbHelper = new FeedReaderDbHelper(getContext());
将信息输入到数据库
通过将一个 ContentValues 对象传递至 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_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle);
// Insert the new row, returning the primary key value of the new row
long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);
insert() 的第一个参数即为表格名称。
从数据库读取信息
要从数据库中读取信息,请使用 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_SUBTITLE
};
// Filter results WHERE "title" = 'My Title'
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };
// How you want the results sorted in the resulting Cursor
String sortOrder =
FeedEntry.COLUMN_NAME_SUBTITLE + " 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 移动方法之一,您必须在开始读取值之前始终调用这些方法。 一般情况下,您应通过调用 moveToFirst() 开始,其将“读取位置”置于结果中的第一个条目中。 对于每一行,您可以通过调用 Cursor 获取方法之一读取列的值,比如 getString() 或 getLong()。对于每种获取方法,您必须传递所需列的索引位置,您可以通过调用getColumnIndex() 或 getColumnIndexOrThrow() 获取。例如:
cursor.moveToFirst();
long itemId = cursor.getLong(
cursor.getColumnIndexOrThrow(FeedEntry._ID)
);
从数据库删除信息
// Define 'where' part of query.
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
// Specify arguments in placeholder order.
String[] selectionArgs = { "MyTitle" };
// Issue SQL statement.
db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs);
更新数据库
当您需要修改数据库值的子集时,请使用 update() 方法。
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 title
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
String[] selectionArgs = { "MyTitle" };
int count = db.update(
FeedReaderDbHelper.FeedEntry.TABLE_NAME,
values,
selection,
selectionArgs);