如何选择数据存储
来源:Android Develop - API Guides - Data Storage - Storage Options
Android给提供了几种应用数据持久化的选择,如何选择数据存储的解决方案要根据我们的特殊需要,比如根据数据是应该对应用私有还是数据可以让其他应用(或用户)获取,以及数据需要多大的空间来存储。
数据存储的解决方案有如下几种:
- Shared Preferences
- 以键值对的形式存储私有的、简单的数据。
- Internal Storage
- 在设备内部存储中存储私有数据。
- External Storage
- 在共享的外部存储中存储公共数据。
- SQLite Databases
- 在私有的数据库中存储结构化的数据。
- Network Connection
- 用我们自己的网络服务器在网络中存储数据。
Android提供了一种将数据暴露给其他应用的方式 — 使用content provider。 Content provider 是可选的组件,它可以暴露应用数据的 读/写 使用权限, 并且可以在数据的使用上添加我们需要的任何限制条件。 您可以在Content Providers开发者指南中了解有关它的更多详情。
Shared Preference 应用
SharedPreferences
类提供了一套框架,我们可以用其存储以及恢复的私有的、持久化的、键值对类型的数据。我们可以使用 SharedPreferences
来存储任何类型的私有数据:
boolean、float、int、long、string。数据会在用户活动时会持久存在 (即便应用被kill掉)。
User Preferences
Sharedpreferences 不能准确的存储 "user preferences," 比如用户选择的一个铃声。如果你对在应用中创建user preferences 感兴趣的话,请看PreferenceActivity
,,它提供了用来创建可以自动持久化的
user preferences的Activity框架。(PS:没搞清这里说的是什么,可能是指各种Preference UI的持久化,另外PreferenceActivity目前已经不可用,PreferenceFragment已经代替了它的作用)
以下是为应用获得 SharedPreferences
的两种方法:
getSharedPreferences()
- 如果需要用到多个preferences使用这个方法,第一个参数为用来标识不同preferences的名字。(这里的preferences属于应用程序)getPreferences()
- 如果你的Activity只需要使用一个preference则调用这个方法来获取,因为这个preferences是你的Activity的唯一一个,不需要提供名字来获取。
写入数据:
- 调用 SharedPreferences的
edit()
方法来获取SharedPreferences.Editor。
- 使用
putBoolean()
或者putString()等等类似的方法来添加数据。
- 使用
commit()方法来提交新添加的数据。
读取数据:使用SharedPreferences
的 getBoolean()
以及getString()
等等类似的方法来获取数据。
以下为在计算器应用中使用preferences存取静音按键模式设置的例子:
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(); } }
Internal Storage 使用
我们可以将文件直接存到 internal storage 中。 缺省情况下,存到internal storage中的文件对我们的应用是私有的,其他应用(以及用户)都没有办法获取它们。当用户卸载我们的应用时,这些文件也会被移除。
在 internal storage 中创建文件并写入数据:
- 调用
openFileOutput()
方法并传入文件名以及操作模式两个参数以获取到FileOutputStream。
- 调用
write()方法来将数据写入文件。
- 使用
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
,
and MODE_WORLD_WRITEABLE
.
注意: MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
在
API level 17之后已经过时。 Android N 版本开始,使用它们的话会抛出 SecurityException
异常.
这意味着应用的目标版本如果为 Android N 或者更高的话,则不可以通过文件名来共享私有文件,并且尝试共享像这样的"file://" URI 也会导致 FileUriExposedException
异常被抛出。如果你的应用需要和其他应用共享私有文件,需要使用 FileProvider
并传入 FLAG_GRANT_READ_URI_PERMISSION。具体请看
Android Develop - Training - Building Apps With Contents Sharing -
Sharing Files。
从 internal storage中读文件:
调用openFileInput()方法并传入文件名以返回
FileInputStream。
- 使用
read()方法来读取bytes。
使用close()方法来关闭stream。
注意: 如果你希望在应用中存储一个编译期的静态文件(即应用编译打包时不会对文件作任何处理),那么请将这个文件存到 res/raw/
目录下。 你可以使用openRawResource()方法来打开它,
同时需要传入类似 R.raw.<filename>
的资源
ID。这个方法会返回 InputStream
,你可以用它来读取文件 (但是不可以对原始的文件进行写操作)。
存储缓存文件
如果你想缓存一些数据而不想将其持久化存储,你应该调用getCacheDir()
来打开一个 File
,它是位于 internal
storage 中的应用存储自身临时缓存文件的目录。
当设备的 internal storage 存储空间所剩不多时,Android 会删掉这些缓存来恢复存储空间。然而,你不应该依靠系统来为你清理这些文件。 你应该自行维护这些缓存文件,让其保持合理的空间消耗,比如1MB。当用户卸载了你的应用时,这些缓存文件也会被移除。
其他比较有用的方法
-
getFilesDir()
- 得到你的内部文件存储目录的绝对路径。
getDir()
- 在internal storage 空间中创建一个(或打开已经存在的) 目录。
deleteFile()
- 删除一个存储在internal storage 空间中的文件。
fileList()
- 返回现在存储在应用中的文件列表。
External Storage 使用
每个兼容Android系统的设备都支持一个共享的 external storage ,我们可以用它来存储文件。 它可以是可移动的存储媒介 (例如一个SD卡) ,或者位于 手机内部存储中(不可移动)。存储在 external storage 中的文件对所有应用都是可读的,并且当用户使用USB以文件传输模式将设备连接到电脑时。用户也可以去更改 external storage 中的文件。
提醒: 当用户将 external storage 挂载到电脑上或者移除了这些存储媒介时,external storage 会变得不可用,并且存储在 external storage 中的文件没有强制的安全措施来维护,所有应用都可以读写这些存储在 external storage 中的文件并且用户也可以删除它们。
获取 external storage
如果需要对 external storage 进行文件的读写操作,应用需要获取 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 (存储应用私有的文件)这个章节。
检查存储媒介是否可用
在你对 external storage 进行任何操作之前,你应该先调用 getExternalStorageState()
这个方法来检查当前存储媒介是否可用。External
storage 的存储可能被挂载到了电脑上、缺失、只读或者在其它状态下。下面是两个用于检查存储可用性的方法的例子:
/* 检查 external storage 是否可读写 */ public boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { return true; } return false; } /* 检查 external storage 是否为只读 */ 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()
方法也会返回其它你希望检查的状态,例如存储是否正在被共享(即挂载到了电脑上)、是否完全缺失、是否已经被移除,当你的应用需要获取而无法获取 external
storage 时你可以去检查状态以用更多的信息来通知用户。
存储应用共享文件
在 Media 扫描中隐藏文件
在你的
external storage 文件目录中包含一个空的名为.nomedia
的文件 (注意文件名中的前缀是个点)。这可以阻止media 扫描后读取你的文件并将其通过 MediaStore
content
provider 暴露给其他应用。然而,如果你希望文件真正私有,你应该将其按照私有的方式存储,具体请看下面的 save
them in an app-private directory 这个章节。
通常用户通过应用获取的文件应该在设备上的公共的位置进行存储,其他的应用可以获取这些存储在公共的位置中的文件并且用户也可以轻易从设备上复制它们,当这样做的时候,你应该使用一个公共的文件目录,例如 Music/、
Pictures/、
Ringtones/。
调用 getExternalStoragePublicDirectory()来获取公共的文件目录
File
,方法中需要传入你想获取的文件目录类型例如 DIRECTORY_MUSIC
, DIRECTORY_PICTURES
,DIRECTORY_RINGTONES 或者其他的。通过将你的文件存储到与文件类型相一致的文件目录中,系统的
media 扫描可以合理的对你的文件进行分类
(例如,铃声文件在系统设置中会以铃声类型出现,而不是音乐类型)。
下面是一个在公共的图片目录中创建一个新的相册的方法的例子:
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()这个方法以使用在 external
storage 中的私有文件目录,这个方法也需要一个
type
参数来指明子文件目录的类型 (例如 DIRECTORY_MOVIES
)。如果你不需要一个特定的
media 目录,那么可以传入 null
来获取应用的私有文件目录的根目录。
自 Android 4.4 开始,在应用私有文件目录中读写文件不需要再获取 READ_EXTERNAL_STORAGE
或者 WRITE_EXTERNAL_STORAGE
权限。所以你可以通过加入maxSdkVersion
属性来为需要获取这两个权限的低版本来申请权限。
<manifest ...> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="18" /> ... </manifest>
注意: 当用户删除了你的应用,这个私有文件目录里的所有的文件内容都会被删除,并且系统的 media 扫描不会来读取这个目录中的文件,所以这些文件不可以在 MediaStore
content
provider中获取到。同样地, 你 不应该使用这个私有文件目录 存储属于用户的 media 文件,例如应用拍摄或修改的照片以及用户购买的音乐等,这些应该被存储在公共的文件目录中,详细请看上面的 saved
in the public directories (存储应用共享文件)章节。
有时候,设备会从手机内部存储中分配一个分区给 external storage 使用,或者也会给其提供一个SD卡卡槽。当设备在 Android 4.3 或更低的版本中运行时, 这个 getExternalFilesDir()
方法仅仅能够获取在手机内部存储中的 external
storage ,应用没有办法通过这个方法来对SD卡进行读写操作。 然而自Android 4.4 开始,调用 getExternalFilesDirs()方法可以获取手机内部存储以及SD卡的 external
storage 两个位置,其会返回一个
File
数组来分别表示不同的存储位置。数组中的第一个值为首要的 external
storage ,并且你不应该去使用这个位置,除非它满了或者不可用了。如果你希望在Android 4.3 或更低版本中获取也能够获取不同的存储位置,请使用 support library's 中的静态方法 ContextCompat.getExternalFilesDirs()
。这个也可以返回一个 File
数组。但是在
Android 4.3 或者更低版本中,数组中通常只包含一个值。
提醒:尽管 getExternalFilesDir()
和getExternalFilesDirs()
返回的文件目录不可以被 MediaStore
content
provider获取,但其他应用如果获取了 READ_EXTERNAL_STORAGE
权限的话则可以取到所有
external storage 中的文件,包含这些 external storage 中私有文件目录中的文件。如果你需要完全限制其他应用对你的文件的获取,那你应该将文件存储在 internal storage 中而不是 external
storage 中。
存储缓存文件
如果要打开在
external storage 中存储缓存文件的文件目录,请调用 getExternalCacheDir()方法
,如果用户卸载了这个应用,这些缓存文件也会被删除。
类似上面提及的 ContextCompat.getExternalFilesDirs()方法,你也可以调用
ContextCompat.getExternalCacheDirs()方法来获取多个 external
storage (如果存在第二个 external storage )中的缓存文件目录。
注意: 你需要谨慎的管理缓存文件并且通过应用的生命周期来移除那些不再需要用到的缓存文件,这对于维护文件空间以及应用性能而言非常重要。
Databases 使用
Android提供了对 SQLite 数据库的完整的支持。你创建的任何数据库都可以在应用中的任何类中通过数据库名称来获取到,但是其他应用不能获取。
创建一个新的 SQLite 数据库的推荐方法:创建 SQLiteOpenHelper
的子类并覆盖其 onCreate()
方法,在
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
,其代表数据库并提供了对 SQLite
数据库操作的方法。
Android 不会对超过标准的 SQLite 外的概念强加限制。我们应该加入一个自增长的主键并让其成为快速查找某条记录的唯一ID,这对私有的数据来说可能不需要,但是如果你实现了一个 content
provider,你必须包含一个唯一的ID来表示BaseColumns._ID
常量。
你可以使用 SQLiteDatabase
query()
方法来执行 SQLite
的查询,这个方法可以接受多个查询的参数,例如需要查询的表格、查询的行及其条件、查询的列、如何分组等等。更复杂的查询,比如需要列名称的别名等,你应该使用 SQLiteQueryBuilder,它提供了几个方便的方法来构建查询参数。
SQLite 查询会返回一个 Cursor
,它指向了查询返回的所有行。
你可以使用这个 Cursor
来对数据库查询的结果进行操作,并且可以从中读取查询记录的行和列。
这是两个Android 使用 SQLite 数据库的demo, Note Pad ,Searchable Dictionary 。
数据库调试
Android SDK 包含一套 sqlite3
数据库工具,其可以让我们浏览数据库表格内容、执行SQL命令以及完成其他与 SQLite
数据库相关的功能。具体如何使用请查看 Examining sqlite3 databases from a remote shell 。(PS:就是adb里面那些对数据库操作的指令)
Network Connection 使用
你可以使用网络 (当其可用时)在网络服务器上存储和恢复数据,网络的操作需要用到下面两个包中的类: