Android-存儲基礎(chǔ)

前言

存儲適配系列文章:

Android-存儲基礎(chǔ)
Android-10、11-存儲完全適配(上)
Android-10、11-存儲完全適配(下)
Android-FileProvider-輕松掌握

在持久化數(shù)據(jù)的時候,一般都是選擇存入到文件里,本篇將著重分析Android 存儲相關(guān)的知識,也是為Android 10.0 11存儲適配打基礎(chǔ)。
通過本篇文章,你將了解到:

1、存儲劃分
2、內(nèi)部存儲
3、外部存儲
4、易混淆點說明

1、存儲劃分

Android 4.4 之前

在Android 4.4 之前,由于硬件發(fā)展受限,手機自身的存儲空間有限,需要通過外置SD卡來擴(kuò)展存儲空間。


image.png

如上圖,手機自身的存儲空間,稱之為機身存儲,在Android 4.4 之前作為內(nèi)部存儲使用。當(dāng)然內(nèi)部存儲空間一般是不夠用的,所以需要通過插入外置SD卡來擴(kuò)充存儲空間,這當(dāng)做外部存儲。

Android 4.4之后

在Android 4.4 之后(含),手機機身存儲擴(kuò)大了:


image.png

如上圖,機身存儲劃分為兩部分:

1、內(nèi)部存儲
2、外部存儲

當(dāng)然,依然可以插入SD卡來擴(kuò)充存儲空間,這部分的存儲空間稱為擴(kuò)展的外部存儲空間。只是現(xiàn)在機身存儲都比較大,很少插入SD卡了。
接下來將以Android 4.4 之后的存儲劃分來分析具體的存儲方案。

2、內(nèi)部存儲

存放位置

回想一下平時使用的持久化方案:

1、SharedPreferences---->適用于存儲小文件
2、數(shù)據(jù)庫---->存儲結(jié)構(gòu)比較復(fù)雜的大文件

以上這些文件都是默認(rèn)放在內(nèi)部存儲里。
"/" 表示根目錄,內(nèi)部存儲里給每個應(yīng)用按照其包名各自劃分了目錄,假設(shè)App的包名為:com.fish.myapplication
那么該文件在內(nèi)部存儲里的目錄為:
/data/user/0/com.fish.myapplication/

第一個"/"表示根目錄,其后每個"/"表示目錄分割符。
"0" 表示是第一個用戶,后續(xù)添加了多用戶則生成相應(yīng)的用戶目錄:


image.png

如上圖,新增了兩個用戶,生成的目錄分別是:"11"、"12"。目前來說,很少開啟多用戶的。
一般來說,adb shell里是沒有權(quán)限查看/data目錄的。若要查看內(nèi)部存儲,通常是通過Android Studio側(cè)邊欄Device File Explorer選擇對應(yīng)的目標(biāo)設(shè)備查看。


image.png

同樣的,如果包名為:com.fish.myapplication,則對應(yīng)的內(nèi)部存儲目錄為:
/data/data/com.fish.myapplication/

/data/user/0/com.fish.myapplication/ 會將值轉(zhuǎn)換到/data/data/com.fish.myapplication/ 路徑下。
每個App的內(nèi)部存儲空間僅允許自己訪問(除非有更高的權(quán)限,如root),程序卸載后,該目錄也會被刪除。

存儲內(nèi)容

除了SharedPreferences、數(shù)據(jù)庫文件,內(nèi)部存儲還存放了哪些文件呢?
為方便起見,只查看/data/data/目錄下的。


image.png

剛開始有只有兩個空目錄。
當(dāng)進(jìn)行寫入SharedPreferences,創(chuàng)建數(shù)據(jù)庫、寫入文件等操作后新增了幾個目錄:


image.png

大致介紹一下以上目錄作用:

1、cache-->存放緩存文件
2、code_cache-->存放運行時代碼優(yōu)化等產(chǎn)生的緩存
3、databases-->存放數(shù)據(jù)庫文件
4、files-->存放一般文件
5、shared_prefs-->存放SharedPreferences 文件
6、lib-->存放App依賴的so庫 是軟鏈接,指向/data/app/ 某個子目錄下

