一篇文章完全明白Android文件存儲

前言

  • Android中常常須要使用文件存儲用戶數據
  • 本文將梳理各個版本中的文件存儲,但願能幫上忙。

文件存儲 思惟導圖

1. 簡介

Android開發中有五種數據持久化API:java

持久化 示意圖

2. 內部存儲空間(Internal Storage)

2.1 劃分

內部存儲 示意圖

  • 目錄:/data/data/
  • 特色:
  • 每一個應用獨佔一個以包名命名的私有文件夾
  • 在應用卸載時被刪除
  • 對MediaScanner不可見
  • 適用場景:私密數據
2.2 API

內部存儲 API

  • 相關的API
data/data/<包名>/ 描述
Context#getDir(String name,int mode):File! 內部存儲根目錄下的文件夾(不存在則新建)
data/data/<包名>/files/ 描述
Context#getFilesDir():File! files文件夾
Context#fileList():Array<String!>! 列舉文件和文件夾
Context#openFileInput(String name):FileInputStream! 打開文件輸入流(不存在則拋出FileNotFoundException)
Context#openFileOut(String name,int mode):FileOutputStream! 打開文件輸出流(文件不存在則新建)
Context#deleteFile(String name):Boolean! 刪除文件或文件夾
data/data/<包名>/cache/ 描述
Context#getCacheDir():File! cache文件夾
data/data/<包名>/code_cache/ 描述
Context#getCodeCacheDir():File! 存放優化過的代碼(如JIT優化)
data/data/<包名>/no_backup/ 描述
Context#getNoBackUpFIlesDir():File! 在Backup過程當中被忽略的文件
  • 訪問模式參數android

try(FileOutputStream fos = openFileOutput("file_name",MODE_WORLD_WRITEABLE)){面試

fos.write("Not sensitive information".getBytes());

}catch (IOException e){數組

e.printStackTrace();

}緩存

// 異常:
Caused by: java.lang.SecurityException: MODE_WORLD_READABLE no longer supported
Caused by: java.lang.SecurityException: MODE_WORLD_WRITEABLE no longer supported多線程

*   **MODE_PRIVATE**:只對在應用內可見

*   **MODE_APPEND**:若是文件存在,則在文件末尾追加;文件不存在,則與 MODE_PRIVATE 相同。

*   **MODE_WORLD_READABLE** 和 **MODE_WORLD_WRITEABLE**:容許其餘應用訪問(不要使用)

*   版本變動:**棄用**常量 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE(API 17)

*   版本變動:**禁用**常量 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE(API 24)

## 3\. 外部存儲(External Storage/Shared Storage)

##### 3.1 定義

早期的Android設備存儲空間較小,有一個內置(build-in)的存儲空間,即內部存儲,另外還有一個能夠移除的存儲介質,即外部存儲(如SD卡)。可是隨着設備內置存儲空間增大,不少設備已經足以將內置存儲空間一分爲二,一塊爲內部存儲,一塊爲外部存儲。

*   **全部應用都可讀寫**,原則上不該保存敏感信息

*   檢查是否掛載

外部存儲並不老是可用的,由於外部存儲能夠移除(早期設備)或者做爲USB存儲設備鏈接到PC,訪問前**必須檢查是否掛載**(mounted):

boolean mExternalStorageAvailable = false;
boolean mExternalStorageWriteable = false;
/ 檢查外部存儲是否可讀寫 /
void updateExternalStorageState() {架構

String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
        // 可讀寫
    mExternalStorageAvailable = mExternalStorageWriteable = true;
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        // 可讀
    mExternalStorageAvailable = true;
    mExternalStorageWriteable = false;
} else {
    mExternalStorageAvailable = mExternalStorageWriteable = false;
}

}併發

*   監聽外部存儲狀態

BroadcastReceiver mExternalStorageReceiver;
/ 開始監聽 /
void startWatchingExternalStorage() {ide

mExternalStorageReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
            // 更新狀態
        updateExternalStorageState();
    }
};
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
filter.addAction(Intent.ACTION_MEDIA_REMOVED);
// 動態註冊廣播接收器
registerReceiver(mExternalStorageReceiver, filter);
updateExternalStorageState();

}
/ 中止監聽 /
void stopWatchingExternalStorage() {學習

// 註銷廣播接收器
unregisterReceiver(mExternalStorageReceiver);

}

*   權限

<manifest...>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="18" />
    ...

</manifest>

*   版本變動:動態權限(API 23)

*   讀權限:**android.permission.READ_EXTERNAL_STORAGE**

