在开发过程中,我们经常会遇到读写文件,比如缓存图片、网页,下载视频、图片等。这就带来一个问题,针对于不同的情形,我们应该将文件置于何处?
一、内部存储和外部存储的概念
在Android中,我们可以把数据存储在SharedPreferences、内部存储、外部存储、SQLite数据库和网络上。其中内部存储指的是设备内存,一般存储私有数据;外部存储是共享的,可以用来存储公有数据。
1.1、什么是内部存储和外部存储
所有Android设置都有两个文件存储区域:内部和外部存储。这些名称在Android早期产生,当时大多数设备都提供内置的非易失性内存(内存存储),以及移动存储介质,比如SD卡(外部存储)。一些设备将永久性存储空间划分为内部和外部分区,即便没有移动存储介质,也始终有两个存储空间,并且无论外部存储设备是否可移动,API的行为均一致。
内部存储
- 始终可用
- 默认情况下只有自己应用可以访问此处保存的文件
- 当用户卸载应用时,系统会从内存存储中删除应用的所有文件
当希望用户或其他应用均无法访问文件时,内存存储是最佳选择。
外部存储
- 并非始终可用,因为用户可能会采用USB存储的形式装载外部存储或者移除
- 全局可读的
- 当用户卸载应用时,只有getExternalFilesDir()目录下的文件会被系统删除
对于无需访问限制以及希望与其他应用共享或允许用户使用电脑访问文件时,外部存储是最佳选择。
==尽管应用默认安装在内部存储中,但可以在mainfest文件中指定android:installLocation属性,这样应用便可以安装在外部存储中。当APK非常大且外部存储空间大于内存内存时,用户更青睐这个选择。==
1.2、使用内部存储
在内部存储中保存文件时,可以通过调用以下两种方法之一作为File的相应目录:
getFilesDir()
返回表示应用的内部目录的File
getCacheDir()
返回表示应用临时缓存文件的内部目录。
另外,如果需要缓存某些文件,应该调用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;
}
==应用的内部存储设备目录由应用在Android文件系统特定位置中的包名指定。在技术上,如果将文件模式设置为可读,另一个应用可以读取该内部文件。但是,另一个应用需要知道包名和文件名。==
除了上面的介绍,还有方法可以保存文件到内部存储中,
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_APPEND、MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE。
==MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE从API 17开始废弃。从Android N开始,如果再使用会抛出SecurityException异常。这意味着在Android N之后的版本不能通过“file:”URI分享文件了,会导致FileUriExposedException抛出。如果你的应用需要和别的应用分享私有文件,需要使用FileProvider,具体可参考分享文件==
从内部存储中读取文件也分为三步:
1)调用openFileInput方法并传入文件名,返回FileInputStream。
2)调用read()方法读取字节
3)调用close()关闭输入流
1.2.1、保存缓存文件
如果你想缓存某些数据,而不是一直持有,你需要调用getCacheDir()方法打开一个代表应用暂时保存缓存文件的内部目录。
当设备的内部存储容量不足时,Android会删除这些缓存文件来恢复控件。然而,你不应该依赖于系统来删除缓存文件。你应该始终自己管理缓存文件并且有责任让它们限制在一个范围,比如1MB。当用于卸载应用时,这些文件会被移除。
其他有用的方法
getFilesDir()
返回使用openFileOutput(String,int)方法创建文件的目录
getDir()
创建(或打开现有)的内部存储空间中的目录。
deleteFile()
删除内部存储中的文件
fileList()
返回关联当前应用的私有文件
1.3、使用外部存储
每一个Android兼容的设置支持共享外部存储来保存文件。但是这个存储是可移除的(比如SD卡)或者一个内置存储(不能移除的)。存储在外部存储的文件是共享的,并且用户可以通过将其连接到电脑上传输文件时可以修改。
==如果用户将外部存储挂载到电脑上或移除,那么外部存储就变成不可获得了。所有的应用可以读写保存在外部存储上的文件并且用户可以移除。==
1.3.1、使用作用域目录访问
在Android 7.0及其以后,如果需要获取外部存储上的指定目录,使用作用域目录访问。作用域目录访问简化了应用如何获取标准的外部存储目录,比如Pictures目录。具体可以参考使用作用域目录访问。
1.3.2、获得外部存储
为了在外部存储上读写文件,应用需要声明READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE系统权限。比如:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
如果同时需要读写文件,你只需要声明WRITE_EXTERNAL_STORAGE权限即可,因为这个权限隐式地包含了读权限。
==从Android 4.4开始,如果只是读应用私有的文件,那么这些权限不是必需的==
1.3.3、检查媒体的可用性
在使用外部存储之前,需要调用getExyernalStorageState()方法检查外部存储是否可用。外部存储可能会被挂载到电脑上,或被移除,或只读,或其他状态。下面是一段检查可用性的代码:
/* 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;
}
getExternalStorageState()方法返回多种状态,比如外部存储是否可共享,是否消失,是否移除等。
1.3.4、保存可以分享给其他应用的文件
通常,如果要创建别的应用可以获得的文件,一般存储在公共位置,这样别的应用可以获得,用户也可以执行复制等操作。当这样做的时候,你需要使用共享公共目录,比如Music/,Pictures/,和Ringtones/等。
为了获得相应的公共目录,调用getExternalStoragePublicDirectory()方法,并传入你想要目录的类型,比如DIRECTORY_MUSIC,DIRECTORT_PICTURES,DIRECTORY_RINGTONES或者其他。通过将文件保存在相应的媒体目录下,系统的媒体扫描器可以在系统中分类文件(比如,铃声出现在铃声设置中,而不是音乐)。举个例子,下面是创建一个公有的相册目录:
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;
}
1.3.5、保存应用私有的文件
如果你在保存的文件不希望给别的应用使用(比如应用自己的音效),你需要调用getExternalFilesDir()方法获取外部存储上的私有存储目录。这个方法也需要一个type参数来指定子目录的类型(比如DIRECTORY_MOVIES)。如果你不需要指定媒体目录,传入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>
==当用户卸载应用时,这个目录和其内容会被删除。同时,系统媒体扫描器不会读这些目录下的文件,因此不可以通过MediaStore获得。因此,不应该使用该目录保存照片等信息,这些文件应该保存在公有目录中==
现在,有很多设备既有内置的外置存储,又可以插入SD卡。当这样的设置运行在Android 4.3及其低版本中,getExternalFilesDir()方法只会得到内置的外部存储,应用不能读写SD卡。从4.4开始,你可以通过getExternalFilesDirs()获取两个目录,返回一个文件数组。数组中第一个元素是主共享外部存储,一般都应先使用该目录除非它满了。如果想在4.3及其低版本中获取目录,需要使用支持库中的静态方法,ContextCompat。getExternalFilesDirs(),这个方法也会返回一个File数组,但是在Android4.3及其低版本中,只有一个元素。
==尽管getExternalFilesDir()和getExternalFilesDirs()得到的目录不会被MediaStore扫描到,但是其他包含READ_EXTERNAL_STORAGE权限的应用可以获得这些文件。如果你需要严格限制文件的获得,那么应该保存在内部存储中==
1.3.6、保存缓存文件
为了打开外部存储上保存缓存文件的目录,应该调用getExternalCacheDir()方法。如果应用被卸载,这些文件会自动被删除。
类似于上面提到的ContextCompat.getExternalFilesDirs()方法,你也可以在第二块外部存储中调用ContextCompat.getExternalCacheDirs获取缓存目录。
二、查询可用空间
如果事先知道保存文件的大小,那么就可以查出是否有足够的可用空间来存储文件,就不需要调用getFreeSpace()或getTotalSpace()方法引起IOException了。这些方法分别提供目前的可用空间和总空间。
但是,系统并不保证你可以写入与getFreeSpace()一样多的字节。如果返回的数字比要保存的文件大出几MB,或者文件系统所占用的空间不到90%,那么可以安全操作,否则,不应写入存储。
==在保存文件之前,无需检查可用空间量。可以尝试立刻写入文件,然后在IOException出现时将其捕获。如果你不知道确切空间量,可能需要那么做。==
三、总结
从上面可以看出,如果需要将文件保存在内部存储中,都需要调用Context的方法。无论是getFilesDir()还是getCacheDir,亦或是getFileOutput()和getFileInput()方法,都是Context的方法;但是如果需要获取外部存储的目录,那么都需要使用Environment提供的方法,比如getExternalFilesDir()等方法。
本文详细介绍了Android应用中文件存储的不同方式,包括内部存储和外部存储的区别及使用方法,以及如何查询可用空间。
972

被折叠的 条评论
为什么被折叠?