訪問方式

既然知道了各類文件存儲的目錄,那么如何讀寫這些文件呢?
我們知道在Java 的世界里,操作文件有兩種方式:

字符流和字節(jié)流

以字節(jié)流為為例,一個簡單的讀取寫入文件Demo:

    //寫入文件
    private void writeFile(String filePath) {
        if (TextUtils.isEmpty(filePath))
            return;

        try {
            File file = new File(filePath);
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);
            String writeContent = "hello world\n";
            bos.write(writeContent.getBytes());
            bos.flush();
            bos.close();

        } catch (Exception e) {

        }
    }

    //從文件讀取
    private void readFile(String filePath) {
        if (TextUtils.isEmpty(filePath))
            return;

        try {
            File file = new File(filePath);
            FileInputStream fileInputStream = new FileInputStream(file);
            BufferedInputStream bis = new BufferedInputStream(fileInputStream);
            byte[] readContent = new byte[1024];
            int readLen = 0;
            while (readLen != -1) {
                readLen = bis.read(readContent, 0, readContent.length);
                if (readLen > 0) {
                    String content = new String(readContent);
                    Log.d("test", "read content:" + content.substring(0, readLen));
                }
            }
            fileInputStream.close();
        } catch (Exception e) {

        }
    }

可以看出,通過FileInputStream/FileOutputStream構(gòu)造函數(shù)傳入File對象即可實現(xiàn)文件讀寫,而File對象的構(gòu)造依賴于文件的存放路徑,因此重點在于如何獲取文件的路徑。
分別說明各個目錄下文件的讀寫:
1、讀寫files目錄下文件

#Context.java
public abstract File getFilesDir();

使用方式:

    private String getFilePath(Context context) {
        //獲取files根目錄
        File fileDir = context.getFilesDir();
        //獲取文件
        File myFile = new File(fileDir, "myFile");
        return myFile.getAbsolutePath();
    }

context.getFilesDir()的結(jié)果是返回files目錄:

/data/user/0/com.fish.myapplication/files/

拿到對應(yīng)文件的File對象后,構(gòu)造相應(yīng)的輸入輸出流即可實現(xiàn)對該文件的讀寫。可以看出,過程雖然簡單但是有點枯燥,因此Google將這些步驟封裝好了,直接返回對應(yīng)文件的FileOutputStream/FileInputStream:

#Context.java
    public abstract FileInputStream openFileInput(String name)
        throws FileNotFoundException;

    public abstract FileOutputStream openFileOutput(String name, @FileMode int mode)
        throws FileNotFoundException;

其中name 表示文件名,mode表示訪問權(quán)限。

2、讀寫cache目錄下文件
與讀取files目錄相似:

#Context.java
public abstract File getCacheDir();

context.getCacheDir()的結(jié)果是返回cache目錄:

/data/user/0/com.fish.myapplication/cache/

3、讀寫shared_prefs目錄下文件
SharedPreferences 提供了簡易的快速持久化數(shù)據(jù)的方案。

    private void testSP(String fileName, String key, String value) {
        if (TextUtils.isEmpty(fileName) || TextUtils.isEmpty(key) || TextUtils.isEmpty(value))
            return;

        //構(gòu)造SP文件
        SharedPreferences sp = getSharedPreferences(fileName, MODE_PRIVATE);

        //寫入SP
        sp.edit().putString(key, value).commit();

        //讀取SP
        String myValue = sp.getString(key, "");
    }

其內(nèi)部也是使用了輸入輸出流,以寫入SP文件為例:

#SharedPreferencesImpl.java
    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        ...
        //構(gòu)造輸出流
        FileOutputStream str = createFileOutputStream(mFile);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        FileUtils.sync(str);
        str.close();
        ...
    }

4、讀寫數(shù)據(jù)庫目錄下文件
創(chuàng)建數(shù)據(jù)庫:

MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(v.getContext(), "myDB", null, 10);

myDB是數(shù)據(jù)庫文件名。打開數(shù)據(jù)庫的相應(yīng)表,即可讀寫數(shù)據(jù)。
獲取數(shù)據(jù)庫文件路徑:

#Context.java
Context.public abstract File getDatabasePath(String name);

獲取結(jié)果如下:

/data/user/0/com.fish.myapplication/databases/myDB

5、讀寫code_cache目錄下文件

#Context.java API>=21
public abstract File getCodeCacheDir();

獲取結(jié)果如下:

/data/user/0/com.fish.myapplication/code_cache/

以上是分別列舉了各個子目錄/文件的獲取方式,如果想獲取:/data/user/0/com.fish.myapplication/,可通過:

#Context.java
public abstract File getDataDir();

該方法需要API>=24。

3、外部存儲

外部存儲分為兩部分:自帶外部存儲和擴(kuò)展外部存儲(外置SD卡)

A、自帶外部存儲存儲

存放位置

存儲的根目錄是:"/"。
根目錄下幾個需要關(guān)注的目錄:

/data/
/sdcard/
/storage/

其中/data/目錄前面已經(jīng)分析過。

/sdcard/是軟鏈接,指向/storage/self/primary
而/storage/下有幾個目錄:


image.png

/storage/self/primary/是軟鏈接,指向/storage/emulated/0/

也就是說/sdcard/、/storage/self/primary/ 真正指向的是/storage/emulated/0/

存儲內(nèi)容

image.png

如上圖所示,/sdcard/目錄下的子目錄看起來都比較眼熟。
這些子目錄分為分為三部分:

第一部分:共享存儲空間

也就是所有App共享的部分,比如相冊、音樂、鈴聲、文檔等。
共享存儲空間按文件類型又分為兩部分:
1、媒體文件

  • DCIM/ 和 Pictures/-->存儲圖片
  • DCIM/、Movies/ 和 Pictures-->存儲視頻
  • Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/-->存儲音頻文件
  • Download/-->下載的文件

2、文檔和其它文件

Documents-->存儲如.pdf類型等文件

第二部分:App外部私有目錄

  • Android/data/--->存儲各個App的外部私有目錄
    與內(nèi)部存儲類似,命名方式是:Android/data/xx------>xx指應(yīng)用的包名。
    如:/sdcard/Android/data/com.fish.myapplication

第三部分:其它目錄

比如各個App在/sdcard/目錄下創(chuàng)建的目錄,如支付寶創(chuàng)建的目錄:alipy/,微博創(chuàng)建的目錄:com.sina.weibo/,qq創(chuàng)建的目錄:com.tencent.mobileqq/等。

訪問方式

與訪問內(nèi)部存儲文件類似,外部存儲也可以通過構(gòu)造輸入輸出流訪問文件。

讀寫共享存儲空間

視頻、圖片等可能分散存儲在各個不同的目錄里,如果想要獲取所有的圖片地址,那么得需要遍歷不同的目錄尋找,效率顯而易見的低。Android 將視頻、圖片等信息存儲在數(shù)據(jù)庫里,每當(dāng)某個App想要訪問這些共享的媒體文件時只需要查找數(shù)據(jù)庫對應(yīng)的表,讀取符合條件的行,找出每個媒體的文件路徑等信息。
App查詢共享存儲空間的媒體方式是:通過ContentProvider訪問。

訪問媒體文件
以查詢圖片為例:

    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));
        }
    }

查詢到圖片的地址,當(dāng)然就可以展示圖片了。

訪問文檔和其它文件
Storage Access Framework 簡稱SAF:存儲訪問框架
以查看.pdf文件為例:

    private void startSAF() {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("application/pdf");
        startActivityForResult(intent, 100);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == 100) {
            Uri uri = data.getData();
        }
    }

SAF實際上就是調(diào)用系統(tǒng)提供的選擇器,選中后在onActivityResult(xx)里接收結(jié)果,拿到Uri后當(dāng)然就可以讀寫對應(yīng)的文件了。