*   讀+寫權限:**android.permission.WRITE_EXTERNAL_STORAGE**

*   版本變動:訪問外部存儲的私有目錄不須要申請權限(API 19)

##### 3.2 劃分

![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xNTY3OTEwOC04M2VkYjJkZDE2MzJlYmU2)

#### 外部存儲 示意圖

*   私有目錄(private):`storage/emulated/0/Android/`

*   每一個應用獨佔以包名命名的私有文件夾

*   **在應用卸載時被刪除**

*   **對MediaScanner不可見**(例外:多媒體文件夾 API 21)

*   特色

*   適用場景:**非私密數據,須要隨應用卸載刪除**

*   公共目錄(public):外部存儲中除了私有目錄外的其餘空間

*   全部應用共享

*   **在應用卸載時不會被刪除**

*   **對MediaScanner可見**

*   特色

*   適用場景:**非私密數據,不須要隨應用卸載刪除**

##### 3.3 外部存儲API

![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xNTY3OTEwOC1hNDM2Nzk5MzFjNjlkZDAx)

#### 外部存儲 API

> 由於外部存儲不必定可用,因此返回值可爲空或空數組

*   公共目錄:

| storage/emulated/0/ | 描述 |
| --- | --- |
| Environment.getExternalStorageDirectory():File? | 外部存儲根目錄 |
| Environment.getExternalStoragePublicDirectory(name:String?):File? | 外部存儲根目錄下的文件夾 |
| Environment.getExternalStorageState():String! | 外部存儲狀態 |

*   私有目錄:

| storage/emulated/0/Android/data/<包名>/ | 描述 |
| --- | --- |
| Context.getExternalCacheDir():File? | cache文件夾 |
| Context.getExternalCacheDirs():Array<File!>! | 多部分cache文件夾(API 18) |
| Context.getExternalFilesDir(type: String?):File? | files文件夾 |
| Context.getExternalFIlesDirs(type:String?):Array<File!>! | 多部分files文件夾(API 18) |
| Context.getExternalMediaDirs():Array<File!>! | 多部分多媒體文件夾(API 21) |

*   版本變動:多部分外部存儲——Context#getExternalFilesDirs()(API 18)

    有些設備能夠外接存儲設備(如SD卡)來得到更大的外部存儲空間,至關於有多部分外部存儲空間,一塊內置,一塊外置。在存儲空間足夠時,應該優先存儲在內置的部分。

    > 兼容:Context.getExternalFilesDirs():Arra<File!>!,在低版本中數組只會返回一個元素,指向內置的外置存儲的路徑

版本變動:外部存儲多媒體文件夾——`Context.getExternalMediaDirs()(API 21`):對`MediaScanner`可見

## 4\. 補充

##### 4.1 緩存文件

*   內部存儲和外部存儲中都有一個緩存文件夾:

*   data/data/<包名>/cache/

*   storage/emulated/0/Android/data/<包名>/cache/

*   當設備存儲空間不足時,緩存文件能夠被回收,系統回收策略爲:

*   閾值

StorageManager sm = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
UUID uuid = sm.getUuidForPath(getCacheDir());
long byteSize = sm.getCacheQuotaBytes(uuid);

*   查詢已分配的緩存空間
sm.getCacheSizeBytes(uuid)
*   行爲——緩存粒度
// 整個文件夾視爲一個緩存總體,在系統回收空間時清空文件夾sm.setCacheBehaviorGroup(dirFile,true)
*   行爲——保留文件結構
// 在系統回收文件時,清空文件數據(length=0),而不是直接刪除文件sm.setCacheBehaviorTombstone(dirFile,true)
*   before Android O(before API 26) 策略:按照文件修改時間(modified time)排序,越早時間將優先被刪除

    > 漏洞:應用能夠設置文件修改時間到一個稍晚的時間(好比2050年),保持不被刪除

*   since Android O(since API 26) 策略:系統分別爲每一個應用設置緩存空間閾值,設備存儲空間不足時,超過閾值的應用將優先刪除緩存,低於閾值的應用緩存會被保留。系統會動態修改閾值,用戶使用頻率越高的應用閾值越高。

*   清除應用的數據的選項(在系統設置或手機管家中):

*   清除緩存:清除應用的**內部存儲緩存文件夾** 與 **外部存儲緩存文件夾**;

*   清除數據:清除應用的**內部存儲** 與 **外部存儲空間私有目錄**;

### 4.2 android:installLocation

*   可選值

*   **internalOnly**(默認):安裝在內部存儲,內部存儲空間不足時沒法安裝;

*   **auto**:優先安裝在內部存儲,內部存儲空間不足時,嘗試安裝在外部存儲;

