Android存儲(1)-- 你還在亂用Android存儲嘛!!!

簡介

Q: 爲何會單獨詳解Android存儲這個模塊? A: 進一步分析Google對Android存儲模塊的設計,最初只支持內部存儲,到後來國內ROM廠商各自改造,人爲的把內部存儲分爲Internal,External致使開發者對Androi存儲系統產生不少迷惑。同時破壞了Android原有的生態系統,Google一直對這塊都沒有發佈更新,直到Android6.0引入了適配存儲(Adoptable Storage)的方式來支持外置的SD卡。html

Q: 現有Android存儲模塊亂用現象有哪些? A: 一、隨意建立文件夾 二、隨意訪問別的應用數據 三、私有數據和公有數據沒有明顯區分java

存儲區概念

全部的Android設備都有兩塊存儲區域:Internal StorageExternal Storage。它們的名稱來源於早期的Android系統,那時候你們的手機都內置(Permanent)一塊較小存儲板(即Internal Storage),並配上一個的外置的(Removable)儲存卡(即External Storage)。後來部分手機開始將最初定義的Internal Storage,即內置存儲,分紅InternalExternal兩部分。這樣一來就算沒有外置儲存,手機也有InternalExternal兩塊存儲區域。當手機能夠插入外置SD卡的時候,這個時候插入的SD卡被稱做Physical External Storageandroid

這兩塊存儲區域的區別是:git

Internal Storage External Storage
可信度 永遠可用(Permanent) 可能不可用,最典型的當設備做爲USB存儲被mount時不可用

封裝庫(對內外存儲經常使用API封裝)

Storagegithub

使用

dependencies {
    compile 'io.github.changjiashuai:storage:1.1.0'
}
複製代碼

存儲選項

Android系統中你有仔細考慮過數據的存儲使用嗎?例如數據應該是應用的私有數據,仍是可供其餘應用(和用戶)訪問,以及您的數據須要多少空間等。在Android提供了多種選項來保存永久性的應用數據。 本文介紹其中的兩種方式(其他的另開博文介紹)。一、內部存儲 二、外部存儲 最後會針對這兩種方式經常使用API進行封裝,並提供library數據庫

擴展閱讀

Android存儲(2)--適配器存儲 Android存儲(3)--外部存儲暴露給應用程序以前Android系統掛載過程 Android存儲(4)--各類設備類型的外部存儲配置示例:Emulated primary,Physical primary, physical secondary 今後不再會爲獲取外置存儲路徑中出現emulated,0而疑惑啦數組


如下正式開始本文內容介紹緩存

內部存儲

在設備內存中存儲私有數據。 內部存儲在Android文件系統的位置是當咱們在打開DDMS下的File Explorer面板的時候,/data目錄就是所謂的內部存儲(ROM)。可是注意,當手機沒有root的時候不能打開此文件夾。 其中的兩個目錄重點說下:安全

  1. /data/app : app文件夾裏存放着咱們全部安裝的app的apk文件
  2. /data/data: 第二個文件夾是data,也就是咱們常說的/data/data目錄(存儲包私有數據)

應用內部私有數據:/data/data/包名/XXX 此目錄下將每個APP的存儲內容按照包名分類存放好。 好比:bash

  1. data/data/包名/shared_prefs 存放該APP內的SP信息
  2. data/data/包名/databases 存放該APP的數據庫信息
  3. data/data/包名/files 將APP的文件信息存放在files文件夾
  4. data/data/包名/cache 存放的是APP的緩存信息

您能夠直接在設備的內部存儲中保存文件。默認狀況下,保存到內部存儲的文件是應用的私有文件,其餘應用(和用戶)不能訪問這些文件。當用戶卸載您的應用時,這些文件也會被移除。Android對其提供了一些方法能夠操做這些數據

要建立私有文件並寫入到內部存儲:

  1. 使用文件名稱和操做模式調用openFileOutput()。這將返回一個FileOutputStream
  2. 使用write()寫入到文件。
  3. 使用close()關閉流式傳輸。

要從內部存儲讀取文件:

  1. 調用openFileInput()並向其傳遞要讀取的文件名稱。這將返回一個FileInputStream
  2. 使用read()讀取文件字節。
  3. 而後使用close()關閉流式傳輸。

保存緩存文件

