【翻译】Android 数据存储
之前闲着无聊,研究了下 Android 存储方面的知识,顺便翻译了下官方文档(虽然有已经被翻译过...)。这里就算是水一篇博客好了 # ̄▽ ̄#
1.1 应用数据存储
Android为你提供了一些用来持久保存应用数据的选择。你所选择的解决方案依赖于你明确的需求,例如数据对于你的应用来说是否是私有的,还是能被其他应用(以及用户)所获取,以及你的数据需要多大的存储空间。
你的数据存储方式为以下几种:
(1) Shared Preferences 共享偏好
以键值对的方式存储私有的简单的数据
(2) Internal storage 内部存储空间
存储私有数据在设备的内存上
(3) External storage 外部存储空间
存储公共数据在共享的外部存储上
(4) SQLite Databases 数据库
存储结构化数据在私有的数据库中
(5) Network Connection网络连接
在网络上用你自己的网络服务器存储数据
Android为你提供了一种为其它应用程序暴露私有数据的方式来——使用 content provider . (内容共享)。content provider 是一种可选的组件,它为那些受限于你施加限制的应用数据暴露了读写方法。需要更多有关使用conten providers的信息,请见content providers文档
1.1.1 使用共享偏好
SharedPreferences 该类 提供了一种通用的框架允许你去保存和重新获取持久性的、以键值对方式保存的原始数据种类。你可以使用SharedPreferences去保存任意的原始数据:布尔类型、单精度浮点数、整型、长整型、和字符串类型。这种数据会持续作用在整个用户会话期间(即使你的应用被销毁了)。
为了在你的应用中获取sharedpreferences 对象,可以使用下列两种方法之一:
· getSharePreferences() – 若果你需要获取多种以文件名区分的偏好文件,请使用这种方法。可以用第一个参数来指定文件。
· getPreferences() – 若果你只需要一个偏好文件在你的Activity中,请使用这种方法。因为这种方法会为你的Activity提供唯一的preferences 文件,而你不需要提供文件名。
写入值:
(1) 调用 edit() 函数来获取一个 SharedPreferences.Editor。
(2) 使用诸如putBoolean()和putString()等方法来添加值。
(3) 使用commit()方法来提交新的值
读取值,使用SharePreferences的方法,诸如getBoolean()和getString。
以下是一个例子,它在计算器中保存了静音模式的偏好。
public class Calc extends Activity {
public static final String PREFS_NAME = "MyPrefsFile";
@Override
protected void onCreate(Bundle state){
super.onCreate(state);
. . .
//存储偏好
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
boolean silent = settings.getBoolean("silentMode", false);
setSilent(silent);
}
@Override
protected void onStop(){
super.onStop();
// 我们需要一个Editor对象来保存偏好的改变
// 所有的对象都来自于android.context.Context
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("silentMode", mSilentMode);
// Commit the edits!
editor.commit();
}
}
1.1.2 使用内部存储
你可以直接在你的设备内部存储中保存文件。默认情况下,保存在内部存储中的文件对你的应用来说是私有的,其它应用不能获取它们(即使是用户)。当用户卸载你的应用时,这些文件也会被删除。
在你的内部存储中创建和写一份私有文件:
(1) 通过文件名和操作模式调用 openFileOutput()。它将返回一个FileOutPutStream对象。
(2) 使用Write() 方法,对文件执行写操作
(3) 使用close() 方法关闭流
举个例子
String FILENAME = "hello_file";
String string = "hello world!";
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();
MODE_PRIVATE 会创建一个文件(或者取代原有的同名文件),并且使它对你的应用来说是私有的。
其它可以使用的模式有:MODE_APPEDN MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE
从内部存储中读取文件
(1) 调用 openFileInput() 并传递文件名。它将返回一个FileInputStream对象
(2) 使用read()方法读取字节
(3) 使用close()方法关闭流
小提示:在应用中,如果你想在编译的时候保存一个静态的文件,那么请在工程的res/raw/目录保存它。你可以调用openRawResource()方法来打开它,通过传递R.raw.<Filename>的id,这个方法返回一个InputStream对象,你可以用它来读取文件(但是你不能用他来写入原始文件)
• 存储缓存文件
如果你想要临时存储一些数据,而不是永久性的存储它。你应该使用getCacheDir()方法去打开一个文件。这个文件代表一个内部存储路径,在这你可以存储临时的缓存文件。
当设备的内部存储空间较低时,Android系统可能会删除这些缓存文件来恢复空间。然而,你不应该依赖于你的系统去为你清理这些文件。你应该始终自己去维持这些缓存文件,并且控制它的空间消耗在一个合理的范围内,例如1MB。当用户卸载你的应用时,这些文件将会被删除。
• 其它有用的方法
getFilesDir()
获取在文件系统中,你内部文件保存的绝对路径。
getDir()
在你内部存储空间中创建(如果存在则打开)路径。
deleteFile()
删除内部存储中的文件。
fileList();
以数组的方式返回你应用中存储的文件列表。
1.1.3使用外部存储
任何兼容Android的设备都支持一个共享的外部存储,你可以在这里存储文件。外部存储可以是一种可移除的存储媒体(例如 SD card)或者内部的(不可移除的)储存。当用户接通了USB大容量存储设备在电脑上传输文件时,保存在外部存储上的文件时世界可读的(world-readable)并且可以被用户修改。
注意:外部存储可能会变成不可获得的状态,如果用户将外部存储连接到电脑上,或者用户移除了媒体。并且,由于没有一种作用于文件的强制安全措施,所用的应用都可以读或者写那些存放在外部存储的文件,用户也可以删除它们。
• 获得对外部存储的访问
为了读写外部存储上的文件,你的应用必须获取READ_EXTERNAL_STORAGE 或者 WRITE_EXTERNAL_STORAGE 两个系统权限。举个例子:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
如果你需要同时获得读和写文件的权限,那么你只需要请求WRITE_EXTERNAL_STORAGE 权限。因为它也间接的请求了读的权限。
提示:从android 4.4开始,这些权限并不是必须的如果你只是去读写你应用的私有文件。想要知道更多信息,可以查看以下章节关于 存储应用私用的文件。
• 查看媒介是否可获得
在你做任何有关外部存储的工作之前,你应该总是先调用getExternalStorageState()方法来检查媒介是否可获得。媒介可能连接到了电脑,未找到,只读,或者处于其它状态。例如,你可以使用以下一堆代码检查外部存储的状态。
/* 检查外部存储是否可获得以用来进行读和写操作*/
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
/* 检查外部存储是否可获得并至少能进行读操作*/
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()方法返回了一些你可能想要检查的其它状态。例如,媒介是否能被共享(连接到电脑),是否完全未找到,还是已经被永久的移除,等等。当你的应用需要连接到媒体的时候,你可以使用这些状态以告知用户更多的信息。
• 存储可以被其它应用共享的文件
通常来讲,用户可能会通过你的应用获得新的文件,这些文件应该被保存在设备上的公有空间中。这样其它应用就能获取它们,并且用户也能很容易拷贝这些文件。当你这样做时,你应该使用其中一种共享空间的路径,例如 Music/,Pictures/,and Rintones/
为了获得一个代表合适的公有路径的File对象,调用getExternalStoragePublicDirectory()方法,并传递一个你想要的路径类型,例如DIRECTORY_MUSIC, DIRECTORY_PICTURES, DIRECTORY_RINGTONES,或其它。通过合理的存放你的文件在对应的媒体种类路径下,系统的媒体扫描器就能合理的在系统中分类你的文件(例如,铃声会在系统设置中以铃声出现,而不是音乐)。
举个例子,以下是一种在公有相册路径下,为新的相册创建路径发方法。
public File getAlbumStorageDir(String albumName) {
// 获取用户公有的相册路径
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
• 存储应用私有的文件
如果你正在处理一些不打算被其他应用使用的文件(例如,只会被你的应用使用的图像纹理或者音效),你应该使用getExternalFilesDir()在外部存储上创建一个私有存储路径。这个方法也需要一个类型参数来指明子路径的类型(例如 DIRECTORY_MOVIES),如果不需要一个明确的媒体路径,传值null来获得你应用私有路径的根路径。
从Android4.4开始,读写应用私有路径中的文件不再需要 READ_EXTERNAL_STORAGE 或者WRITE_EXTERNAL_STORAGE 两个权限。所以这两个权限只有在maxSdkVersion的版本低于18时,才需要被声明。
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
...
</manifest>
注意:当用户卸载了你的应用时,这个路径及其中的内容都会被删除。系统媒体扫描器也不会去读取这些路径下的文件。所以这些文件在meiaStore的content provider中是获取不到的。同理,你也不应该在这些路径存下存放那些本质上属于用户的媒体,例如通过你的应用捕获或处理过的照片,或者用户通过你的应用购买的歌曲,这些文件看起来本应该被保存在共享路径中的文件。
有时候,设备可能会分配一部分内部存储作为外部存储,当然也可能会提供SD卡的卡槽。当这种设备运行在Android4.3或者更低的版本上时,getExternalFilesDir()方法只会提供一本分内部存储的使用权,你的应用并不能读写SD卡上的文件。然而,从Android 4.4开始,通过getExternalFilesDirs()方法,你即可以获得外部存储的使用权,也可以获得内部存储的使用权。它返回一个File对象的数组,该数组包含每一个位置的入口路径。数组中所包含的第一个入口路径,被认为是主要的外部存储,并且你应当使用这一场所,除非它已经被占满或者是不可获得的。如果你想要在Android4.3或以下版本获得两者的路径,可以使用支持包中的静态方法。ContextCompat.getExternalFilesDirs(). 这个方法也返回了一个File对象的数组,但在Android4.3或以下的版本上,它通常只包含一个入口。
注意:尽管getExternalFilesDir()和getExternalFilesDirs()方法提供的路径并不会被MediaStore的Content provider获得。但是,其它拥有READ_EXTERNAL_STORAGE权限的应用可以获得所有外部存储上的文件,并包含它们。如果你需要严格的限制你的文件的使用权,你应该将你的文件写在内部存储上。
• 存储缓存文件
想要打开一个代表着在外部存储上存放缓存文件的路径,你可以调用getExternalCacheDire()方法,如果用户卸载你的应用,这些文件会自动被删除。
类似于上述所提到的ContextCompat.getExternalFilesDirs()方法,你同样可以获取在第二个外部存储上的缓存入口(如果可以获得的话),通过调用ContextCompat.getExternalCacheDirs()方法。
小提示:为了保护你的文件存储空间,并维持你应用的表现。在整个应用的生命周期中,细心的管理你的缓存文件,并在它们不被需要的时候移除它们这些,这是很重要的。
1.1.4使用数据库
Android 提供了完整的SQLite数据库的支持。任何类都能通过数据库名称来访问你创建的数据库,但是该应用之外的类侧不能。
建议通过继承SQLiteOpenHelper类来创建新的SQLite数据库,并重写其中的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的类的实例。为了向数据库中写入和读出数据,可以分别调用getWriteableDatabase()和getReadableDatabase()方法。它们都会返回一个SQLiteDatabase对象,它代表着一个数据库对象并向外提供对SQLite的操作方法。
通过使用SQLiteDatabase query()方法,你可以对SQLite数据库执行查询操作,它可以接受不同种类的查询参数,例如表名、投影、选择运算、列名、分类以及其它参数。为了执行一些更复杂的查询操作,例如那些需要列的别名的操作,你应该使用SQLiteQueryBuilder这个类,它提供了一些便捷的操作方法来构建查询操作。
每一次SQLite的查询操作都会返回一个游标,它指向所有查询结果的行。游标的原理是使你通过它可以驾驭你从数据库中查询的结果,并读取行和列。
想获得一些在Android中演示如何使用SQLite数据库的例子,可以查看Note Pad和Searchable Ditionary 这些应用。
• 数据库调试
Android SDK 包含了一个 sqlite3 数据库工具,它允许你浏览表的内容,运行SQL命令,并执行其它有用的SQLite数据库操作。查阅Examining sqlite3 databases from a remote shell 来学习如何使用该工具。
1.1.5使用网络连接
你可以使用网络(当可获得时) 在你自己的基于web的服务上,来存储并重新获取数据,为了获得更多的联网操作,你可以使用以下包中的类:
java.net
Android.net
1.2应用安装路径
从API版本8开始,你可以允许你的应用安装在外部存储上(例如,设备的SD卡)。这是一个可选项,你可以在Mainfest文件中用 android:installLocation 参数来声明它。如果你没有声明这个元素,你的应用只会被装在内部存储上,并且不能被移动到外部存储中。
为了允许系统在外部存储上安装你的应用,修改mainfest文件,在其中包含android:installLocation参数,可以使用的值有preferExternal 或者 auto
举例:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="preferExternal"
... >
如果你声明的参数的值是preferExternal ,则你要求你的应用装载在外存储上 。但是系统并不保证你的应用会被安装在外部存储上。如果外部存储已经满了,系统会安装在内部存储上。用户可以在外部存储和内部存储之间转移你的应用。
如果你声明的参数是 auto,则你暗示你的应用可能会被安装在外部存储上,但你对安装的位置并没有指明。系统会基于一些因素来决定在什么位置安装你的应用。用户同样也可以在外部存储和内部存储之间转移你的应用。
当你的应用被安装在外部存储上时:
(1) 只要外部存储处于连接状态,对于应用的表现就没有任何影响。
(2) .apk文件会被保存在外部存储上,但是用户所有的私有数据,数据库,优化后的.dex文件以及提取的本地代码都会被保存在内部存储上。
(3) 唯一的用来存储你应用的容器已经被一个随机产生的密钥加密过。这个密钥只会被最初安装它的设备所加密。因此,一个被安装在外部存储上的应用只会为一台设备工作。
(4) 用户可以通过系统设置将你的应用移动到内部存储上。
警告:当用户通过USB大容量存储设备与你的电脑共享文件或者通过系统设置移除了你的SD卡,使得外部存储对于你的设备来说变得不可获得,那么被安装在外部存储上的应用都会立即被结束掉。
1.2.1向后兼容性
让应用安装在外部存储上的这一特征,只有设备运行的api版本在8(Android的版本2.2)或以上才有用。那些基于api版本8构建的应用总是会被安装在内部存储上,并且不能被转移到外部存储上(即使设备的api版本为8),然而当你的应用是为api版本8或以下的版本来设计的时候,你可以选择是否支持api版本为8或者以上的版本,并通过使用api版本8或者更低来编译。
为了让应用安装在外部存储上,并对低于api 8 的版本保持兼容,你需要:
(1) 在<mainifest>元素中,包含 android:installLocation参数,使用”auto”或”preferExternal” 两个值之一。
(2) 使你的 android:minSdkVersion 参数保持在低于api 8的版本,并确保你应用中的代码只使用了那些兼容该版本的应用程序接口。
(3) 为了编译你的应用,修改构建目标的api 版本为 8。这是必须的,因为在更早的Android库中不明白 android:installLocation 这个参数,当它出现时,也不会去编译它。
(4) 当你的应用被安装在低于api本版8的设备上时,android:installLocation参数会被忽略,并且应用会被安装在内部存储上
注意:尽管在更早的版本中,xml中的标记会被忽略,但当你的minSdkVersion小于8 时你必须小心不要去使用api版本8中的应用程序接口,除非你在你的代码中做过了必要的向后兼容的工作。
1.2.2不应该被安装在外部存储上的应用
当用户连接了USB大容量存储设备在电脑上共享文件时(或者卸载、移除了外部存储),任何正在运行的、安装在外部存储上的应用都会被结束掉。系统实际上会不知道这些应用在哪,直到大容量存储设备被重新安装到设备上。除去结束掉应用,使应用不能被用户获得的后果之外,这种做法也会使得某些种类的应用发生严重的错误。为了让你的应用一直表现的如你预期那样,如果它使用了以下的特征,你不应该允许你的应用被安装在外部存储上。当外部存储未被装载时,可能会出现以下结果:
服务:
你正在运行的服务会被结束掉,即使外部存储被重新装载时也不会重新运行。但是,你依然可以注册ACTION_EXTERNAL_APPLICATIONS_AVAILABLE的事件广播。当被安装在外部存储上的应用对系统来说变得可获得时,这个广播会通知你的应用,此时你可以重启你的服务。
闹钟服务:
你的通过AlarmManager注册的闹铃会被取消,你必须手动再次注册闹钟服务,当外部存储被重新装载时。
输入法引擎:
你的输入法引擎会被默认的取代。当你的外部存储被重新装载时,用户可以打开系统设置重新使用你的输入法引擎。
动态壁纸:
你正在运行的动态壁纸会被默认的动态壁纸所代替。当外部存储被重新装载时,用户可以重新选择你的动态壁纸。
应用组件:
你的应用组件会从主界面移除。当外部存储重新装载时,你的应用组件对于用户来说是不可选取的,直到系统重置主界面应用(通常直到系统重启,都不会这样)
账户管理:
你通过AccountManager创建的用户会消失,直到外部存储重新装载。
异步适配:
你的AbstractThreadedSyncAdapter和其它异步方法都会停止工作,直到外部存储重新装载。
设备管理者:
你的DeviceAdminReceiver及其所有管理功能都会不能使用,这可能会给设备功能造成不可预见的后果,即便外部存储重新装载后,这个问题也会持续。
监听启动完成的广播接收者:
在外部存储被装载之前,系统会发送ACTION_BOOT_COMPLETED的广播。如果你的应用安装在外部存储上,你永远也接受不到这个广播。
如果你的应用使用了任何以上列出的特征,你应该允许你的应用被安装在外部存储上。默认情况下,系统也不会允许你的应用安装在外部存储上,所以你也不需要担心那些已经存在的应用。然而,如果你的确信你的应用永远也不应该被安装在外部存储上,那么你可以通过声明android:installLoaction的值为”internalOnly”。尽管这并不会改变默认行为,但这个声明明确的指出了你的应用应该被安装在内部存储上,并作为一个提醒告知其它开发者。
1.2.3应该被安装在外部存储上的应用
简而言之,任何没有使用到上述特征的应用安装在外部存储上时,都是安全的。大型游戏一般都是应该被安装在外部存储上的应用类型,因为当游戏闲置时,不需要额外的服务。当外部存储变得不可获得时,游戏进程会被结束掉。当外部存储重新可获得,用户重启了游戏时(假设游戏在整个活动周期中合理的保存了它的状态) 也不会有任何可见的影响。
如果你的应用需要一些兆字节的文件,你应该仔细考虑是否应该将应用安装在外部存储上,以便让用户更好的保护内部存储上的空间。