Android-10沉衣、11-存儲(chǔ)完全適配(上)

前言

存儲(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ū)域劃分:


image.png

其中哲戚,以下目錄無(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)限后龟再,提示用戶作出選擇:


image.png

訪問(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();
        }
    }
image.png

可以看出融涣,/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ō)話:


image.png
image.png

可以看出/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曾雕。

您若喜歡奴烙,請(qǐng)點(diǎn)贊、關(guān)注翻默,您的鼓勵(lì)是我前進(jìn)的動(dòng)力

持續(xù)更新中缸沃,和我一起步步為營(yíng)系統(tǒng)恰起、深入學(xué)習(xí)Android

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末修械,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子检盼,更是在濱河造成了極大的恐慌肯污,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蹦渣,居然都是意外死亡哄芜,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門柬唯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)认臊,“玉大人,你說(shuō)我怎么就攤上這事锄奢∈纾” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵拘央,是天一觀的道長(zhǎng)涂屁。 經(jīng)常有香客問(wèn)我,道長(zhǎng)灰伟,這世上最難降的妖魔是什么拆又? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮栏账,結(jié)果婚禮上帖族,老公的妹妹穿的比我還像新娘。我一直安慰自己发笔,他們只是感情好盟萨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著了讨,像睡著了一般捻激。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上前计,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天胞谭,我揣著相機(jī)與錄音,去河邊找鬼男杈。 笑死丈屹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的伶棒。 我是一名探鬼主播旺垒,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼肤无!你這毒婦竟也來(lái)了先蒋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤宛渐,失蹤者是張志新(化名)和其女友劉穎竞漾,沒(méi)想到半個(gè)月后眯搭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡业岁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年鳞仙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笔时。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡棍好,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出允耿,到底是詐尸還是另有隱情梳玫,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布右犹,位于F島的核電站提澎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏念链。R本人自食惡果不足惜盼忌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掂墓。 院中可真熱鬧谦纱,春花似錦、人聲如沸君编。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吃嘿。三九已至祠乃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間兑燥,已是汗流浹背亮瓷。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留降瞳,地道東北人嘱支。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像挣饥,于是被迫代替她去往敵國(guó)和親除师。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容