若是您想要緩存一些數據,而不是永久存儲這些數據,應該使用getCacheDir() 來打開一個File,它表示您的應用應該將臨時緩存文件保存到的內部目錄。

當設備的內部存儲空間不足時,Android 可能會刪除這些緩存文件以回收空間。 但您不該該依賴系統來爲您清理這些文件, 而應該始終自行維護緩存文件,使其佔用的空間保持在合理的限制範圍內(例如 1 MB)。 當用戶卸載您的應用時,這些文件也會被移除。

其餘實用方法

getFilesDir() 獲取在其中存儲內部文件的文件系統目錄的絕對路徑。 getDir() 在您的內部存儲空間內建立(或打開現有的)目錄。 deleteFile() 刪除保存在內部存儲的文件。 fileList() 返回您的應用當前保存的一系列文件。

針對以上Android提供的API簡單封裝內部存儲類以下:

public class Storage {
    ......

    public static class InternalStorage {

        /** * 獲取在其中存儲內部文件的文件系統目錄的絕對路徑。 * * @return /data/data/包名/files */
        public static File getFilesDir(Context context) {
            return context.getFilesDir();
        }

        /** * @return 返回您的應用當前保存的一系列文件 */
        public static String[] getFileList(Context context) {
            return context.fileList();
        }

        /** * @param name 文件名 * @return 刪除保存在內部存儲的文件。 */
        public static boolean deleteFile(Context context, String name) {
            return context.deleteFile(name);
        }

        /** * 向指定的文件中寫入指定的數據 * * @param name 文件名 * @param content 文件內容 * @param mode MODE_PRIVATE | MODE_APPEND 自 API 級別 17 以來,常量 MODE_WORLD_READABLE 和 * MODE_WORLD_WRITEABLE 已被棄用 */
        public static void writeFileData(Context context, String name, String content, int mode) {
            FileOutputStream fos = null;
            ...
                fos = context.openFileOutput(name, mode);
                fos.write(content.getBytes());
            ...
        }

        /** * 打開指定文件,讀取其數據,返回字符串對象 */
        public static String readFileData(Context context, String fileName) {
            String result = "";
            FileInputStream fis = null;
            ...
                fis = context.openFileInput(fileName);
            ...
        }
        
        /** * @param name 文件名 * @param mode MODE_PRIVATE | MODE_APPEND 自 API 級別 17 以來,常量 MODE_WORLD_READABLE 和 * MODE_WORLD_WRITEABLE 已被棄用 * @return 在您的內部存儲空間內建立(或打開現有的)目錄。 * @see #writeFileData(Context, String, String, int) */
        public static File getDir(Context context, String name, int mode) {
            return context.getDir(name, mode);
        }

        /** * 將臨時緩存文件保存到的內部目錄。 使其佔用的空間保持在合理的限制範圍內(例如 1 MB) * * @return /data/data/包名/cache */
        public static File getCacheDir(Context context) {
            return context.getCacheDir();
        }

        /** * dir: data|user/0 * * @return /data/{dir}/包名 */
        public static File getDataDir(Context context) {
            return ContextCompat.getDataDir(context);
        }

    }
    ......
}
複製代碼

外部存儲

在共享的外部存儲中存儲公共數據。

每一個兼容 Android 的設備都支持可用於保存文件的共享「外部存儲」。 該存儲多是可移除的存儲介質(例如 SD 卡)或內部(不可移除)存儲。 保存到外部存儲的文件是全局可讀取文件,並且,在計算機上啓用 USB 大容量存儲以傳輸文件後,可由用戶修改這些文件。

注:若是用戶在計算機上裝載了外部存儲或移除了介質,則外部存儲可能變爲不可用狀態,而且在您保存到外部存儲的文件上沒有實施任何安全性。全部應用都能讀取和寫入放置在外部存儲上的文件,而且用戶能夠移除這些文件。

獲取外部存儲的訪問權限

要讀取或寫入外部存儲上的文件,您的應用必須獲取READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE系統權限。 例如:

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>
複製代碼

若是您同時須要讀取和寫入文件,則只需請求 WRITE_EXTERNAL_STORAGE 權限,由於此權限也隱含了讀取權限要求。

注:從 Android 4.4開始,若是您僅僅讀取或寫入應用的私有文件,則不須要這些權限。 如需瞭解更多信息,請參閱下面有關保存應用私有文件的部分。

