數據存儲之——Android內、外存儲分區&常用存儲目錄詳解(Android Q)

本文將以實用的角度來講解Android中文件操作的常用方式。

存儲的”內“和“外”


所有Android設備都有兩個文件存儲區域:內部存儲空間(internal Storage)和外部存儲空間(external Storage)。這些名稱是在Android早期確定的,那時候大部分設備都提供內置的非易失性內存(內部存儲空間)以及可移動存儲媒介(如,Micro SD卡,提供外部存儲空間)。現在,很多設備將永久性存儲空間劃分爲單獨的“內部”和“外部”分區。因此,即使沒有可移動存儲媒介,這兩種存儲空間也始終存在,並且無論外部存儲空間是否可移動,這兩種存儲空間的API行爲在Android系統上都是相同的。

所以,Android系統從邏輯上,只分爲"internal Storage" 與 “external Storage” 兩個存儲分區。

內部存儲分區(internal Storage)

內部存儲分區,物理位置主要包括了Android系統根目錄下的/data、/System、/cache等目錄。

內部存儲分區的特點:
  • 內部分區總是可用。
  • 它存放App私有文件,並且不可被其他App訪問。
  • App卸載後,存儲在內部分區上的該App數據將會被清除。
  • 不需要額外申請權限。

外部存儲分區(external Storage)

它有以下幾個特點:

  • 外部分區並不總是可用。
  • 保存在這裏的文件可能被其他程序訪問。
  • 當用戶卸載app時,系統僅僅會刪除external中的緩存目錄(Context.getExternalCacheDir())和file目錄(Context.getExternalFilesDir())下的相關文件。
  • 需要申請WRITE_EXTERNAL_STORAGE或READ_EXTERNAL_STORAGE權限。

我們在開發過程中,經常需要讀取或者存儲一些數據,這些數據可以存儲在內部分區中,也可以存儲在外部分區中,但不同的操作方式會有很大區別,我們下面來詳細進行分析。

內部存儲分區的訪問


本節重點來分析內部存儲分區的數據訪問。內部存儲包含了/system、/data、/cache等目錄及其子目錄。

/system

系統存放目錄,它和/sdcard以及/data是同級的,是存儲根目錄的一級子目錄。

訪問方式

可以通過Environment類的getRootDirectory方法訪問:

    private static final String ENV_ANDROID_ROOT = "ANDROID_ROOT"; //環境變量
    private static final File DIR_ANDROID_ROOT = getDirectory(ENV_ANDROID_ROOT, "/system");//如果環境變量指定了,則使用指定值,否則使用"/system"
    public static @NonNull File getRootDirectory() {
        return DIR_ANDROID_ROOT;
    }

這裏通常返回目錄是"/system"。

子目錄

/system/app:存放rom本身附帶的軟件即系統軟件。
/system/data:存放/system/app中,核心系統軟件的數據文件信息。
/system/priv-app:存放手機廠商定製的系統級別的應用的apk文件。
/system/bin:存放系統的本地程序,裏面主要是Linux系統自帶的組件。
/system/media:存放一些音效、鈴聲、開關機動畫等。

/data目錄

/data目錄時我們App私有數據存儲的頂級目錄,可以通過Environment.getDataDirectory()獲取。

Environment.getDataDirectory()源碼:

    private static final File DIR_ANDROID_DATA = getDirectory(ENV_ANDROID_DATA, "/data");
    /**
     * Return the user data directory.
     */
    public static File getDataDirectory() {
        return DIR_ANDROID_DATA;
    }

我們通常不會直接使用該目錄進行數據存儲操作。

應用程序私有根目錄

應用程序私有目錄,它的根目錄位於/data/data/<app包名>/文件夾下。

可通過Context對象的getDataDir()方法來獲取,在開發時,通常我們不應該直接使用該目錄,而應該使用file、cache等系統已經定義好的目錄。

getDataDir()方法

