Android | 文件存儲

前言

  • Android中經(jīng)常需要使用文件存儲用戶數(shù)據(jù)
  • 本文將梳理各個版本中的文件存儲,希望能幫上忙。
文件存儲 思維導(dǎo)圖

1. 簡介

Android開發(fā)中有五種數(shù)據(jù)持久化API:

持久化 示意圖

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

2.1 劃分

內(nèi)部存儲 示意圖
  • 目錄:/data/data/
  • 特點(diǎn):
    • 每個應(yīng)用獨(dú)占一個以包名命名的私有文件夾
    • 在應(yīng)用卸載時被刪除
    • 對MediaScanner不可見
  • 適用場景:私密數(shù)據(jù)

2.2 API

內(nèi)部存儲 API
  • 相關(guān)的API
data/data/<包名>/ 描述
Context#getDir(String name,int mode):File! 內(nèi)部存儲根目錄下的文件夾(不存在則新建)
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! 存放優(yōu)化過的代碼(如JIT優(yōu)化)
data/data/<包名>/no_backup/ 描述
Context#getNoBackUpFIlesDir():File! 在Backup過程中被忽略的文件
  • 訪問模式參數(shù)

    • MODE_PRIVATE:只對在應(yīng)用內(nèi)可見

    • MODE_APPEND:如果文件存在,則在文件末尾追加;文件不存在,則與 MODE_PRIVATE 相同。

    • MODE_WORLD_READABLEMODE_WORLD_WRITEABLE:允許其他應(yīng)用訪問(不要使用)

    • 版本變更:棄用常量 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE(API 17)

    • 版本變更:禁用常量 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE(API 24)

    // 舉例(targetSdkVersion >= 24):
    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
    

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

3.1 定義

早期的Android設(shè)備存儲空間較小,有一個內(nèi)置(build-in)的存儲空間,即內(nèi)部存儲,另外還有一個可以移除的存儲介質(zhì),即外部存儲(如SD卡)。但是隨著設(shè)備內(nèi)置存儲空間增大,很多設(shè)備已經(jīng)足以將內(nèi)置存儲空間一分為二,一塊為內(nèi)部存儲,一塊為外部存儲。

  • 所有應(yīng)用均可讀寫,原則上不應(yīng)保存敏感信息
  • 檢查是否掛載

外部存儲并不總是可用的,因?yàn)橥獠看鎯梢砸瞥ㄔ缙谠O(shè)備)或者作為USB存儲設(shè)備連接到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;
    }
}
  • 監(jiān)聽外部存儲狀態(tài)
BroadcastReceiver mExternalStorageReceiver;
/* 開始監(jiān)聽 */
void startWatchingExternalStorage() {
    mExternalStorageReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
                // 更新狀態(tài)
            updateExternalStorageState();
        }
    };
    IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
    filter.addAction(Intent.ACTION_MEDIA_REMOVED);
    // 動態(tài)注冊廣播接收器
    registerReceiver(mExternalStorageReceiver, filter);
    updateExternalStorageState();
}
/* 停止監(jiān)聽 */
void stopWatchingExternalStorage() {
        // 注銷廣播接收器
    unregisterReceiver(mExternalStorageReceiver);
}
  • 權(quán)限

    • 讀權(quán)限:android.permission.READ_EXTERNAL_STORAGE
    • 讀+寫權(quán)限:android.permission.WRITE_EXTERNAL_STORAGE
    • 版本變更:訪問外部存儲的私有目錄不需要申請權(quán)限(API 19)
    <manifest...>
            <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                             android:maxSdkVersion="18" />
            ...
    </manifest>
    
    • 版本變更:動態(tài)權(quán)限(API 23)

3.2 劃分

外部存儲 示意圖
  • 私有目錄(private):storage/emulated/0/Android/

    • 特點(diǎn)

      • 每個應(yīng)用獨(dú)占以包名命名的私有文件夾
      • 在應(yīng)用卸載時被刪除
      • 對MediaScanner不可見(例外:多媒體文件夾 API 21)
    • 適用場景:非私密數(shù)據(jù),需要隨應(yīng)用卸載刪除

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

    • 特點(diǎn)

      • 所有應(yīng)用共享
      • 在應(yīng)用卸載時不會被刪除
      • 對MediaScanner可見
    • 適用場景:非私密數(shù)據(jù),不需要隨應(yīng)用卸載刪除

3.3 外部存儲API

外部存儲 API

因?yàn)橥獠看鎯Σ灰欢捎茫苑祷刂悼蔀榭栈蚩諗?shù)組

  • 公共目錄:
storage/emulated/0/ 描述
Environment.getExternalStorageDirectory():File? 外部存儲根目錄
Environment.getExternalStoragePublicDirectory(name:String?):File? 外部存儲根目錄下的文件夾
Environment.getExternalStorageState():String! 外部存儲狀態(tài)
  • 私有目錄:
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)

    有些設(shè)備可以外接存儲設(shè)備(如SD卡)來獲得更大的外部存儲空間,相當(dāng)于有多部分外部存儲空間,一塊內(nèi)置,一塊外置。在存儲空間足夠時,應(yīng)該優(yōu)先存儲在內(nèi)置的部分。

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

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


4. 補(bǔ)充

