【Android Developers Training】 25. 保存文件
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。
原文链接:http://developer.android.com/training/basics/data-storage/files.html
Android使用的文件系统和其它平台上使用的磁盘文件系统比较类似。这节课将描述如何通过File的APIs对Android文件系统进行读写文件。
一个文件对象适合于按既定的顺序读或者写大量数据,而非跳跃式地进行。例如,它适合于图像文件或者任何在网络上交换的数据。
这堂课将展示如何在你的应用中,执行与基本文件相关的任务。我们假设你熟悉Linux文件系统基础知识,以及java.io中标准文件输入/输出流的APIs。
一). 选择内存或外存
所有Android设备有两个文件存储区域:“内部(internal)”和“外部(external)”存储。这两个名字的由来要追溯到早期的Android,那时大多数设备提供内置的非易失性存储器(内存),加上一个可移除的闪存介质,比如迷你SD卡(外存)。一些设备将永久存储空间划分为“内部”和“外部”两个部分,所以即使没有闪存介质,仍然会有两个存储空间。与此同时,不管外存是否是可移除的,对于API来说没有差异。下面将列举出每个存储空间的特性。
内存:
- 永远可以获取的到
- 默认情况下,存储在这里的文件只有你的应用自身能获取到
- 当用户卸载了你的应用,系统会从内存中删除所有该应用的相关文件
综上所述,当你期望你的文件不会被用户或者其他应用获取时,内存将是最好的选择。
外存:
- 它并不能永远都可获得,因为用户可以将外存作为一个USB存储而挂载起来,并且在一些情况下会把它从设备上移除
- 存储在这里的文件可被任意访问,而不在你的控制之内
- 当用户删除了你的应用时,只有在你将文件存储在通过getExternalFilesDir()方法所得到的目录下时,系统才会删除你的文件
综上所述,外存适合于存储那些对访问没有限制的文件,以及你希望和其他应用共享的文件,或者你希望用户可以通过电脑来获取到的文件。
Tip:
虽然默认情况下应用汇存储在内存,但你可以定义清单文件中的“android:installLocation”这一属性字段,这样你的应用就可以存储在外存上。当APK文件大小很大,同时用户拥有一个比内存空间要大的外存时,用户会期望能够这么做。更多信息可以阅读:App Install Location。
二). 获得操控外存的权限许可
为了获得写入外存的权限,你必须在你的清单文件(manifest file)中声明“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>然而,如果你的应用使用“WRITE_EXTERNAL_STORAGE”的授权许可,那么它暗示了同时还拥有读外存的权限。
在内存中保存文件不需要任何权限许可。你的应用永远都具有读和写在内存中其自身所对应的目录的权限。
三). 在内存中保存一个文件
当将一个文件存入内存时,你可以通过调用以下任一一种方法来取得合适的目录作为一个File对象:
- getFilesDir():返回一个File对象,它代表了你的应用所拥有的内存中的一个目录。
- getCacheDir():返回一个File对象,它代表了存放应用的临时缓存文件的内存目录。请确保删除每一个不再需要的文件,同时制定一个任何时刻你能使用的存储空间的大小限制,比如1MB。如果系统在运行时存储空间不足,它可能会在没有任何警告的情况下删除你的缓存文件。
为了在上述任何一个目录中创建文件,你可以使用构造函数File(),传递给它上述两个方法中的一个来特定你的内存中的目录路径。例如:
File file = new File(context.getFilesDir(), filename);
另外,你可以调用openFileOutput()来获得一个文件输出流,以此将数据写入你的内存目录中的一个文件。例如:
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()方法。例如,下面的方法从一个URL中提取出文件名,然后利用该名字创建一个文件,在你的应用的内存目录中:
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标识,它们将永远无法被其他应用所访问到。
四). 在外存上保存一个文件
因为外存可能是无法获得的,比如当用户已经把存储挂载至PC上,或者已经移除了提供外存的SD卡。你应该在每次访问它之前先确认对应的卷是否存在。你可以通过调用getExternalStorageState()来查询外存的状态。如果返回的状态是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):
属于你的应用的合法文件,用户卸载你的应用时,这些文件也会被同时删除。虽然从技术上说,因为这些文件存储于外存,所以它们可以被用户或其他应用访问到,但实际上这些文件在你的应用范围之外向用户提供任何数据。当用户卸载了你的应用时,系统会删除所有你的应用外部私有目录下的文件。此类文件的例子有:你应用所下载的额外的资源文件或者临时的多媒体文件。
如果你希望在外存中存储公有文件,使用getExternalStoragePublicDirectory()来获得一个File对象,它代表了外存上一个合适的目录。这个方法接受一个参数,该参数指定了你希望存储的文件类型,这样它们就能与其他公有文件一起被统一地管理了,文件类型诸如: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; }
如果你希望存储属于你应用的私有文件,那么你可以调用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()。
不管你使用的是getExternalStoragePublicDirectory()(用于共享的文件),还是getExternalFilesDir()(用于私有的文件),使用API常量提供的目录名(诸如DIRECTORY_PICTURES)是很重要的。这些目录名保证了系统会正确地处理这些文件。例如,存储于DIRECTORY_RINGTONES下的文件会被系统的多媒体扫描器分类为铃声而不是音乐。
五). 查询空余空间
如果你能提前知道你要存储多大的数据,那么你将知道是否有足够的空间来存储这些数据,从而避免IO异常(IOException)。通过调用getFreeSpace()或者getTotalSpace()这两个方法可以实现上述的预期。这两个方法分别提供了在存储卷内的当前可用空间和总空间。这些信息还可以用来避免向存储卷内填充超出阈值数量的数据。
然而,系统不会保证你可以写入和getFreeSpace()所返回的可用空间一样大小的数据。如果返回的数量比你希望存储的数量多了几兆,或者文件系统的使用率小于90%,那么继续执行是没有问题的。否则你可能无法写入数据。
Note:
你不必在你存储文件之前检查可用空间的大小。你可以尝试直接写入文件,然后当异常发生时捕捉IOException。你需要这么做如果你不知道你具体需要多少空间。例如,如果你在保存之前转变了文件的编码(把PNG格式的图片转换为JPEG,此时你无法预知文件的大小)。
六). 删除一个文件
你应该将不再需要的文件删除。删除文件最直接的方法是对文件对象自身调用delete()。
myFile.delete();
如果文件存储于内存,你也可以通过Context来定位,然后调用deleteFile()删除文件:
myContext.deleteFile(fileName);
Note:
当用户卸载了你的应用,Android系统会删除如下文件:
- 所有你在内存存储的文件
- 所有你通过getExternalFilesDir()方法在外存存储的文件
然而,你要定期手动地删除所有通过getCacheDir()创建的临时文件以及其它你不再需要的文件。