*   **preferExternal**:優先安裝在外部存儲,外部存儲空間不足時,嘗試安裝在內部存儲;

*   外部存儲被移除時,安裝在外部存儲空間上的應用會被系統殺死。直到外部存儲從新掛載時,系統發出**ACTION_EXTERNAL_APPLICATIONS_AVAILABLE**廣播。

*   對於佔用存儲空間較大的應用來講,就有必要考慮安裝在外部存儲。舉例:反編譯**王者榮耀**查看AndroidManifest文件,能夠看到使用了「auto」選項。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.tencent.tmgp.sgame"
    platformBuildVersionCode="28"
    platformBuildVersionName="9"
    android:compileSdkVersion="28"
    android:compileSdkVersionCodename="9"
    android:installLocation="auto"
    android:theme="@android:style/Theme.NoTitleBar">
## 4.3 存儲空間

##### 4.3.1 查詢

*   File

val target = File(context.filesDir,"my-download")
target.freeSpace // 未分配容量(Root用戶可用的容量)
target.usableSpace // 可用容量(非Root用戶可用的容量)
target.totalSpace // 所有容量(包括已分配容量和未分配容量)

*   StatFs(API 18)

val target = File(context.filesDir,"my-download")
val stat = StatFs(target)
val blockSize = stat.blockSizeLong
stat.freeBlocksLong * blockSize // 同上
stat.availableBlocksLong * blockSize // 同上
stat.blockCountLong * blockSIze // 同上

*   StorageManager(API 26)

val target = File(context.filesDir,"my-download")
val ssm = getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
val sm = getSystemService(Context.STORAGE_SERVICE) as StorageManager
val uuid = sm.getUuidForPath(target)
ssm.getFreeBytes(uuid) // 可用容量(非Root用戶可用的容量)
ssm.getTotalBytes(uuid) // 完整的物理容量(好比64G)

*   StorageStatsManager(API 26)
val target = File(context.filesDir,"my-download")val ssm = getSystemService(Context.STORAGE_STATS_SERVICE) as   StorageStatsManagerval sm = getSystemService(Context.STORAGE_SERVICE) as StorageManagerval uuid = sm.getUuidForPath(target)ssm.getFreeBytes(uuid)  // 可用容量(非Root用戶可用的容量)ssm.getTotalBytes(uuid) // 完整的物理容量(好比64G)
```

4.3.2 分配

  • before API 26

if(downloadSize <= target.getAvailableSpace()){

// 磁盤空間充足,能夠寫入
  ...

}

> 注意:即便判斷磁盤空間充足,也可能在寫入過程當中拋出IOException(空間不足),由於沒法避免多線程或多進程併發寫入。

*   since API 26
val target = File(context.filesDir,"my-download")

val sm = getSystemService(Context.STORAGE_SERVICE) as StorageManager
val uuid = sm.getUuidForPath(target)
if(downloadSize <= sm.getAllocatableBytes(uuid){

try(FileOutPutStream os = FileOutPutStream(target)){
          // 預分配downloadSize大小的空間給當前應用
          sm.allocateBytes(os.getFD(),downloadSize)
          // 寫入
          ...
  }

}else{

// 空間不足,請求用戶自行清理空間
  val intent = Intent(StorageManager.ACTION_MANAGE_STORAGE);
  intent.putExtra(StorageManager.EXTRA_UUID,uuid);
  // 須要的空間
  intent.putExtra(StorageManager.EXTRA_REQUESTED_BYTES,downloadSize);
  context.tartActivityForResult(intent,REQUEST_CODE);

}

> StorageManager#allocateBytes()能夠避免了併發寫入形成空間不足異常

## 5\. 總結

*   隱私性

    | 位置 | 其餘應用 | 未root用戶 | root用戶 | MediaScanner |
    | --- | --- | --- | --- | --- |
    | 內部存儲 | X | X | √ | X |
    | 私有內部存儲 | √ | √ | √ | 僅多媒體文件夾 |
    | 公共內部存儲 | √ | √ | √ | √ |

*   生存期![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xNTY3OTEwOC04OGJmMzg5YmM2NWMyMzZj)

*   版本演進![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xNTY3OTEwOC1iYzk1NWM5YWNhYTBhMTQz)
### Android開發資料+面試架構資料 免費分享 點擊連接 便可領取
[《Android架構師必備學習資源免費領取(架構視頻+面試專題文檔+學習筆記)》](https://shimo.im/docs/VqoXZ2j9E04V4nhk/ )
相關文章
相關標籤/搜索