4.1 緩存文件

  • 內(nèi)部存儲和外部存儲中都有一個緩存文件夾:
    • data/data/<包名>/cache/
    • storage/emulated/0/Android/data/<包名>/cache/
  • 當(dāng)設(shè)備存儲空間不足時,緩存文件可以被回收,系統(tǒng)回收策略為:
    • before Android O(before API 26)
      策略:按照文件修改時間(modified time)排序,越早時間將優(yōu)先被刪除

      漏洞:應(yīng)用可以設(shè)置文件修改時間到一個稍晚的時間(比如2050年),保持不被刪除

    • since Android O(since API 26)
      策略:系統(tǒng)分別為每個應(yīng)用設(shè)置緩存空間閾值,設(shè)備存儲空間不足時,超過閾值的應(yīng)用將優(yōu)先刪除緩存,低于閾值的應(yīng)用緩存會被保留。系統(tǒng)會動態(tài)修改閾值,用戶使用頻率越高的應(yīng)用閾值越高。
      • 閾值
        StorageManager sm = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
        UUID uuid  = sm.getUuidForPath(getCacheDir());
        long byteSize = sm.getCacheQuotaBytes(uuid);
        
      • 查詢已分配的緩存空間
        sm.getCacheSizeBytes(uuid)
        
      • 行為——緩存粒度
        // 整個文件夾視為一個緩存整體,在系統(tǒng)回收空間時清空文件夾
        sm.setCacheBehaviorGroup(dirFile,true)
        
      • 行為——保留文件結(jié)構(gòu)
        // 在系統(tǒng)回收文件時,清空文件數(shù)據(jù)(length=0),而不是直接刪除文件
        sm.setCacheBehaviorTombstone(dirFile,true)
        
  • 清除應(yīng)用的數(shù)據(jù)的選項(xiàng)(在系統(tǒng)設(shè)置或手機(jī)管家中):
    • 清除緩存:清除應(yīng)用的內(nèi)部存儲緩存文件夾外部存儲緩存文件夾;
    • 清除數(shù)據(jù):清除應(yīng)用的內(nèi)部存儲外部存儲空間私有目錄;

4.2 android:installLocation

  • 可選值

    • internalOnly(默認(rèn)):安裝在內(nèi)部存儲,內(nèi)部存儲空間不足時無法安裝;
    • auto:優(yōu)先安裝在內(nèi)部存儲,內(nèi)部存儲空間不足時,嘗試安裝在外部存儲;
    • preferExternal:優(yōu)先安裝在外部存儲,外部存儲空間不足時,嘗試安裝在內(nèi)部存儲;
  • 外部存儲被移除時,安裝在外部存儲空間上的應(yīng)用會被系統(tǒng)殺死。直到外部存儲重新掛載時,系統(tǒng)發(fā)出ACTION_EXTERNAL_APPLICATIONS_AVAILABLE廣播。

  • 對于占用存儲空間較大的應(yīng)用來說,就有必要考慮安裝在外部存儲。舉例:反編譯王者榮耀查看AndroidManifest文件,可以看到使用了“auto”選項(xiàng)。

    <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 sm = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val uuid = sm.getUuidForPath(target)
    sm.getAllocatableBytes(uuid) // 當(dāng)前應(yīng)用的可用容量(包括可用容量和全部應(yīng)用的緩存空間)
    
  • StorageStatsManager(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)
    

4.3.2 分配

  • before API 26

    val target = File(context.filesDir,"my-download")
    if(downloadSize <= target.getAvailableSpace()){
          // 磁盤空間充足,可以寫入
          ...
    }
    

    注意:即使判斷磁盤空間充足,也可能在寫入過程中拋出IOException(空間不足),因?yàn)闊o法避免多線程或多進(jìn)程并發(fā)寫入。

  • 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)){
                  // 預(yù)分配downloadSize大小的空間給當(dāng)前應(yīng)用
                  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()可以避免了并發(fā)寫入造成空間不足異常


5. 總結(jié)

  • 隱私性
    位置 其他應(yīng)用 未root用戶 root用戶 MediaScanner
    內(nèi)部存儲 X X X
    私有內(nèi)部存儲 僅多媒體文件夾
    公共內(nèi)部存儲
  • 生存期


    生存期 示意圖
  • 版本演進(jìn)


    版本演進(jìn) 示意圖

延伸閱讀

  • [Android | DiskLruCache磁盤緩存]
  • [Android | 文件共享]
  • [Android | 文件安全]
  • [Android | 文件多線程安全]
  • [Android | 多媒體文件管理]
  • [Android | 磁盤優(yōu)化]

推薦閱讀


參考文獻(xiàn)


感謝喜歡!你的點(diǎn)贊是對我最大的鼓勵!歡迎關(guān)注彭旭銳的簡書!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 保存文件 Android 使用與其他平臺上基于磁盤的文件系統(tǒng)類似的文件系統(tǒng)。 本課程講述如何使用 Android ...
    李建彪閱讀 1,220評論 0 2
  • Android文件存儲 一, 當(dāng)應(yīng)用用到拍攝功能時,拍攝照片比較大,保存在數(shù)據(jù)庫不現(xiàn)實(shí),必須保存在私有存儲空間...
    eagerabu閱讀 767評論 0 4
  • 引言:文件存儲[內(nèi)部存儲]和[外部存儲]。SD 卡上的文件路徑。時間:2017年06月17日作者:JustDo23...
    JustDo23閱讀 969評論 0 4
  • 一直一來沒有認(rèn)真關(guān)注過android 的文件存儲,現(xiàn)在做一個總結(jié),我認(rèn)為有用的,網(wǎng)上的博客真是寫的叫一個渣渣,根本...
    子丿龍閱讀 419評論 0 0
  • 存儲路徑及演化 首先看這張文件從Android文件存儲使用參考轉(zhuǎn)載的存儲結(jié)構(gòu)圖,里面明確了通過各種Android接...
    黃怡菲閱讀 2,148評論 4 11

友情鏈接更多精彩內(nèi)容