Saving Data
Android有三种主要的保存数据的方法
* 使用键值对将简单数据类型的数据保存在shared preference文件中
* 将随意的文件保存在Android的文件系统中
* 通过SQLite来进行数据库管理
Saving Key-Value Sets
如果你有相对较小的键值对集合想要保存,你应该使用SharedPreferences APIs。每一个SharedPreferences 文件都有文件框架进行管理,并且文件可以是私有的也可以是公开的。
**Note:**SharedPreferences APIs只是用来读取和写入键值对,请不要与Preferences APIs 混淆了,它是用来帮助你为你的app构建一个用户界面的。
Get a Handle to a SharedPreferences
你可以通过下面两个方法中的一个创建一个新的shared preference文件或访问一个现有的文件:
* getSharedPreferences():如果你需要多个shared preference文件,你可以通过指定name去获取,这个name在第一个参数中指定。你可以在app中任何context中调用这个方法。
* getPreferences():如果你只需要一个shared preference文件,你可以在activity中使用这个方法。因为这个方法恢复一个属于这个activity的默认shared preference文件,所以你不要指定name参数。
例如,下面的代码在一个Fragment中执行。它通过指定一个资源字符串R.string.preference_file_key来访问shared preference,并且使用private mode,这样这个文件就只能被你的app访问了。
Context context = getActivity();
SharedPreferences sharedPref = context.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE);
在为你的shared preference文件起名字时,你需要在你的app中使用一个独一无二的名字,比如”com.example.myapp.PREFERENCE_FILE_KEY”
另外,如果你只需要一个shared preference文件,你可以使用getPreferences()方法:
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
Caution:如果你在创建一个shared preference文件时使用了参数 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE,那么其他的app知道了这个文件的标识符也可以访问这个文件。
Write to Shared Preferences
为了将数据写入shared preferences,需要创建一个SharedPreferences.Editor通过调用SharedPreferences中的edit()方法。
通过例如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();
Read from Shared Preferences
为了从一个shared preferences文件中取回值,可以调用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);
Saving Files
File对象适合从开始到结束没有跳过地读取或写入大量的数据。比如图像文件或通过网络交换的任何内容。
Choose Internal or External Storage
所有的Android设备都有两块文件存储区域:“内部”和“外部”存储。
以下列表汇总了各存储空间的实际信息:
* 内部存储:
* 它始终可用。
* 保存在此处的文件只有你的应用可以访问。
* 当用户卸载你的应用时,系统会从内部存储中删除你的应用的所有文件。
当你希望确保用户和其他应用不会访问你的文件时,内部存储是最佳选择。
* 外部存储:
* 它并非始终可用。
* 它是全局可读的,因此此处保存的文件可能不受你的控制被读取。
* 当用户卸载你的应用时,只有你是通过getExternalFilesDir()将你的应用的文件保存在目录中,系统才会删除此处保存的文件。
当不需要访问限制以及你希望与其他应用或允许用户使用计算机访问时,外部存储是最佳选择。
Tip:尽管应用默认安装在内部存储中,但你可以在manifest文件中指定android:installLocation属性来将你的应用安装到外部存储中。当APK非常大时且它们的外部存储大于内部存储时,用户更青睐这个选择。
Obtain Permissions for External Storage
要向外部存储写入文件,你必须在manifest文件中请求WRITE_EXTERNAL_STORAGE权限。
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
Caution:目前所有的应用都可以读取外部存储,而无需特别权限。但在未来的版本中会得到改进。如果你的应用需要读取外部存储(而不是写入),你将需要声明READ_EXTERNAL_STORAGE权限。要确保你的应用继续正常工作,你需要在更改生效前声明此权限。
<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...
</manifest>
你无需任何权限,即可在内部存储中保存文件,你的应用始终具有在其内部存储目录中读写的权限。
Save a File on Internal Storage
在内部存储中保存文件时,你可以通过调用以下两种方法之一获取作为File的相应目录:
- getFilesDir():返回表示你的应用的内部目录的File。
- getCacheDir():返回表示你的应用的临时缓存文件的目录的File。务必删除所有不再需要的文件并对在指定时间您使用的内存量实现合理大小限制,比如,1MB。 如果在系统即将耗尽存储,它会在不进行警告的情况下删除您的缓存文件。
要在这些目录之一中新建文件,你可以使用File的构造函数,传递指定您的内部存储目录的上述方法之一所提供的 File。例如:
File file = new File(context.getFilesDir(), filename);
另外你可以调用openFileOutput()方法去获取一个FileOutputStream将文件写入你的内部存储目录。例如:
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()方法,例如:
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;
}
Note:在Android的文件系统中,你的应用的内部存储目录是由你的应用的包名所指定的。从技术上来讲,如果你将文件模式设置为可读,其他的应用也可以访问你的外部存储中的文件。但是,其他的应用也需要知道你的包名和文件名。除非你显式的设置了文件时可读可写的,其他的应用是无法访问你的应用的内部存储目录的。所有只要你的应用的内部存储的文件使用了MODE_PRIVATE,其他的应用将无法访问。
Save a File on External Storage
因为外部存储可能不可用,所以你需要在访问之前确认外部存储是可用的。你可以通过调用getExterStorageState()方法来查询外部存储的状态。如果返回的状态是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;
}
即使外部存储是可以被用户和其他应用修改的,你也可以将两类文件保存在这里:
- Public files
可以被其他应用和用户自由访问的文件。当用户卸载应用后,用户仍然能够访问这些文件。
- Private files
文件只属于你的应用,当应用被卸载时,文件也将被删除。
如果你想要在外部存储中保存Public files,可以使用getExternalStoragePublicDirectory()方法去获取一个表示外部存储相应目录的File。这个方法用一个参数指定你想要保存文件的类型,这样它们就能够有逻辑地和其他Public files组织在一起。比如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;
}
如果你想要你的应用保存Private files,你可以通过调用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;
}
如果没有适合您文件的预定义子目录名称,您可以改为调用 getExternalFilesDir()
并传递 null。这将返回外部存储上您的应用的专用目录的根目录。
切记,getExternalFilesDir()在用户卸载您的应用时删除的目录内创建目录。如果您正保存的文件应在用户卸载您的应用后仍然可用—比如,当您的应用是照相机并且用户要保留照片时—您应改用 getExternalStoragePublicDirectory()。
无论您对于共享的文件使用 {@linkandroid.os.Environment#getExternalStoragePublicDirectory getExternalStoragePublicDirectory()} 还是对您的应用专用文件使用getExternalFilesDir(),您使用诸如 DIRECTORY_PICTURES
的 API 常数提供的目录名称非常重要。这些目录名称可确保系统正确处理文件。 例如,保存在 DIRECTORY_RINGTONES中的文件由系统媒体扫描程序归类为铃声,而不是音乐。
查询可用空间
如果您事先知道您将保存的数据量,您可以查出是否有足够的可用空间,而无需调用getFreeSpace()或getTotalSpace()引起IOException。这些方法分别提供目前的可用空间和存储卷中的总空间。此信息也可用来避免填充存储卷以致超出特定阈值。
但是,系统并不保证您可以写入与getFreeSpace()指示的一样多的字节。如果返回的数字比您要保存的数据大小大出几 MB,或如果文件系统所占空间不到 90%,则可安全继续操作。否则,您可能不应写入存储。
Note:保存您的文件之前,您无需检查可用空间量。 您可以尝试立刻写入文件,然后在 IOException出现时将其捕获。 如果您不知道所需的确切空间量,您可能需要这样做。 例如,如果在保存文件之前通过将 PNG 图像转换成 JPEG 更改了文件的编码,您事先将不知道文件的大小。
删除文件
您应始终删除不再需要的文件。删除文件最直接的方法是让打开的文件参考自行调用delete()。
myFile.delete();
如果文件保存在内部存储中,你还可以请求context通过调用deleteFile()来定位和删除文件:
myContext.deleteFile(fileName);
Note:当用户卸载您的应用时,Android 系统会删除以下各项:
* 您保存在内部存储中的所有文件
* 您可以使用getExternalFilesDir()保存外部存储中的所有文件。
但是,您应手动删除使用getCacheDir()定期创建的所有缓存文件并且定期删除不在需要的文件。
Saving Data in SQL
对于重复使用的或结构化的数据,将数据保存到一个数据库是非常理想的,比如联系人信息。在Android上,你将需要一些API来操作数据库,它们都可以在android.database.sqlite中找到。
Define a Schema and Contract
SQL最重要的一个准则就是模式:关于如何组织数据库的正式声明。模式在你创建数据库的语句中体现。你也许会发现它非常适合创建一个同伴类,比如contract类,这个类会通过一种系统的,自文档描述的方式显式地制定模式的布局。
contract类是定义URI名字,表名,列名常量的容器。contract类允许你在同一个包的其他类中使用相同的常量。这使得你可以改动一处的一个列名,代码中相同的列名都被改变。
组织一个contract类最好的方式是将数据库的全局定义放在类的根级别,然后为枚举其列的每个表创建一个内部类。
Note:通过实现BaseColumns接口,内部类可以继承到一个叫作_ID的主键字段。一些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";
}
}
Create a Database Using a SQL Helper
一旦你定义了数据库的外观,你应该实现创建和维护数据库和表的方法。这里是创建和删除一个表的典型语句:
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()。
使用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()):
Put Information into a Database
通过传递一个Content Values对象到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()的第一个参数就是表名。
第二个参数告诉框架在Content Values为空时要做什么。如果你指定了列名,框架会插入一行并且将该列的值设为null;如果你指定了null,框架将不会在没有值时插入一行。
Read Information form 啊Database
使用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())从Cursor的每一行读取到每一列的值。你必须传递你所需列的索引位置,你可以通过getColumnIndex()和getColumnIndexOrThrow()获取。例如:
cursor.moveToFirst();
long itemId = cursor.getLong(
cursor.getColumnIndexOrThrow(FeedEntry._ID)
);
Delete Information form a Database
从表中删除行,你需要提供识别行的选择条件。数据库API提供了一种创建选择条件的机制,用来防止SQL注入。该机制将选择条件划分为选择子句和选择参数。该子句定义要查看的列,并允许你合并列测试。参数是根据绑定到子句的项进行测试的值。因为结果不像常规SQL那样处理,所以它不受SQL注入的影响。
// 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 a Database
当你想修改数据库一个子集的值时,可以使用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 title
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
String[] selectionArgs = { "MyTitle" };
int count = db.update(
FeedReaderDbHelper.FeedEntry.TABLE_NAME,
values,
selection,
selectionArgs);