getDataDir()方法的實現實在ContextImpl中:

    @Override
    public File getDataDir() {
        if (mPackageInfo != null) {
            File res = null;
            if (isCredentialProtectedStorage()) {
                res = mPackageInfo.getCredentialProtectedDataDirFile();
            } else if (isDeviceProtectedStorage()) {
                res = mPackageInfo.getDeviceProtectedDataDirFile();
            } else {
                res = mPackageInfo.getDataDirFile(); //mPackageInfo是LoadedApk的對象。
            }
            ……
        } else {
            throw new RuntimeException(
                    "No package details found for package " + getPackageName());
        }
    }

這裏其實有個判斷,但通常情況下,邏輯會走到res = mPackageInfo.getDataDirFile()這裏,mPackageInfo是LoadedApk的對象,最終數據來源是ApplicationInfo對象傳遞進來的。

應用程序files目錄

Context對象的getFilesDir()方法可以獲得應用私有目錄的file目錄,位置是通常是:/data/data/<app包名>/files文件夾。

我們對文件操作常用的方法,Context對象的openFileOutput()方法的文件根目錄地址就是files目錄。

ContextImpl中的源碼:

    @Override
    public File getFilesDir() {
        synchronized (mSync) {
            if (mFilesDir == null) {
                mFilesDir = new File(getDataDir(), "files");
            }
            return ensurePrivateDirExists(mFilesDir);
        }
    }

該目錄是我們需要經常使用的目錄。

應用程序cache目錄

cache目錄是我們App內部存儲的緩存目錄。它可以通過Context對象的getCacheDir()方法來獲得,位置是通常是:/data/data/<app包名>/cache文件夾。如果您想暫時保留而非永久存儲某些數據,則應使用特殊的緩存目錄來保存這些數據。不應依賴系統爲您清理這些文件,而應始終自行維護緩存文件,使其佔用的空間保持在合理的限制範圍內(例如 1MB)。當用戶卸載您的應用時,這些文件也會隨之移除。

getCacheDir()方法

Context對象的getCacheDir()方法可以獲取cache目錄。

ContextImpl中的源碼:

    @Override
    public File getCacheDir() {
        synchronized (mSync) {
            if (mCacheDir == null) {
                mCacheDir = new File(getDataDir(), "cache");
            }
            return ensurePrivateCacheDirExists(mCacheDir, XATTR_INODE_CACHE);
        }
    }

cache文件有以下幾個特點需要注意:

  1. 系統將在磁盤空間不足時自動刪除此目錄中的文件。
  2. 系統將始終首先刪除舊文件。
  3. 我們可以使用StorageManager類的相關方法更好的管理我們的刪除規則。
  4. App所佔緩存空間的大小可以通過StorageManager.getCacheQuotaBytes(java.util.UUID)來獲得。
  5. 超過App所分配限額的緩存空間將被優先刪除,我們應該儘可能的使我們的cache空間內的文件低於限額值,這會使得我們的cache文件最大可能的減少被刪除的概率。

databases目錄

databases目錄存放了應用程序的數據庫文件,位置是通常是:/data/data/<app包名>/databases文件夾。

getDatabasesDir()方法

Context對象的getDatabasesDir()方法可以獲取databases目錄。

ContextImpl中的源碼:

    private File getDatabasesDir() {
        synchronized (mSync) {
            if (mDatabasesDir == null) {
                if ("android".equals(getPackageName())) {
                    mDatabasesDir = new File("/data/system");
                } else {
                    mDatabasesDir = new File(getDataDir(), "databases");
                }
            }
            return ensurePrivateDirExists(mDatabasesDir);
        }
    }

該方法是一個私有方法,不能直接訪問,我們通常使用DB的相關封裝方法來進行訪問。這裏我們可以看到,如果應用程序的包名是“android”,則DB的目錄是"/data/system",否則,DB的目錄是/data/data/<app包名>/databases。

shared_prefs目錄