檢查介質可用性

在使用外部存儲執行任何工做以前,應始終調用getExternalStorageState()以檢查介質是否可用。介質可能已裝載到計算機,處於缺失、只讀或其餘某種狀態。

String state = Environment.getExternalStorageState();
state = MEDIA_UNKNOWN| MEDIA_REMOVED | MEDIA_UNMOUNTED |MEDIA_CHECKING | MEDIA_NOFS | MEDIA_MOUNTED | MEDIA_MOUNTED_READ_ONLY | MEDIA_SHARED | MEDIA_BAD_REMOVAL | MEDIA_UNMOUNTABLE
複製代碼

保存可與其餘應用共享的文件

通常而言,應該將用戶可經過您的應用獲取的新文件保存到設備上的「公共」位置,以便其餘應用可以在其中訪問這些文件,而且用戶也能輕鬆地從該設備複製這些文件。 執行此操做時,應使用共享的公共目錄之一,例如 MusicPicturesRingtones等。

要獲取表示相應的公共目錄的File,請調用getExternalStoragePublicDirectory(),向其傳遞您須要的目錄類型,例如DIRECTORY_MUSICDIRECTORY_PICTURESDIRECTORY_RINGTONES或其餘類型。經過將您的文件保存到相應的媒體類型目錄,系統的媒體掃描程序能夠在系統中正確地歸類您的文件(例如鈴聲在系統設置中顯示爲鈴聲而不是音樂)。

例如,如下方法在公共目錄中建立了一個指定名稱的目錄:

public File getStoragePublicDirWithName(String type, String name) {
        // Get the directory for the user's public directory.
        File file = new File(getStoragePublicDirectory(type), name);
        if (!file.mkdirs()) {
        Log.e(TAG, "Directory not created");
    }
    return file;
}
複製代碼

保存應用私有文件

若是您正在處理的文件不適合其餘應用使用(例如僅供您的應用使用的圖形紋理或音效),則應該經過調用getExternalFilesDir()來使用外部存儲上的私有存儲目錄。此方法還會採用type參數指定子目錄的類型(例如DIRECTORY_MOVIES)。 若是您不須要特定的媒體目錄,請傳遞null以接收應用私有目錄的根目錄。

Android 4.4開始,讀取或寫入應用私有目錄中的文件再也不須要READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE權限。所以,您能夠經過添加maxSdkVersion 屬性來聲明,只能在較低版本的Android中請求該權限:

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="18" />
    ...
</manifest>
複製代碼

注:當用戶卸載您的應用時,此目錄及其內容將被刪除。此外,系統媒體掃描程序不會讀取這些目錄中的文件,所以不能從MediaStore內容提供程序訪問這些文件。 一樣,不該將這些目錄用於最終屬於用戶的媒體,例如使用您的應用拍攝或編輯的照片或用戶使用您的應用購買的音樂等 — 這些文件應保存在公共目錄中。

有時,已分配某個內部存儲器分區用做外部存儲的設備可能還提供了SD卡槽。在使用運行Android 4.3和更低版本的這類設備時,getExternalFilesDir()方法將僅提供內部分區的訪問權限,而您的應用沒法讀取或寫入SD卡。不過,從Android 4.4開始,可經過調用getExternalFilesDirs()來同時訪問兩個位置,該方法將會返回包含各個位置條目的File數組。數組中的第一個條目被視爲外部主存儲;除非該位置已滿或不可用,不然應該使用該位置。 若是您但願在支持Android 4.3 和更低版本的同時訪問兩個可能的位置,請使用支持庫中的靜態方法 ContextCompat.getExternalFilesDirs()。 在Android 4.3 和更低版本中,此方法也會返回一個File數組,但其中始終僅包含一個條目

注意: 儘管MediaStore內容提供程序不能訪問getExternalFilesDir()getExternalFilesDirs()所提供的目錄,但其餘具備READ_EXTERNAL_STORAGE權限的應用仍可訪問外部存儲上的全部文件,包括上述文件。若是您須要徹底限制您的文件的訪問權限,則應該轉而將您的文件寫入到內部存儲。

保存緩存文件

要打開表示應該將緩存文件保存到的外部存儲目錄的File,請調用getExternalCacheDir()。 若是用戶卸載您的應用,這些文件也會被自動刪除。

