所有的Android设备都支持保存文件到外部存储,外部存储可能是可移除的存储介质(例如SD卡)或者手机内部介质,保存到外部存储的文件是全局可读取文件,用户可以修改这些文件。
使用场景
(1)当你想将你的应用的文件和其它应用共享(例如拍照应用拍的照片)。
(2)当你的应用产生的文件在应用卸载后任然需要为用户保留。
(3)对于无需访问限制或者允许用户使用计算机访问的文件。
注意:如果你希望你的文件“绝对私有”,那么应该使用内部存储,外部存储即使存储的是私有文件,用户也可以通过计算机访问(例如拷贝到计算机打开)。
如何使用
(1)保存外部私有文件(android/data/packagename)
保存在这里的文件属于你的应用,但这些文件从技术上是可以被用户和其他应用访问。当用户卸载你的应用时,系统会删除应用外部私有目录中的所有文件。这里面不会被系统的内容提供者程序所扫描,所以用户的媒体文件不适宜存储在这里(例如用户拍的照片和购买的音乐等等)。
从Android4.4开始,读取和写入外部私有目录的文件不再需要权限,但是为了兼容较低版本,我们的最佳做法如下:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
在使用外部存储执行任何工作之前,都需要检查介质的可用性:
/**
*判断外部存储是否挂载
*/
private static boolean isExternalStorageMounted() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
示例代码:
/**
*
* @param context
* @param inputStream 要写入的数据的输入流
* @param directory android/data/packagename/Files/directory
* @param fileName 文件名
* @param isAppend 是否是追加写入
* @throws Exception
*/
public static void savePrivateFileExternal(Context context, InputStream inputStream, String directory,
String fileName, boolean isAppend) throws Exception {
if (isExternalStorageMounted()) {
File file = new File(context.getExternalFilesDir(directory), fileName);
writeToFile(inputStream, file, isAppend);
} else {
throw new Exception("保存私有文件到外部存储失败,手机外部存储不可用");
}
}
private static void writeToFile(InputStream inputStream, File file, boolean isAppend) throws Exception {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file, isAppend);
byte[] bytes = new byte[DEFAULT_BUFFER_SIZE];
int total = 0;
if (-1 != (total = inputStream.read(bytes))) {
fileOutputStream.write(bytes, 0, total);
}
} catch (Exception e) {
throw new Exception("文件写入异常");
} finally {
try {
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}
}
如果要保存缓存文件到外部存储的私有目录,只需要更改一行代码
File file =new File(context.getExternalCacheDir(),fileName);
需要注意的是,你需要在应用的整个生命周期对缓存文件进行管理,以保持应用的性能和手机的存储空间。
(2)保存可与其它应用共享的文件(根目录/Picture;根目录/DCIM;……)
有些文件可能在用户卸载应用之后任然需要为用户保留,例如照片或者用户购买的音乐;有些文件你可能希望系统的应用程序可以扫描到或者其它同类型的应用可以扫描到。这样的文件都需要保存在共享目录中。
在清单文件中请求权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
目前,读取外部存储的公共目录是不需要申请权限的,但是Android官方宣称会在未来的版本中进行更改,所以当我们需要读取外部存储还是在清单文件中将权限申请写上。
另外如果你的应用已经申请了WRITE_EXTERNAL_STORAG权限,那么它也隐含读取外部存储的权限。
个人建议只要你的应用需要使用外部存储进行读写就将上面的两个权限都在清单文件中写上。
Android 6.0以后对于写入外部公共存储权限需要动态申请。
与存储外部私有文件一样,你同样需要检查外部存储介质是否可用。
示例代码:
/**
*
* @param inputStream
* @param directory
* @param fileName
* @param isAppend
* @throws Exception
*/
public static void savePublicFileExternal(InputStream inputStream, String directory,String fileName, boolean isAppend) throws Exception {
if (isExternalStorageMounted()) {
File file = new File(Environment.getExternalStoragePublicDirectory(directory), fileName);
writeToFile(inputStream, file, isAppend);
} else {
throw new Exception("保存公共文件失败,手机外部存储不可用");
}
}
对于directory参数,这里不可以随意给一个目录,这里需要给的目录是Android系统自带的目录,例如DIRECTORY_MUSIC,DIRECTORY_PICTURE,DIRECTORY_DCIM等等,
只有使用这样的目录系统程序才能扫描到相应的文件,以便正确的分类。
(3)在外部存储的根目录下保存自己应用的文件(根目录/packagename)
Android官方网站并没有提这样的保存文件的做法,但是如果你观察手机的存储就会发现现在好多的应用都在根目录下新建一个文件夹来保存自己的文件。这个文件夹多以包名命名。
关于为什么要这么做的原因,个人的理解:
1.应用需要保存一些数据文件在应用被卸载时不被系统删除。
2.要保存的这些文件并不是所有的都需要共享,有的只有应用本身需要使用。
其实,现在很多时候即使是一些需要共享的文件,像照片、音乐也存储在这个目录下。
保存在这个目录关于权限方面需要做的操作与“保存与其它应用共享的文件”完全一样,权限的使用完全参考就可以,这里不再说明。
当然,它也一样需要检查外部存储的介质是否可用。
示例代码:
/**
*
* @param context
* @param inputStream
* @param fileName
* @param isAppend
* @throws Exception
*/
public static void saveFileExternal(Context context, InputStream inputStream, String fileName,
boolean isAppend) throws Exception {
if (isExternalStorageMounted()) {
File file = new File(Environment.getExternalStorageDirectory(), context.getPackageName());
if (file.exists()) {
writeToFile(inputStream, new File(file, fileName), isAppend);
} else {
if (file.mkdirs()) {
writeToFile(inputStream, new File(file, fileName), isAppend);
} else {
throw new Exception("在外部存储创建应用目录失败");
}
}
} else {
throw new Exception("保存公共文件失败,手机外部存储不可用");
}
}
一些需要说明的点
(1)Android 7.0推出了“使用作用域目录访问”这样的一个概念。
这个概念是针对“保存与其它文件共享的文件”的,就是说你要将文件保存在Android系统所自带的一些公共目录,像DIRECTORY_MUSIC,DIRECTORY_DOCUMENTS这些。
Android官方认为有些时候应用只是想把文件存储到那些公共目录中的一个,但是上面的用法中,通过动态权限的申请,一次就赋予了应用所有公共目录的读写权限,Android官方认为这样的做法不合适,所以希望将它拆开。
具体如何使用“使用作用域目录访问”,这里以存入DIRECTORY_DOCUMENTS目录为例:
第一步,请求授予对于指定目录的访问:
@TargetApi(Build.VERSION_CODES.N)
public static void storagePublicFileExternal(Activity activity, int requestCode) {
StorageManager storageManager = (StorageManager) activity.getSystemService(Context.STORAGE_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
StorageVolume storageVolume = storageManager.getPrimaryStorageVolume();
Intent intent = storageVolume.createAccessIntent(Environment.DIRECTORY_DOCUMENTS);
activity.startActivityForResult(intent, requestCode);
}
}
在用户确认授予权限后,就会调用当前Activity的onActivityResult方法,并在返回的Intent中包含了指定目录访问的URI。
第二步,在onActivityResult保存外部指定目录访问的URI,这样就不必重复授权。
示例代码:
@TargetApi(Build.VERSION_CODES.KITKAT)
public static void savePersistableUriPermission(Context context, Uri uri){
ContentResolver contentResolver = context.getContentResolver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
第三步,利用指定目录的URI,通过v4包中DocumentFile写入文件,这里不能使用java中的File相关类。示例代码:
public void savePublicFileExternalDocument(Context context, Uri uri, String fileName) throws Exception {
ContentResolver contentResolver = context.getContentResolver();
DocumentFile documentFile = DocumentFile.fromTreeUri(this, uri).createFile("text/plain", fileName);
OutputStream outputStream = contentResolver.openOutputStream(documentFile.getUri());
outputStream.write("Hello!".getBytes());
}
(2)查询可用空间
在保存文件时有两种方式可以确保不会因为存储空间不足的原因导致程序异常:
1.通过调用getFreeSpace()和getTotalspace()查询可用空间和存储卷中的总空间。在写入文件之前检查空间是否足够。需要注意的是只有在你需要写入的文件的大小小于剩余的空间90%,才可以保证文件能被正常写入。
2.如果你不知道所需的确切空间,可以直接写入,然后再捕获IOException,进行异常处理。
参考:
https://developer.android.google.cn/training/basics/data-storage/files.html
https://developer.android.google.cn/guide/topics/data/data-storage.html#filesExternal