【翻译】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应该被安装在外部存储上的应用

简而言之,任何没有使用到上述特征的应用安装在外部存储上时,都是安全的。大型游戏一般都是应该被安装在外部存储上的应用类型,因为当游戏闲置时,不需要额外的服务。当外部存储变得不可获得时,游戏进程会被结束掉。当外部存储重新可获得,用户重启了游戏时(假设游戏在整个活动周期中合理的保存了它的状态) 也不会有任何可见的影响。
如果你的应用需要一些兆字节的文件,你应该仔细考虑是否应该将应用安装在外部存储上,以便让用户更好的保护内部存储上的空间。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容