讀寫App外部私有目錄

剛開始并沒有自己App的包名。


image.png

調(diào)用如下方法后:

    private void testAppDir(Context context) {
        //4個基本方法
        File fileDir = context.getExternalFilesDir(null);
        //API>=19
        File[] fileList = context.getExternalFilesDirs(null);

        File cacheDir = context.getExternalCacheDir();
        //API>=19
        File[] cacheList = context.getExternalCacheDirs();

        //指定目錄,自動生成對應(yīng)的子目錄
        File fileDir2 = context.getExternalFilesDir(Environment.DIRECTORY_DCIM);
    }

再查看目錄樹:


image.png

可以看出再/sdcard/Android/data/目錄下生成了com.fish.myapplication/目錄,該目錄下有兩個子目錄分別是:files/、cache/。當(dāng)然也可以選擇創(chuàng)建其它目錄。
2、App卸載的時候,兩者都會被清除。

讀寫其它目錄

只要拿到根目錄,就可以遍歷尋找其它子目錄/文件。

    private void testOtherDir(Context context) {
        File rootDir = Environment.getExternalStorageDirectory();
    }

返回的rootDir路徑:/storage/emulated/0/。

B、擴(kuò)展外部存儲(外置SD卡)

存儲位置

當(dāng)給設(shè)備插入SD卡后,查看其目錄:
/sdcard/ 依然指向/storage/self/primary,繼續(xù)來看/storage/:


image.png

可以看出,多了sdcard1,軟鏈接指向了/storage/77E4-07E7/。

存儲內(nèi)容

取決于SD卡上裝了什么東西。

訪問方式

還記得上面獲取外部存儲-App私有目錄方式嗎?

File[] fileList = context.getExternalFilesDirs(null);

返回File對象數(shù)組,當(dāng)有多個外部存儲時候,存儲在數(shù)組里。


image.png

返回的數(shù)組有兩個元素,一個是自帶外部存儲存儲,另一個是剛插入的SD卡。
拿到路徑后,當(dāng)然就可以訪問相應(yīng)的文件了。

4、易混淆點說明

以上分別闡述了內(nèi)部存儲、自帶外部存儲、擴(kuò)展外部存儲等,這幾者關(guān)系如下:


image.png

其中比較容易混淆的是:
內(nèi)部存儲與外部存儲里的App私有目錄,兩者命名風(fēng)格很像。

不同點:

/data/data/com.fish.myapplication/ 位于內(nèi)部存儲,一般用于存儲容量較小的,私密性較強的文件。而/sdcard/Android/data/com.fish.myapplication/ 位于外部存儲,作為App私有目錄,一般用于存儲容量較大的文件,即使刪除了也不影響App正常功能。

相同點:

1、屬于App專屬,App自身訪問兩者無需任何權(quán)限。
2、App卸載后,兩者皆被刪除。
3、兩者目錄下增加的文件最終會被統(tǒng)計到"設(shè)置->存儲和緩存"里。

另外,常見的在設(shè)置里的"存儲與緩存"項:


image.png

當(dāng)點擊"Clear cache" 時:

內(nèi)部存儲/data/data/com.fish.myapplication/cache/、 /data/data/com.fish.myapplication/code_cache/目錄會被清空
外部存儲/sdcard/Android/data/com.fish.myapplication/cache/ 會被清空

當(dāng)點擊"Clear storage" 時:

內(nèi)部存儲/data/data/com.fish.myapplication/下除了lib/,其余子目錄皆被刪除
外部存儲/sdcard/Android/data/com.fish.myapplication/被清空
\color{Red}{注:該功能慎用,因為會刪除用戶數(shù)據(jù)庫,SP文件等,相當(dāng)于重置了App}

接下來將分析Android 10.0 11 存儲適配。
本文基于Android 10.0。

您若喜歡,請點贊、關(guān)注,您的鼓勵是我前進(jìn)的動力

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,844評論 2 372

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