與前述ContextCompat.getExternalFilesDirs()類似,您也能夠經過調用ContextCompat.getExternalCacheDirs()來訪問輔助外部存儲(若是可用)上的緩存目錄。

提示:爲節省文件空間並保持應用性能,您應該在應用的整個生命週期內仔細管理您的緩存文件並移除其中再也不須要的文件,這一點很是重要。

針對以上Android提供的API簡單封裝內部存儲類以下:

package io.github.changjiashuai.library;

import android.content.Context;
import android.os.Environment;
import android.support.v4.content.ContextCompat;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

import static android.content.ContentValues.TAG;

/** * <a href="https://developer.android.com/guide/topics/data/data-storage.html">data-storage</a> * * Email: changjiashuai@gmail.com * * Created by CJS on 2017/2/28 11:22. */

public class Storage {

    ......
    
    public static class ExternalStorage {
    
        /** * 判斷外部設置是否有效 */
        public static boolean isEmulated() {
            return Environment.isExternalStorageEmulated();
        }

        /** * 判斷外部設置是否能夠移除 */
        public static boolean isRemovable() {
            return Environment.isExternalStorageEmulated();
        }

        public static String getStorageState() {
            return getExternalStorageState();
        }

        /* Checks if external storage is available for read and write */
        public static boolean isStorageWritable() {
            ...
        }

        /* Checks if external storage is available to at least read */
        public static boolean isStorageReadable() {
            ...
        }

        /** * @return /storage/emulated/0/Android/data/包名/cache */
        public static File getCacheDir(Context context) {
            return context.getExternalCacheDir();
        }

        public static File createDirInCacheDir(Context context, String name) {
            ...
        }

        public static String getCacheDirPath(Context context) {
            ...
        }

        public static File[] getCacheDirs(Context context) {
            return ContextCompat.getExternalCacheDirs(context);
        }

        /** * 保存應用私有文件 * @return /storage/emulated/0/Android/data/包名/files/{type} */
        public static File getFilesDir(Context context, String type) {
            return context.getExternalFilesDir(type);
        }

        public static File createDirInFilesDir(Context context, String type, String name) {
            ...
        }

        public static String getFilesDirPath(Context context, String type) {
            ...
        }

        public static File[] getFilesDirs(Context context, String type) {
            return ContextCompat.getExternalFilesDirs(context, type);
        }

        /** * @return 保存可與其餘應用共享的文件 */
        @Deprecated
        public static File getStoragePublicDirectory(String type) {
            return Environment.getExternalStoragePublicDirectory(type);
        }

        public static File getStoragePublicDir(String type) {
            return Environment.getExternalStoragePublicDirectory(type);
        }

        /** * Writing to this path requires the * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permission * * @param type 目錄類型 * @param name 目錄名稱 * @return 在公共目錄中建立了一個指定名稱的目錄: */
        public static File createDirInStoragePublicDir(String type, String name) {
            ...
        }

        public static String getStoragePublicDirPath(String type) {
            ...
        }

        /** * @return /storage/emulated/0/Android/data/包名 */
        public static File getDataPkgDir(Context context) {
            ...
        }

        /** * @param name 文件名 * @return /storage/emulated/0/Android/data/io.github.changjiashuai.storage/{name} */
        public static File createDirInDataPkgDir(Context context, String name) {
            ...
        }

        public static String getDataPkgDirPath(Context context) {
            ...
        }
    }

    ......
    
}
複製代碼

注意事項

  1. 自API級別17以來,常量MODE_WORLD_READABLEMODE_WORLD_WRITEABLE已被棄用。從Android N開始,使用這些常量將會致使引起SecurityException。這意味着,面向Android N和更高版本的應用沒法按名稱共享私有文件,嘗試共享file://URI將會致使引起FileUriExposedException。若是您的應用須要與其餘應用共享私有文件,則能夠將FileProviderFLAG_GRANT_READ_URI_PERMISSION配合使用。另請參閱共享文件(後續會講解)。
  2. 提示:若是在編譯時想要保存應用中的靜態文件,請在項目的res/raw/目錄中保存該文件。可使用openRawResource()打開該資源並傳遞R.raw.<filename>資源 ID。此方法將返回一個InputStream,您可使用該流式傳輸讀取文件(但不能寫入到原始文件)。

相關文章
相關標籤/搜索