最近翻译了一篇Android Developer上的文章,文章的原路径是Storage Options。这篇文章说的是Android中的存储选择。
Android为我们提供了几种存储稳固的应用数据的选择。你选择的方法取决于你的具体的需求,例如数据是否应该是对于当前应用是私有的或者对于其他的应用是可访问的,还有你的数据需要多大的空间。
你的数据存储方式如下:
- Shared Preferences:
用key-value的方式存储基本的私有数据。
- Internal Storage:(内部存储)
在设备内存上存储私有数据。
- External Storage:(外部存储)
在扩展存储上存储共有的数据。
- SQLite Databases:
在私有数据库上存储有组织的数据。
- Network Connection:
使用你自己的网络服务器再往上存储数据。
Android提供了一种方式去向其他的Android应用暴露你的数据甚至包括私有数据,也就是content provider。content provider是一个可选择的控件,它暴露了你的应用数据的读或者写的权限,受限于任何你想要加的限制。如果你想了解更多关于 content provider的信息,去看content provider的文档。
Using Shared Preferences
SharedPreferences类提供了一个框架,它允许你去存储和取回以key-value格式的数据。你可以使用SharedPreferences去存储任何基本数据类型:booleans, floats, ints, longs, 和strings。这个数据将会持续用户周期(即使你的应用已经被杀死了)。
Shared Preferences并不是严格地用来存储用户偏好的,例如用户选择了什么铃声。如果你对于为你的应用创建用户偏好(user preferences)感兴趣,看PreferenceActivity,它提供了一个Activity框架去创建user preferences,它将自动使用Shared Preferences。
你可以使用以下的方法去为你的应用获取一个SharedPreferences对象:
getSharedPreferences():如果你需要多个由名字区分的preferences的文件,你用第一个参数来区分这些文件,你可以使用这个方法。
getPreferences():如果你只需要为你的Activity提供一个preferences文件,你可以使用这个方法。因为这仅仅是一个针对你的Activity的 preferences 文件,你不必提供一个名字来区分。
来写值:
1. 调用edit()去获取SharedPreferences.Editor。
2. 使用这些方法putBoolean()等来添加值。
3. 用commit()来提交这些新的值。
用getBoolean()等方法来读取存储的值。
这是一个在一个计算器里为键盘按键存储preference的例子:
public class Calc extends Activity {
public static final String PREFS_NAME = "MyPrefsFile";
@Override
protected void onCreate(Bundle state){
super.onCreate(state);
. . .
// Restore preferences
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
boolean silent = settings.getBoolean("silentMode", false);
setSilent(silent);
}
@Override
protected void onStop(){
super.onStop();
// We need an Editor object to make preference changes.
// All objects are from android.context.Context
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("silentMode", mSilentMode);
// Commit the edits!
editor.commit();
}
}
使用内部存储
你可以在设备的内部存储上直接存放文件。默认情况下,存储到内部存储的文件对于你的应用是私有的并且其他的应用不能获取到它们。当用户卸载了你的应用时,这些文件也同时被移除了。
为了创建和写入一个私有的文件到内部存储:
1. 调用openFileOutput()方法,它会返回一个FileOutputStream。
2. 用write()去写到文件里。
3. 用close()去关闭我们的流(stream)。
例如:
String FILENAME = "hello_file";
String string = "hello world!";
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();
MODE_PRIVATE 将会创建文件(或者替代重名的文件)并且使他对于你的应用来说是私有的。
其他可以选择的模式是:MODE_APPEND, MODE_WORLD_READABLE, 和MODE_WORLD_WRITEABLE。
从内部存储中读取一个文件:
1. 调用openFileInput(),它返回了FileInputStream。
2. 用read()从文件中读取字节。
3. 用close()关闭文件流。
Tip:如果你想在编译的时候去在你的应用里存储一个文件,存储你的文件在这个项目目录:res/raw/。你可以用openRawResource()打开它,传递参数 R.raw.<文件名> 。这种方式返回InputStream,你可以用这个流去读这个文件。(但是你不能对最初的文件进行写操作)
存储缓存文件
如果你想缓存一些数据,而不是一直存储它们,你可以使用getCacheDir()去打开一个文件,这个文件指的是你的应用存储临时缓存文件的内部目录。
当设备的内部存储空间不足时,Android有可能删除这些缓存文件来释放空间。但是,你不应该依赖系统去清理这些缓存文件。你应该自己去控制缓存文件的添加和删除并且保持它们占据的空间在一个合理的范围内(例如1MB)。当用户卸载你的应用时,这些文件将被移除。
其它有用的方法
getFilesDir():
获取你的内部文件被存储的文件系统的目录的绝对路径。
getDir():
创建(或者打开一个已经存在的)一个内部存储空间的目录。
deleteFile():
删除内部存储里的一个文件。
fileList():
返回目前被你的应用存储的文件的一个列表。
使用扩展存储
每部Android设备都会支持用来存储文件的外部存储。外部存储可以是一个可以移除的媒介(例如SD卡)或者一个内部存储(不可移除的)。存储在外存的文件是全局可读的并且可以被允许USB mass 存储的用户所修改。
注意:如果用户在电脑上增加了外部存储或者移除了媒介,外部存储将会变得不可用。并且你存储到外存的文件也会没有安全保证。所有的应用都可以读写放置在外存的文件并且他们也可以删除它们。
获取外部存储的权限
为了在外部存储器上读写文件,你的应用必须获取READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE系统权限。例如:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
如果你想同时读写文件,你只需要添加WRITE_EXTERNAL_STORAGE权限,因为它已经包括了读权限。
提示:从Android 4.4版本开始,如果你仅仅读写你应用的私有文件,这些允许并不是必须的。更多的内容,见saving files that are app-private。
检查媒介的可用性
在你操作外存之前,你应该调用getExternalStorageState()去检查当前媒介(外部存储目录)是否可用。这个媒介将会被添加到电脑,丢失,只读,或者在其它的状态。例如,下面的方法你可以用来检测存储介质的可用性:
/* 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;
}
getExternalStorageState()方法会返回其它的状态,例如这个介质是否在被共享(连接到了电脑),丢失了,被错误地移除等等。当你的应用需要外部存储的权限的时候,你可以使用这些返回值去告诉用户目前的外部存储的状态。
存储可以与其它应用共享的文件
通常地,用户可以通过你的APP得到的新的文件应该存储在一个公用的地方,在这个公用的地方其它的APP可以访问他们并且用户可以从设备中轻易的拷贝它们。当这么做的时候,你应该使用共享的公有目录之一,例如:Music/, Pictures/, and Ringtones/。
调用getExternalStoragePublicDirectory()方法,通过传递某种你需要的目录的类型,例如DIRECTORY_MUSIC, DIRECTORY_PICTURES, DIRECTORY_RINGTONES(铃声目录)等,去获取相应公有目录里的文件。通过存储你的文件到相应的目录中,系统的扫描器将会对你的文件进行分类。(例如,铃声将在系统中被设置为铃声的目录,而不是音乐)
例如,这里是一个在公有的照片目录为新的相册创建目录的方法:
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;
}
把你的文件藏起来放置文件扫描仪把它扫描出来(隐藏自己的文件)
你可以在你的外存目录下包含一个名叫 .nomedia 的空文件(在文件名里注意点的前缀)。这将放置Android系统去获取到你的文件并且通过MediaStore content provider把它们提供给其它的应用。但是,如果你的文件的确对于你的应用是私有的,你应该把它们存储在app的私有目录(save them in an app-private directory)。
存储应用的私有文件
如果你想处理并不想给其它应用使用的文件(例如仅仅由你的应用使用的图片或者音效),你应该通过调用getExternalFilesDir()在外部存储中去使用私有的存储目录。这个方法同样需要一个参数去区分子目录的类型(例如DIRECTORY_MOVIES)。如果你不需要一个特殊的外接媒介目录,传递null去过去你的私有应用目录的根目录。
从Android 4.4版本开始,在你的app 私有目录读写文件不再需要READ_EXTERNAL_STORAGE 或WRITE_EXTERNAL_STORAGE权限。所以你只用在更低的Android版本通过添加maxSdkVersion属性去声明这个权限,如下:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
...
</manifest>
注意:当用户卸载了你的应用时,这个目录以及它的内容都将被删除。同样地,系统扫描将不会读取这些目录下的文件,所以MediaStore content provider将不会获取到它们的内容。同样地,你不应该使用这些目录来存储最后属于用户的文件,例如照片或者你购买的音乐,这些文件应该存放在公有目录里。
有些时候,有一部分内存被分配用作外部存储的Android设备可能仍会提供一个SD卡插槽。当在Android 4.3或者更低的版本时,getExternalFilesDir()方法仅仅提供了内部分配(内存)的权限并且你的app不能读写sd卡。但是,从Android 4.4版本开始,你可以通过调用返回文件数组的方法getExternalFilesDirs()去获得以上两个目录的权限。数组中的第一个入口是主要的外部存储,并且除非它满了或者是不可用,你都应该使用这个地址目录。如果你想在Android 4.3或以下的设备去同时获取以上这两个目录,你可以使用support library 里的静态方法ContextCompat.getExternalFilesDirs()。
这个方法同样包含一个文件数组,但是在Android 4.3或更低的版本上只有一个入口。
注意:尽管由getExternalFilesDir() 和getExternalFilesDirs()提供的目录不能被MediaStore content provider所访问,拥有READ_EXTERNAL_STORAGE权限的其他应用可以访问所有外部存储包括上面目录里的文件。如果你想完全限制你的文件的访问权限,你应该把你的文件写到内存存储中去。
存储缓存文件
调用getExternalCacheDir()方法去打开你认为在外存上应该去存储的缓存文件。如果用户卸载了你的应用,这些文件将会被自动删除。
与ContextCompat.getExternalFilesDirs()类似,我们在上面提过的,你同样可以通过调用
ContextCompat.getExternalCacheDirs()方法在二级外存目录(如果可访问)上去访问缓存目录。
小提示:为了保存你的文件空间和保证你应用的性能,细致地操作你的缓存文件并且在你的应用的生命周期中去删除无用的缓存是非常重要的。
使用数据库
Android 为SQLite数据库提供了完整的支持。任何你创建的数据库可以在你的应用里通过名字去获取,但是在应用外则不可以访问。
Android推荐的创建新的数据库的方法是创建一个SQLiteOpenHelper的子类并且重载onCreate()方法,在这个方法中你可以通过SQLite指令去创建数据库中的表。
例如:
public class DictionaryOpenHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 2;
private static final String DICTIONARY_TABLE_NAME = "dictionary";
private static final String DICTIONARY_TABLE_CREATE =
"CREATE TABLE " + DICTIONARY_TABLE_NAME + " (" +
KEY_WORD + " TEXT, " +
KEY_DEFINITION + " TEXT);";
DictionaryOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DICTIONARY_TABLE_CREATE);
}
}
你可以获取你的SQLiteOpenHelper的实例通过你定义的构造函数。分别调用getWritableDatabase() 和 getReadableDatabase() 去读写数据库。它们都返回了SQLiteDatabase对象,并且为我们的数据库操作提供了相应的方法。
你可以通过使用参数包括表名,选择条件,列,排序等的SQLiteDatabase query()方法去使用SQLite查询语句。对于像这些需要列别名的复杂的查询,你应该使用提供了很多便捷方法去创建查询语句的SQLiteQueryBuilder。
每个SQLite查询都会返回一个指向通过这次查询找到的所有行的Cursor。Cursor通常是一个帮助你来导航数据库查询结果的机制。
例如,你可以看Note Pad和Searchable Dictionary应用去看如何在Android中使用SQLite数据库。
数据库调试
Android SDK包含了一个sqlite3工具,它帮助我们去浏览表的内容,运行数据库指令,并且使用其它有用的数据库功能。看Examining sqlite3 databases from a remote shell如何去使用这个功能。
注意:Android没有为SQLite施加任何限制。我们推荐在你的表里应该包含一个自增长的值域,我们可以通过它去迅速定位一条记录。这对于私有数据并不是必须的,但是当你声明了一个content provider,你必须使用BaseColumns._ID常量去包含一个独一无二的ID。
使用网络连接
你可以使用网络(当它可用时)去存储和取数据。为了做网络操作,使用下面包里的类:
java.net.*
android.net.*