如果應用想存儲一些數據量較小的鍵值對信息,可以使用SharedPreferences來保存數據,例如,一些應用相關的配置信息等。

它可以通過Context對象的getSharedPreferences方法來進行訪問操作,位置是通常是:/data/data/<app包名>/shared_prefs文件夾。

getPreferencesDir()方法

sp目錄可以通過getPreferencesDir()方法來進行獲取,我們不能直接使用該方法。

ContextImpl中的源碼:

    private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }

/cache目錄

下載緩存內容目錄,它和/system以及/data是同級的,目錄是/cache。

該目錄可以通過Environment的getDownloadCacheDirectory方法返回:

    private static final File DIR_DOWNLOAD_CACHE = getDirectory(ENV_DOWNLOAD_CACHE, "/cache");
    public static File getDownloadCacheDirectory() {
        return DIR_DOWNLOAD_CACHE;
    }

外部存儲分區的訪問


外部存儲可能是不可用的,比如遇到SD卡被拔出等情況時,因此在訪問之前應對其可用性進行檢查。我們可以通過執行getExternalStorageState()來查詢外部存儲設備的狀態,若返回狀態爲MEDIA_MOUNTED, 則可以讀寫。

/sdcard

外部存儲的sd卡根目錄,也就是我們平時從文件管理器中能看到的最頂級目錄,它的File絕對路徑爲:/storage/emulated/0。

訪問方式

可以通過Environment類的getExternalStorageDirectory方法訪問:

    @Deprecated
    public static File getExternalStorageDirectory() {
        throwIfUserRequired();
        return sCurrentUser.getExternalDirs()[0];
    }

getExternalDirs方法返回的是所有外部存儲的文件列表,getExternalStorageDirectory返回的是列表中的第一個元素,也就是主外部存儲的目錄。

應用的外部私有文件

外部私有文件,存儲在外部分區,當應用被卸載後,與該應用相關的數據也清除掉。這裏的私有並非其他應用訪問不到,而是指該類數據是當前應用私有的,對其他應用並無用處,並且該類文件會在卸載時,被系統刪除。

這部分存儲的位置位於/Android/data/<app包名>/下。

getExternalFilesDir()方法

通過Context.getExternalFilesDir()方法可以獲取SDCard/Android/data/<app包名>/files/目錄,一般放一些長時間保存的數據。

ContextImpl中的源碼:
    @Override
    public File getExternalFilesDir(String type) {
        // Operates on primary external storage
        final File[] dirs = getExternalFilesDirs(type);
        return (dirs != null && dirs.length > 0) ? dirs[0] : null;
    }

    @Override
    public File[] getExternalFilesDirs(String type) {
        synchronized (mSync) {
            File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName());
            if (type != null) {
                dirs = Environment.buildPaths(dirs, type);
            }
            return ensureExternalDirsExistOrFilter(dirs);
        }
    }

getExternalFilesDir方法的參數,表示將要創建的/files目錄下的子目錄。通過源碼我們看到,它調用了Environment.buildExternalStorageAppFilesDirs(getPackageName())來獲取/files目錄。buildExternalStorageAppFilesDirs根據外部存儲的數量,返回的是一個File的數組,getExternalFilesDir只取第一個,也就是主外部存儲的目錄。

Environment.buildExternalStorageAppFilesDirs方法:
    public static final String DIR_ANDROID = "Android";
    private static final String DIR_DATA = "data";
    private static final String DIR_MEDIA = "media";
    private static final String DIR_OBB = "obb";
    private static final String DIR_FILES = "files";
    private static final String DIR_CACHE = "cache";
    
    @UnsupportedAppUsage
    public static File[] buildExternalStorageAppFilesDirs(String packageName) {
        throwIfUserRequired();
        return sCurrentUser.buildExternalStorageAppFilesDirs(packageName);
    }
內部類UserEnvironment的buildExternalStorageAppFilesDirs方法:
        public File[] buildExternalStorageAppFilesDirs(String packageName) {
            return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName, DIR_FILES);
        }

