Android 的文件存储

选择内存储还是外存储


所有安卓设备都有两个存储区域,内存储和外存储。以前的安卓设备都有提供一个内置的存储(内存储),和一个可插拔的存储(外存储),例如SD卡。但是现在的许多设备是不支持外置存储卡的,而是把内置的存储分成两个区域,相当于内存储和存储。所以不管设备是否支持外置存储卡,它都有内存储和外存储这两个区域。存储操作相关API的调用是一样的。以下是内存储和外存储的区别:

内存储:

  • 一直可用
  • 默认情况下,存储于此的文件只有该应用本身有权限操作。
  • 当用户卸载该应用,系统会删除该应用存储在内存储的所有文件。

如果你不想用户和其他应用操作你的文件,最好的方式就是保存在内存储。

外存储:

  • 并非一直可用,比如当用户用数据线连接电脑并打开数据存储或者移除存储卡。
  • 对用户和其他应用可读。
  • 当用户卸载该应用时必不会删除存储在外存储的文件,除非你保存的时候调用的是getExternalFilesDir().

如果你要保存的文件对操作权限没有要求,比如和其他应用共享或者允许用户操作,那么最好的方式就是存储在外存储。

Tip:** 默认情况下应用是安装在内存储,但是你可以在manifest中指定android:installLocation的属性值让应用安装在外存储上。特别是当应用应用特别大,而用户又有足够的问存储空间时。更多详情,请查阅App Install Location

获得外存储权限


要在外存储上执行写的操作,必须在manifest文件中请求WRITE_EXTERNAL_STORAGE权限:

1
2
3
4
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>

Caution:目前,所有应用不必声明相关权限就可以在外存储上执行读的操作。然而,不久的将来就需要了。如果你的应用需要读取的操作(不需要写的操作),你可以在manifest中声明READ_EXTERNAL_STORAGE权限,这样可以保证应用在后续版本中正常运行(即使当前系统并不要求该权限)。

1
2
3
4
<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...
</manifest>

如果你的应用已经声明了WRITE_EXTERNAL_STORAGE权限,就没必要再声明READ_EXTERNAL_STORAGE权限,因为WRITE_EXTERNAL_STORAGE默认包含了写的权限。

You don’t need any permissions to save files on the internal storage. Your application always has permission to read and write files in its internal storage directory.对于内存储并不需要任何读写的权限。

在内存储中保存文件


当要在内存储存储文件时,可以通过以下两种方式获取合适的存储位置:

  • getFilesDir()

    返回一个分配给该应用的内存储文件夹。

  • getCacheDir()

    返回一个分配给该应用的内存储缓存文件夹File对象。当缓存文件不再需要时记得删除掉。当系统存储不够用时,系统可能会删除掉缓存的文件且没有提示。

要在以上任一种文件夹中创建文件时,可以用File(dir, fileName)构造方法,第一个参数传入以上其中一种方法返回的内存储文件夹对象:

1
File file = new File(context.getFilesDir(), filename);

还有另一种方法,你可以调用[openFileOutput()](http://developer.android.com/reference/android/content/Context.html#openFileOutput(java.lang.String, int))来打开一个连接内存储文件的FileOutputStream 对象。如下所示如何在内存储文件中写入内容:

1
2
3
4
5
6
7
8
9
10
11
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()](http://developer.android.com/reference/java/io/File.html#createTempFile(java.lang.String, java.lang.String))。例如以下的示例从一个URL中提取出文件名,再用这个文件名创建一个存储在缓存文件夹中的文件:

1
2
3
4
5
6
7
8
9
10
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:一个应用的内存储空间是由应用包名标示的在安卓文件系统中的一个特定区域,技术上来讲,如果把文件的mode设为可读,那么其他应用就可访问。但是,前提是其他应用必须预先知道你的应用的包名和要访问的文件名。除非你明确声明某个文件为可读或可写,否则其他应用无法访问你的内存储文件也没有读写的权限。

在外存储中保存文件


因为外存储有可能是不可用的,比如当用户打开USB数据存储或者是卸载提供外存储的SD卡时,所以每当你要使用外存储是最好先核实下外存储是否可用。你可以通过调用getExternalStorageState()来查询外存储的状态。如果返回的状态是MEDIA_MOUNTED,那么表示你可以读写外存储。例如,以下两个非常实用的方法可以用来确定外存储的可用性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 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;
}

虽然说整个外存储对于用户和所有应用都是可操作的,但是有两种类型的文件你可能会存储在这里:

公开文件

对其他应用和用户开放的文件应该存储在这里,当用户卸载你的应用时,这些文件应该保留。比如你的应用拍摄的图片或下载的文件。

私有文件

完全属于你的应用的文件并且在用户卸载你的应用时应该被删除的文件可以存储在这里。虽然技术上来说,存储在外存储的文件对于用户和其他应用都可以访问,但是这些文件对于用户和其他应用一文不值。当用户删除你的应用时,系统会自动删除你的应用存储在这里的所有文件。比如说你的应用下载的资源文件或临时的文件。

如果你想在外存储中保存公共文件,可以用getExternalStoragePublicDirectory()方法来获取一个在外存储中的File文件夹对象。这个方法的第一个参数用来指定要保存的文件的类型,可以被系统分类,例如DIRECTORY_MUSIC或者DIRECTORY_PICTURES。如下所示:

1
2
3
4
5
6
7
8
9
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()来获取相应的文件夹,并在第一个参数指定你想要的类型。每个用这个方式创建的文件夹都是放在一个父文件夹里,这个父文件夹存里放着所有当用户卸载该应用时系统会自动删除的文件。

例如,以下的方法用来创建一个存放照片的文件夹:

1
2
3
4
5
6
7
8
9
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中的文件会被系统media scanner归类为铃音而非音乐。

查询剩余存储空间


如果你在保存某一文件时事先知道该文件的大小,你可以通过调用getFreeSpace() 或者 getTotalSpace()来确定是否有足够的存储空间来保存该文件以免造成IOException异常。

但是,系统并不保证你通过getFreeSpace()查询出来有多少剩余存储就可以存储多少。如果剩余存储容量比你要存储的文件大小大个几MB,那么一般可以成功存储。

Note:其实你也可以不用在存储文件前预先查询剩余多少存储空间,你可以尝试着直接写入数据,然后如果有出现异常直接捕获就行了。这种写法一般可以用在当你事先不知道要写入数据的大小的时候。比如说当你要保存一张PNG格式的图片前要先把他转化为JPEG格式,那么你就不知道要保存的数据的大小。

删除文件


你应该经常删除不再需要的文件,最直接的方法就是直接调用File对象的delete()方法。

1
myFile.delete();

如果要删除的文件是存储在内存储中,你可以通过调用Conext的deleteFile()方法来删除。

1
myContext.deleteFile(fileName);

Note:当用户卸载你的应用时,安卓系统会自动删除以下位置的文件:

  • 存储在内存储中的所有文件
  • 通过getExternalFilesDir()方法存储在外存储中的所有文件。

然而你应该制定一个规则定期删除内存储缓存文件夹中的缓存文件和其他你不再需要的文件。