理解作用域存储
Android长久以来都支持外置存储空间这个功能,也就是我们常说的SD卡存储。这个功能使用得极其广泛,几乎所有的App都喜欢在SD卡的根目录下建立一个自己专属的目录,用来存放各类文件和数据。
那么这么做有什么好处吗?我想了一下,大概有两点吧。第一,存储在SD卡的文件不会计入到应用程序的占用空间当中,也就是说即使你在SD卡存放了1G的文件,你的应用程序在设置中显示的占用空间仍然可能只有几十K。第二,存储在SD卡的文件,即使应用程序被卸载了,这些文件仍然会被保留下来,这有助于实现一些需要数据被永久保留的功能。
然而,这些“好处”真的是好处吗?或 许对于开发者而言这算是好处吧,但对于用户而言,上述好处无异于一些流氓行为。因为这会将用户的SD卡空间搞得乱糟糟的,而且即使我卸载了一个完全不再使用的程序,它所产生的垃圾文件却可能会一直保留在我的手机上。
另外,存储在SD卡上的文件属于公有文件,所有的应用程序都有权随意访问,这也对数据的安全性带来了很大的挑战。
为了解决上述问题,Google在Android 10当中加入了作用域存储功能。
那么到底什么是作用域存储呢?简单来讲,就是Android系统对SD卡的使用做了很大的限制。从Android 10开始,每个应用程序只能有权在自己的外置存储空间关联目录下读取和创建文件,获取该关联目录的代码是:context.getExternalFilesDir()。关联目录对应的路径大致如下:
/storage/emulated/0/Android/data/<包名>/files
将数据存放到这个目录下,你将可以完全使用之前的写法来对文件进行读写,不需要做任何变更和适配。但同时,刚才提到的那两个“好处”也就不存在了。这个目录中的文件会被计入到应用程序的占用空间当中,同时也会随着应用程序的卸载而被删除。
那么有些朋友可能会问了,我就是需要访问其他目录该怎么办呢?比如读取手机相册中的图片,或者向手机相册中添加一张图片。为此,Android系统针对文件类型进行了分类,图片、音频、视频这三类文件将可以通过MediaStore API来进行访问,而其他类型的文件则需要使用系统的文件选择器来进行访问。
我一定要升级吗?
一定会有很多朋友关心这个问题,因为每当适配升级面临着需要更改大量代码的时候,大多数人的第一想法都是能不升就不升,或者能晚升就晚升。而在作用域存储这个功能上面,恭喜大家,暂时确实是可以不用升级的。
目前Android 10系统对于作用域存储适配的要求还不是那么严格,毕竟之前传统外置存储空间的用法实在是太广泛了。如果你的项目指定的targetSdkVersion低于29,那么即使不做任何作用域存储方面的适配,你的项目也可以成功运行到Android 10手机上。
而如果你的targetSdkVersion已经指定成了29,也没有关系,假如你还不想进行作用域存储的适配,只需要在AndroidManifest.xml中加入如下配置即可:
<manifest ... >
<application android:requestLegacyExternalStorage="true" ...>
...
</application>
</manifest>
这段配置表示,即使在Android 10系统上,仍然允许使用之前遗留的外置存储空间的用法来运行程序,这样就不用对代码进行任何修改了。当然,这只是一种权宜之计,在未来的Android系统版本中,这段配置随时都可能会失效(目前Android 11预览版已经确认,这段配置至少在Android 11上不会失效)。
存储权限
Android Q仍然使用READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE作为存储相关运行时权限,但现在即使获取了这些权限,访问外部存储也受到了限制,只能访问自身目录下的文件和公共内体文件。
外部存储结构划分
- 公有目录:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等 地址:/storage/emulated/0/Downloads(Pictures)等 公有目录下的文件不会跟随APP卸载而删除。
- APP私有目录 地址:/storage/emulated/0/Android/data/包名/files
分区存储
每个应用向自己的私有目录读写文件,不需要读写权限。私有文件目录具体路径: storage/emulated/0/android/data/packageName/ ,获取方法: Context#getExternalFilesDir()
应用即使获取了读写权限,也无法访问其他应用的私有目录。
当应用需要获取媒体文件时,通过 MediaStore API 向公共存储目录DCIM、Music或者Movie获取。同样写媒体文件也是如此。并且读写自己的文件时不需要申请权限。 只有读其他应用的媒体文件时才会需要申请READ_EXTERNAL_STORAGE权限。
(更新:Android11为目标平台时,可以使用文件直接路径去访问媒体,这是在Android10上没有的,应用的性能会略有下降,还是推荐使用MediaStore )
当应用需要获取其他非媒体文件时,比如doc、pdf文件,需要使用 系统的文件选择器SAF 来进行访问。
所以WRITE_EXTERNAL_STORAGE权限,在未来的Android11版本里,会被废弃。 (写文件不需要权限,只能在私有目录和公共目录写文件)
文件存储适配
1 获取创建自身目录下的文件夹
- 获取及创建,如果手机中没有对应的文件夹,则系统会自动生成
private void insertOwnerFile() {
//在自身目录下创建apk文件夹
String apkFilePath = getExternalFilesDir("apk").getAbsolutePath();
File newFile = new File(apkFilePath + File.separator + "temp.text");
OutputStream os = null;
try {
os = new FileOutputStream(newFile);
if (os != null) {
os.write("我是插入的数据".getBytes(StandardCharsets.UTF_8));
os.flush();
Toast.makeText(MainActivity.this, "創建陳工" + newFile.getAbsolutePath(), Toast.LENGTH_LONG).show();
} catch (IOException e) {
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException e1) {
}
}
}
2 创建并获取公共目录下的文件路径
private void insertFile() {
ContentResolver contentResolver = getContentResolver();
// Uri uri = MediaStore.Files.getContentUri("external");
Uri uri = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
ContentValues values = new ContentValues();
String path = Environment.DIRECTORY_DOWNLOADS + "/bug";
values.put(MediaStore.Downloads.RELATIVE_PATH, path);
values.put(MediaStore.Downloads.DISPLAY_NAME, "first_bug.text");
values.put(MediaStore.Downloads.TITLE, path);
Uri resultUri = contentResolver.insert(uri, values);
if (resultUri != null) {
Toast.makeText(MainActivity.this, "创建成功", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(MainActivity.this, "创建失败", Toast.LENGTH_LONG).show();
}
try {
OutputStream outputStream = contentResolver.openOutputStream(resultUri);
//将字符串转成字节
byte[] contentInBytes = "我是插入文件的内容".toString().getBytes();
outputStream.write(contentInBytes);
outputStream.flush();
outputStream.close();
Toast.makeText(MainActivity.this, "插入成功", Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
}
}
结合上面代码,我们主要是在公共目录下创建文件或文件夹拿到本地路径uri,不同的Uri,可以保存到不同的公共目录中。接下来使用输入输出流就可以写入文件
3 查询并读取公共目录下的文件
private void findFile() {
// Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
Uri extnerl = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
String selection = MediaStore.Downloads.DISPLAY_NAME + "=?";
String[] args = new String[]{"first_bug.text"};
String[] projections = new String[]{MediaStore.Downloads._ID};
Cursor cursor = getContentResolver().query(extnerl, projections, selection, args, null);
if (cursor.moveToFirst()) {
Uri queryUir = ContentUris.withAppendedId(extnerl, cursor.getLong(0));
Toast.makeText(MainActivity.this, "查询success" + queryUir, Toast.LENGTH_LONG).show();
cursor.close();
ContentResolver contentResolver = getContentResolver();
InputStream inputStream= null;
try {
inputStream = contentResolver.openInputStream(queryUir);
byte[] buffer = new byte[1024];
int len = 0;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((len = inputStream.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bos.close();
String str= new String ( bos.toByteArray());
tv_find_file.setText(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}
图片存储适配
保存图片的方式
private void insertImage() {
String disPlayName = "111.jpg";
ContentResolver contentResolver1 = getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, disPlayName);
contentValues.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpeg");
contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/wxq/");
Uri insert = contentResolver1.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
if (insert != null) {
Bitmap bitmap = getBitmap(this, R.mipmap.ic_launcher);
try {
OutputStream outputStream = contentResolver1.openOutputStream(insert);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
outputStream.close();
Toast.makeText(MainActivity.this, "插入图片成功", Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
}
}
}
搜索图片并获取图片
private void searchImage() {
Uri extnerl = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
String selection = MediaStore.Images.Media.DISPLAY_NAME + "=?";
String[] args = new String[]{"111.jpg"};
String[] projections = new String[]{MediaStore.Images.Media._ID};
Cursor cursor = getContentResolver().query(extnerl, projections, selection, args, null);
if (cursor.moveToFirst()) {
Uri queryUir = ContentUris.withAppendedId(extnerl, cursor.getLong(0));
Toast.makeText(MainActivity.this, "查询success" + queryUir, Toast.LENGTH_LONG).show();
cursor.close();
// 给图片设置bitmap
try {
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(queryUir, "r");
if (fd != null) {
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor());
fd.close();
iv_pic.setImageBitmap(bitmap);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
在AndroidQ中公有目录访问文件,File API 都无法访问,即:file://本地path操作文件,本地加载图片、上传、下载都不支持,只能通过uri来操作
如果下载图片到公共目录,无需再发送广播通知图片更新;
MediaStore.Images.Media.RELATIVE_PATH需要 targetSdkVersion=29 ,故该方法只可在Android10的手机上执行,如果在小于29的系统下调用RELATIVE_PATH会报错:
网络文件下载获取
// 下载图片到picture 目录
public void downLoadImage(final String fileUrl, final String fileName) {
new Thread(new Runnable() {
@RequiresApi(api = Build.VERSION_CODES.Q)
@Override
public void run() {
// TODO Auto-generated method stub
URL url = null;
try {
url = new URL(fileUrl);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
InputStream inputStream = httpURLConnection.getInputStream();
BufferedInputStream bis = new BufferedInputStream(inputStream);
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
final Uri uri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
OutputStream outputStream = getContentResolver().openOutputStream(uri);
if (outputStream != null) {
BufferedOutputStream bos = new BufferedOutputStream(outputStream);
if (copyFileWithStream(bos, inputStream)) { //成功下载图片
runOnUiThread(new Runnable() {
@Override
public void run() {
try {
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(uri, "r");
if (fd != null) {
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor());
fd.close();
iv_pic.setImageBitmap(bitmap);
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
}
bis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private static boolean copyFileWithStream(OutputStream os, InputStream is) {
if (os == null || is == null) {
return false;
}
int read = 0;
while (true) {
try {
byte[] buffer = new byte[1444];
while ((read = is.read(buffer)) != -1) {
os.write(buffer, 0, read);
os.flush();
}
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
try {
os.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}