這裏按照目錄的父子關係,依次創建了相應的目錄:Android、data、packageName目錄、files。

Context.getExternalCacheDir()方法

通過Context.getExternalCacheDir()方法可以獲取到SDCard/Android/data/<app包名>/cache/目錄,一般存放臨時緩存數據時使用。

ContextImpl中的源碼:
    @Override
    public File getExternalCacheDir() {
        // Operates on primary external storage
        final File[] dirs = getExternalCacheDirs();
        return (dirs != null && dirs.length > 0) ? dirs[0] : null;
    }

    @Override
    public File[] getExternalCacheDirs() {
        synchronized (mSync) {
            File[] dirs = Environment.buildExternalStorageAppCacheDirs(getPackageName());
            return ensureExternalDirsExistOrFilter(dirs);
        }
    }

邏輯與files目錄類似,最終調用到Environment內部類UserEnvironment的buildExternalStorageAppCacheDirs方法。

內部類UserEnvironment的buildExternalStorageAppCacheDirs方法:
        public File[] buildExternalStorageAppCacheDirs(String packageName) {
            return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName, DIR_CACHE);
        }

這裏創建了cache目錄。

外部公共存儲目錄

使用Environment的getExternalStoragePublicDirectory方法可以訪問外部公共存儲目錄。

Environment的getExternalStoragePublicDirectory方法:
    public static File getExternalStoragePublicDirectory(String type) {
        throwIfUserRequired();
        return sCurrentUser.buildExternalStoragePublicDirs(type)[0];
    }
內部類UserEnvironment的buildExternalStorageAppCacheDirs方法
        public File[] buildExternalStoragePublicDirs(String type) {
            return buildPaths(getExternalDirs(), type);
        }

getExternalDirs方法返回的是所有外部存儲的文件列表,getExternalStoragePublicDirectory返回的是列表中的第一個元素,也就是主外部存儲中的目錄。

該方法會根據傳遞的參數名爲子目錄,在/sdcard下創建一個子目錄作爲公共訪問目錄。

Environment的getExternalStoragePublicDirectory方法的使用限制

  1. Environment的getExternalStoragePublicDirectory方法的參數應該是以下幾種特定類型:
     *            {@link #DIRECTORY_MUSIC}, 
     *            {@link #DIRECTORY_PODCASTS},
     *            {@link #DIRECTORY_RINGTONES}, 
     *            {@link #DIRECTORY_ALARMS},
     *            {@link #DIRECTORY_NOTIFICATIONS}, 
     *            {@link #DIRECTORY_PICTURES},
     *            {@link #DIRECTORY_MOVIES}, 
     *            {@link #DIRECTORY_DOWNLOADS},
     *            {@link #DIRECTORY_DCIM}, or 
     *            {@link #DIRECTORY_DOCUMENTS}
  1. 參數不能爲null。
  2. 在Android Q中,該接口已經廢棄。替代方案建議使用Context.getExternalFilesDir、MediaStore、Intent.ACTION_OPEN_DOCUMENT。

驗證外部存儲是否可用

由於外部存儲可能會不可用,例如,當用戶將存儲安裝到另一臺機器或移除了提供外部存儲的SD卡時。因此在訪問外部存儲之前,我們需要首先驗證外部存儲是否可用,然後再進行訪問操作。

我們可以通過調用getExternalStorageState()來查詢外部存儲的狀態。如果返回的狀態爲MEDIA_MOUNTED,則可以讀取和寫入文件。如果返回的是MEDIA_MOUNTED_READ_ONLY,則只能讀取文件。

示例代碼:

    /* Checks if external storage is available for read and write */
    public boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        return false;
    }

    /* Checks if external storage is available to at least read */
    public boolean isExternalStorageReadable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state) ||
            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            return true;
        }
        return false;
    }
    

我們在確認外部存儲可用之後,就可以安全的訪問外部存儲設備上的數據了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章