Android | 文件存儲(chǔ)

前言

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

1. 簡(jiǎn)介

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

持久化 示意圖

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

2.1 劃分

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

2.2 API

內(nèi)部存儲(chǔ) API
  • 相關(guān)的API
data/data/<包名>/ 描述
Context#getDir(String name,int mode):File! 內(nèi)部存儲(chǔ)根目錄下的文件夾(不存在則新建)
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:只對(duì)在應(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. 外部存儲(chǔ)(External Storage/Shared Storage)

3.1 定義

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

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

外部存儲(chǔ)并不總是可用的,因?yàn)橥獠看鎯?chǔ)可以移除(早期設(shè)備)或者作為USB存儲(chǔ)設(shè)備連接到PC,訪問前必須檢查是否掛載(mounted):

boolean mExternalStorageAvailable = false;
boolean mExternalStorageWriteable = false;
/* 檢查外部存儲(chǔ)是否可讀寫 */
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)聽外部存儲(chǔ)狀態(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);
    // 動(dòng)態(tài)注冊(cè)廣播接收器
    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
    • 版本變更:訪問外部存儲(chǔ)的私有目錄不需要申請(qǐng)權(quán)限(API 19)
    <manifest...>
            <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                             android:maxSdkVersion="18" />
            ...
    </manifest>
    
    • 版本變更:動(dòng)態(tài)權(quán)限(API 23)

3.2 劃分

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

    • 特點(diǎn)

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

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

    • 特點(diǎn)

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

3.3 外部存儲(chǔ)API

外部存儲(chǔ) API

因?yàn)橥獠看鎯?chǔ)不一定可用,所以返回值可為空或空數(shù)組

  • 公共目錄:
storage/emulated/0/ 描述
Environment.getExternalStorageDirectory():File? 外部存儲(chǔ)根目錄
Environment.getExternalStoragePublicDirectory(name:String?):File? 外部存儲(chǔ)根目錄下的文件夾
Environment.getExternalStorageState():String! 外部存儲(chǔ)狀態(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)
  • 版本變更:多部分外部存儲(chǔ)——Context#getExternalFilesDirs()(API 18)

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

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

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


4. 補(bǔ)充

4.1 緩存文件

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

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

    • since Android O(since API 26)
      策略:系統(tǒng)分別為每個(gè)應(yīng)用設(shè)置緩存空間閾值,設(shè)備存儲(chǔ)空間不足時(shí),超過閾值的應(yīng)用將優(yōu)先刪除緩存,低于閾值的應(yīng)用緩存會(huì)被保留。系統(tǒng)會(huì)動(dò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)
        
      • 行為——緩存粒度
        // 整個(gè)文件夾視為一個(gè)緩存整體,在系統(tǒng)回收空間時(shí)清空文件夾
        sm.setCacheBehaviorGroup(dirFile,true)
        
      • 行為——保留文件結(jié)構(gòu)
        // 在系統(tǒng)回收文件時(shí),清空文件數(shù)據(jù)(length=0),而不是直接刪除文件
        sm.setCacheBehaviorTombstone(dirFile,true)
        
  • 清除應(yīng)用的數(shù)據(jù)的選項(xiàng)(在系統(tǒng)設(shè)置或手機(jī)管家中):
    • 清除緩存:清除應(yīng)用的內(nèi)部存儲(chǔ)緩存文件夾外部存儲(chǔ)緩存文件夾
    • 清除數(shù)據(jù):清除應(yīng)用的內(nèi)部存儲(chǔ)外部存儲(chǔ)空間私有目錄

4.2 android:installLocation

  • 可選值

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

  • 對(duì)于占用存儲(chǔ)空間較大的應(yīng)用來說,就有必要考慮安裝在外部存儲(chǔ)。舉例:反編譯王者榮耀查看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 存儲(chǔ)空間

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(wú)法避免多線程或多進(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{
          // 空間不足,請(qǐng)求用戶自行清理空間
          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)部存儲(chǔ) X X X
    私有內(nèi)部存儲(chǔ) 僅多媒體文件夾
    公共內(nèi)部存儲(chǔ)
  • 生存期


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


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

延伸閱讀

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

推薦閱讀


參考文獻(xiàn)


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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