前言
存儲(chǔ)適配系列文章:
Android-存儲(chǔ)基礎(chǔ)
Android-10斑鸦、11-存儲(chǔ)完全適配(上)
Android-10嘱巾、11-存儲(chǔ)完全適配(下)
Android-FileProvider-輕松掌握
上篇文章分析了Android 存儲(chǔ)相關(guān)的基礎(chǔ)知識(shí)菌湃,說(shuō)到了各個(gè)目錄下文件的訪問(wèn)方式。本篇將著重分析Android 系統(tǒng)版本變更對(duì)存儲(chǔ)訪問(wèn)權(quán)限的影響及其適配方法。
通過(guò)本篇文章障簿,你將了解到:
1世蔗、存儲(chǔ)基本知識(shí)
2寸爆、Android 10.0 之前訪問(wèn)方式
3魔种、Android 10.0 訪問(wèn)方式變更
4、如何不適配Android 10.0
1、存儲(chǔ)基本知識(shí)
先來(lái)看看存儲(chǔ)區(qū)域劃分:
其中哲戚,以下目錄無(wú)需存儲(chǔ)權(quán)限即可訪問(wèn):
1、App自身的內(nèi)部存儲(chǔ)
2、App自身的自帶外部存儲(chǔ)-私有目錄
剩下的都需要申請(qǐng)存儲(chǔ)權(quán)限喇潘,Android 10.0前后對(duì)于存儲(chǔ)作用域訪問(wèn)的區(qū)別就體現(xiàn)在如何訪問(wèn)剩余這些目錄內(nèi)的文件忱屑。
重點(diǎn)在自帶外部存儲(chǔ)之共享存儲(chǔ)空間和其它目錄
2瘪校、Android 10.0 之前訪問(wèn)方式
繼續(xù)細(xì)分為Android 6.0 之前和之后。
Android 6.0 之前訪問(wèn)方式
Android 6.0 之前是無(wú)需申請(qǐng)動(dòng)態(tài)權(quán)限的,在AndroidManifest.xml 里聲明存儲(chǔ)權(quán)限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
就可以訪問(wèn)共享存儲(chǔ)空間、其它目錄下的文件了。
Android 6.0 之后的訪問(wèn)方式
動(dòng)態(tài)申請(qǐng)權(quán)限
Android 6.0 后需要?jiǎng)討B(tài)申請(qǐng)權(quán)限埂陆,除了在AndroidManifest.xml 里聲明存儲(chǔ)權(quán)限外,還需要在代碼里動(dòng)態(tài)申請(qǐng)私股。
//檢查權(quán)限,并返回需要申請(qǐng)的權(quán)限列表
private List<String> checkPermission(Context context, String[] checkList) {
List<String> list = new ArrayList<>();
for (int i = 0; i < checkList.length; i++) {
if (PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(context, checkList[i])) {
list.add(checkList[i]);
}
}
return list;
}
//申請(qǐng)權(quán)限
private void requestPermission(Activity activity, String requestPermissionList[]) {
ActivityCompat.requestPermissions(activity, requestPermissionList, 100);
}
//用戶作出選擇后着帽,返回申請(qǐng)的結(jié)果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == 100) {
for (int i = 0; i < permissions.length; i++) {
if (permissions[i].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(MainActivity.this, "存儲(chǔ)權(quán)限申請(qǐng)成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "存儲(chǔ)權(quán)限申請(qǐng)失敗", Toast.LENGTH_SHORT).show();
}
}
}
}
}
//測(cè)試申請(qǐng)存儲(chǔ)權(quán)限
private void testPermission(Activity activity) {
String[] checkList = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE};
List<String> needRequestList = checkPermission(activity, checkList);
if (needRequestList.isEmpty()) {
Toast.makeText(MainActivity.this, "無(wú)需申請(qǐng)權(quán)限", Toast.LENGTH_SHORT).show();
} else {
requestPermission(activity, needRequestList.toArray(new String[needRequestList.size()]));
}
}
申請(qǐng)權(quán)限后龟再,提示用戶作出選擇:
訪問(wèn)文件
權(quán)限申請(qǐng)成功后,即可對(duì)自帶外部存儲(chǔ)之共享存儲(chǔ)空間和其它目錄進(jìn)行訪問(wèn)适荣。
分別以共享存儲(chǔ)空間和其它目錄為例,闡述訪問(wèn)方式:
訪問(wèn)共享存儲(chǔ)空間
共享存儲(chǔ)空間分為兩類文件:媒體文件和文檔/其它文件万俗。
訪問(wèn)媒體文件
目的是拿到媒體文件的路徑公条,有兩種方式獲取路徑:
1、直接構(gòu)造路徑
以圖片為例零渐,假設(shè)圖片存儲(chǔ)在/sdcard/Pictures/目錄下。
private void testShareMedia() {
//獲取目錄:/storage/emulated/0/
File rootFile = Environment.getExternalStorageDirectory();
String imagePath = rootFile.getAbsolutePath() + File.separator + Environment.DIRECTORY_PICTURES + File.separator + "myPic.png";
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
}
如上,myPic.png的路徑:/storage/emulated/0/Pictures/myPic.png太示,拿到路徑后就可以解析并獲取Bitmap。
2驮瞧、通過(guò)MediaStore獲取路徑
沿用上篇的demo:
private void getImagePath(Context context) {
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);
while(cursor.moveToNext()) {
String imagePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
break;
}
}
同樣的,也是拿到圖片路徑后獲取Bitmap。
還有一種不直接通過(guò)路徑訪問(wèn)的方法:
3、通過(guò)MediaStore獲取Uri
private void getImagePath(Context context) {
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);
while(cursor.moveToNext()) {
//獲取唯一的id
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
//通過(guò)id構(gòu)造Uri
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
openUri(uri);
break;
}
}
與直接拿到路徑不同的是忆家,此處拿到的是Uri。圖片的信息封裝在Uri里艰躺,通過(guò)Uri構(gòu)造出InputStream页响,再進(jìn)行圖片解碼拿到Bitmap
訪問(wèn)文檔和其它文件
1、直接構(gòu)造路徑
與媒體文件一樣段誊,可以直接構(gòu)造路徑訪問(wèn)闰蚕。
2、通過(guò)SAF訪問(wèn)
Storage Access Framework 簡(jiǎn)稱SAF:存儲(chǔ)訪問(wèn)框架连舍。相當(dāng)于系統(tǒng)內(nèi)置了文件選擇器没陡,通過(guò)它可以拿到想要訪問(wèn)的文件信息。
同樣的以獲取圖片為例:
private void startSAF() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
//選擇圖片
intent.setType("image/jpeg");
startActivityForResult(intent, 100);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100) {
//選中返回的圖片封裝在uri里
Uri uri = data.getData();
openUri(uri);
}
}
private void openUri(Uri uri) {
try {
//從uri構(gòu)造輸入流
InputStream fis = getContentResolver().openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(fis);
} catch (Exception e) {
}
}
可以看出索赏,通過(guò)SAF并不能直接拿到圖片的路徑盼玄,圖片的信息封裝在Uri里,通過(guò)Uri構(gòu)造出InputStream潜腻,再進(jìn)行圖片解碼拿到Bitmap埃儿。
訪問(wèn)其它目錄
有兩種方式:
1、直接構(gòu)造路徑
在/sdcard/目錄下直接創(chuàng)建目錄:
private void testPublicFile() {
File rootFile = Environment.getExternalStorageDirectory();
String imagePath = rootFile.getAbsolutePath() + File.separator + "myDir";
File myDir = new File(imagePath);
if (!myDir.exists()) {
myDir.mkdir();
}
}
可以看出融涣,/sdcard/myDir/目錄創(chuàng)建成功童番。
2、通過(guò)SAF訪問(wèn)
與共享存儲(chǔ)空間SAF訪問(wèn)方式一致威鹿。
Android 10.0 之前訪問(wèn)方式總結(jié)
由上面分析的共享存儲(chǔ)空間/其它目錄訪問(wèn)方式可知妓盲,訪問(wèn)目錄/文件可通過(guò)如下兩個(gè)方法:
1、通過(guò)路徑訪問(wèn)专普。路徑可以直接構(gòu)造也可以通過(guò)MediaStore獲取悯衬。
2、通過(guò)Uri訪問(wèn)。Uri可以通過(guò)MediaStore或者SAF獲取筋粗。
Android 6.0 以下訪問(wèn)共享存儲(chǔ)空間/其它目錄步驟:
1策橘、AndroidManifest.xml里聲明存儲(chǔ)權(quán)限
2、通過(guò)路徑或者Uri訪問(wèn)文件
Android 6.0(含)~Android 10.0(不含)訪問(wèn)共享存儲(chǔ)空間/其它目錄步驟:
1娜亿、AndroidManifest.xml里聲明存儲(chǔ)權(quán)限
2丽已、動(dòng)態(tài)申請(qǐng)存儲(chǔ)權(quán)限
3、通過(guò)路徑或者Uri訪問(wèn)文件
3买决、Android 10.0 訪問(wèn)方式變更
為什么要變更
你可能已經(jīng)發(fā)現(xiàn)了上面訪問(wèn)方式的弊端沛婴,比如我們能夠直接在/sdcard/目錄下創(chuàng)建目錄/文件。事實(shí)上督赤,很多App就是這么干的嘁灯,看圖說(shuō)話:
可以看出/sdcard/目錄下,如淘寶躲舌、qq丑婿、qq瀏覽器、微博没卸、支付寶等都自己建了目錄羹奉。
這么看來(lái),導(dǎo)致目錄結(jié)構(gòu)很亂约计,而且App卸載后诀拭,對(duì)應(yīng)的目錄并沒(méi)有刪除,于是就是遺留了很多"垃圾"文件煤蚌,久而久之不處理耕挨,用戶的存儲(chǔ)空間越來(lái)越小。
總結(jié)弊端如下:
1铺然、在設(shè)置里"Clear storage"或者"Clear cache"并不能刪除該目錄下的文件
2、卸載App也不能刪除該目錄下的文件
3酒甸、App可以隨意修改其它目錄下的文件魄健,如修改別的App創(chuàng)建的文件等,不安全
你也許會(huì)問(wèn)插勤,為什么要在/sdcard/目錄下新建自己的目錄呢沽瘦?
大體有以下兩個(gè)原因:
1、此處新建的目錄不會(huì)被設(shè)置里的App存儲(chǔ)用量統(tǒng)計(jì)农尖,讓用戶"看起來(lái)"自己的App占用的存儲(chǔ)空間很小
2析恋、方便操作文件
如何變更
面對(duì)眾多App不講"碼德"隨意新建目錄/文件的現(xiàn)象,Google在Android 10.0上重拳出擊了盛卡。
引入Scoped Storage
翻譯成中文有好幾個(gè)版本:作用域存儲(chǔ)助隧、分區(qū)存儲(chǔ)、沙盒存儲(chǔ)滑沧。
具體中文翻譯不重要并村,下面以分區(qū)存儲(chǔ)指代巍实。
分區(qū)存儲(chǔ)原理:
1、App訪問(wèn)自身內(nèi)部存儲(chǔ)空間哩牍、訪問(wèn)外部存儲(chǔ)空間-App私有目錄不需要任何權(quán)限(這個(gè)與Android 10.0之前一致)
2棚潦、外部存儲(chǔ)空間-共享存儲(chǔ)空間、外部存儲(chǔ)空間-其它目錄 App無(wú)法通過(guò)路徑直接訪問(wèn)膝昆,不能新建丸边、刪除、修改目錄/文件等
3荚孵、外部存儲(chǔ)空間-共享存儲(chǔ)空間妹窖、外部存儲(chǔ)空間-其它目錄 需要通過(guò)Uri訪問(wèn)
分區(qū)存儲(chǔ)的變更在于第二點(diǎn)、第三點(diǎn)处窥。
為什么Uri能夠訪問(wèn)
先來(lái)看為什么通過(guò)路徑無(wú)法直接訪問(wèn)嘱吗。
我們知道訪問(wèn)文件最終是通過(guò)構(gòu)造InputStream/OutputStream來(lái)實(shí)現(xiàn)的,以InputStream為例滔驾,看看其構(gòu)造方法:
#FileInputStream.java
//文件描述符
private final FileDescriptor fd;
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
...
//傳入name谒麦,構(gòu)造FileDescriptor
//沒(méi)有權(quán)限訪問(wèn),則此處拋出異常
fd = IoBridge.open(name, O_RDONLY);
...
}
可以看出哆致,要想FileInputStream 能讀入文件绕德,核心是需要構(gòu)造FileDescriptor,而對(duì)于Android 10.0摊阀,直接通過(guò)路徑構(gòu)造FileDescriptor 會(huì)拋出異常耻蛇。
那么我們自然會(huì)想到,有沒(méi)有通過(guò)構(gòu)造好的FileDescriptor 來(lái)生成FileInputStream對(duì)象胞此,進(jìn)而使用read(xx)方法讀取數(shù)據(jù)臣咖。
還真有,請(qǐng)看:通過(guò)Uri構(gòu)造InputStream漱牵。
InputStream fis = getContentResolver().openInputStream(uri);
進(jìn)入看其源碼:
#ContentResolver.java
public final @Nullable
InputStream openInputStream(@NonNull Uri uri)
throws FileNotFoundException {
...
if (SCHEME_ANDROID_RESOURCE.equals(scheme)) {
...
} else if (SCHEME_FILE.equals(scheme)) {
...
} else {
//通過(guò)Uri構(gòu)造fd是被允許的
AssetFileDescriptor fd = openAssetFileDescriptor(uri, "r", null);
try {
//反過(guò)來(lái)創(chuàng)建InputStream
return fd != null ? fd.createInputStream() : null;
} catch (IOException e) {
throw new FileNotFoundException("Unable to create stream");
}
}
}
AssetFileDescriptor 持有ParcelFileDescriptor 引用夺蛇,而ParcelFileDescriptor 持有FileDescriptor 引用。
同理也適用于FileOutputStream酣胀。因此刁赦,通過(guò)Uri能夠訪問(wèn)文件。
4闻镶、如何不適配Android 10.0
從以上分析可知甚脉,適配Android 10.0 有點(diǎn)麻煩,問(wèn)題來(lái)了有沒(méi)有簡(jiǎn)單的方法繞過(guò)檢測(cè)铆农。
第一種方法
1牺氨、Android 10.0 及其以后才會(huì)有分區(qū)存儲(chǔ)功能,只要Android 設(shè)備不升級(jí)系統(tǒng)到Android 10.0以后,就不會(huì)有問(wèn)題波闹。
2酝豪、可能覺得這是句廢話,其實(shí)不然精堕,有些定制的設(shè)備系統(tǒng)一般都不會(huì)升級(jí)的孵淘。
如果不能使用第一種方法,還可以采用第二種方法歹篓。
第二種方法
1瘫证、Android 一般升級(jí)功能的時(shí)候都會(huì)配合targetSdkVersion使用。只要targetSdkVersion<=28庄撮,分區(qū)存儲(chǔ)功能就不會(huì)開啟背捌。
有關(guān)targetSdkVersion 作用請(qǐng)移步:targetSdkVersion、compileSdkVersion洞斯、minSdkVersion作用與區(qū)別
如果第二種方法也不能使用毡庆,則還有第三種方法。
第三種方法
在AndroidManifest.xml 里application標(biāo)簽下添加:
android:requestLegacyExternalStorage="true" 可禁用分區(qū)存儲(chǔ)
從長(zhǎng)遠(yuǎn)的角度看烙如,以上三個(gè)方法都不是一勞永逸的方法么抗,其中第二種、第三種方法是Google 留給App開發(fā)者適配的緩沖時(shí)間亚铁。
對(duì)于第二種方法:
Google 在App上架App Store 時(shí)候可能會(huì)強(qiáng)制要求升級(jí)targetSdkVersion蝇刀,因此該方法不保險(xiǎn)。
對(duì)于第三種方法:
在Android 11會(huì)忽略該字段徘溢,強(qiáng)制開啟分區(qū)存儲(chǔ)吞琐,該字段也不怎么靠譜。
因此然爆,最終還是需要老老實(shí)實(shí)按照Google 的要求適配Android 10.0站粟,下篇將重點(diǎn)分析Android 10.0/11 該如何來(lái)適配。
本文基于Android 10.0曾雕。