前言
- Android中經(jīng)常需要使用文件存儲(chǔ)用戶數(shù)據(jù)
- 本文將梳理各個(gè)版本中的文件存儲(chǔ),希望能幫上忙。
1. 簡(jiǎn)介
Android開發(fā)中有五種數(shù)據(jù)持久化API:
2. 內(nèi)部存儲(chǔ)空間(Internal Storage)
2.1 劃分
- 目錄:/data/data/
- 特點(diǎn):
- 每個(gè)應(yīng)用獨(dú)占一個(gè)以包名命名的私有文件夾
- 在應(yīng)用卸載時(shí)被刪除
- 對(duì)MediaScanner不可見
- 適用場(chǎng)景:私密數(shù)據(jù)
2.2 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_READABLE 和 MODE_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 劃分
-
私有目錄(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
因?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)
- 閾值
- before Android O(before API 26)
- 清除應(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)化]
推薦閱讀
- 計(jì)算機(jī)組成原理 | 為什么浮點(diǎn)數(shù)運(yùn)算不精確?(阿里筆試)
- Java | 使用 ThreadLocal 實(shí)現(xiàn)無(wú)鎖線程安全
- Android | 自定義屬性
- Android | 再按一次返回鍵退出
- Android | InputManagerService 與輸入事件采集
- 設(shè)計(jì)模式 | 靜態(tài)代理與動(dòng)態(tài)代理
- 筆記 | 使用 Keytool 管理密鑰和證書
參考文獻(xiàn)
- 《Android Application Secure Design/Secure Coding Guidebook》 日本安全編碼工作組 著
- 《Android高性能編程》[西]Enrique Lopez Manas,[意]Diego Grancini 著
- 《Keep Sensitive Data Safe and Private》— Developers
- 《App data & file》— Developers
- 《Files for Miles: Where to Store Them All?》 — Android Dev Summit