前言
存儲適配系列文章:
Android-存儲基礎(chǔ)
Android-10、11-存儲完全適配(上)
Android-10、11-存儲完全適配(下)
Android-FileProvider-輕松掌握
在持久化數(shù)據(jù)的時候,一般都是選擇存入到文件里,本篇將著重分析Android 存儲相關(guān)的知識,也是為Android 10.0 11存儲適配打基礎(chǔ)。
通過本篇文章,你將了解到:
1、存儲劃分
2、內(nèi)部存儲
3、外部存儲
4、易混淆點說明
1、存儲劃分
Android 4.4 之前
在Android 4.4 之前,由于硬件發(fā)展受限,手機自身的存儲空間有限,需要通過外置SD卡來擴(kuò)展存儲空間。
如上圖,手機自身的存儲空間,稱之為機身存儲,在Android 4.4 之前作為內(nèi)部存儲使用。當(dāng)然內(nèi)部存儲空間一般是不夠用的,所以需要通過插入外置SD卡來擴(kuò)充存儲空間,這當(dāng)做外部存儲。
Android 4.4之后
在Android 4.4 之后(含),手機機身存儲擴(kuò)大了:
如上圖,機身存儲劃分為兩部分:
1、內(nèi)部存儲
2、外部存儲
當(dāng)然,依然可以插入SD卡來擴(kuò)充存儲空間,這部分的存儲空間稱為擴(kuò)展的外部存儲空間。只是現(xiàn)在機身存儲都比較大,很少插入SD卡了。
接下來將以Android 4.4 之后的存儲劃分來分析具體的存儲方案。
2、內(nèi)部存儲
存放位置
回想一下平時使用的持久化方案:
1、SharedPreferences---->適用于存儲小文件
2、數(shù)據(jù)庫---->存儲結(jié)構(gòu)比較復(fù)雜的大文件
以上這些文件都是默認(rèn)放在內(nèi)部存儲里。
"/" 表示根目錄,內(nèi)部存儲里給每個應(yīng)用按照其包名各自劃分了目錄,假設(shè)App的包名為:com.fish.myapplication
那么該文件在內(nèi)部存儲里的目錄為:
/data/user/0/com.fish.myapplication/
第一個"/"表示根目錄,其后每個"/"表示目錄分割符。
"0" 表示是第一個用戶,后續(xù)添加了多用戶則生成相應(yīng)的用戶目錄:
如上圖,新增了兩個用戶,生成的目錄分別是:"11"、"12"。目前來說,很少開啟多用戶的。
一般來說,adb shell里是沒有權(quán)限查看/data目錄的。若要查看內(nèi)部存儲,通常是通過Android Studio側(cè)邊欄Device File Explorer選擇對應(yīng)的目標(biāo)設(shè)備查看。
同樣的,如果包名為:com.fish.myapplication,則對應(yīng)的內(nèi)部存儲目錄為:
/data/data/com.fish.myapplication/
/data/user/0/com.fish.myapplication/ 會將值轉(zhuǎn)換到/data/data/com.fish.myapplication/ 路徑下。
每個App的內(nèi)部存儲空間僅允許自己訪問(除非有更高的權(quán)限,如root),程序卸載后,該目錄也會被刪除。
存儲內(nèi)容
除了SharedPreferences、數(shù)據(jù)庫文件,內(nèi)部存儲還存放了哪些文件呢?
為方便起見,只查看/data/data/目錄下的。
剛開始有只有兩個空目錄。
當(dāng)進(jìn)行寫入SharedPreferences,創(chuàng)建數(shù)據(jù)庫、寫入文件等操作后新增了幾個目錄:
大致介紹一下以上目錄作用:
1、cache-->存放緩存文件
2、code_cache-->存放運行時代碼優(yōu)化等產(chǎn)生的緩存
3、databases-->存放數(shù)據(jù)庫文件
4、files-->存放一般文件
5、shared_prefs-->存放SharedPreferences 文件
6、lib-->存放App依賴的so庫 是軟鏈接,指向/data/app/ 某個子目錄下
訪問方式
既然知道了各類文件存儲的目錄,那么如何讀寫這些文件呢?
我們知道在Java 的世界里,操作文件有兩種方式:
字符流和字節(jié)流
以字節(jié)流為為例,一個簡單的讀取寫入文件Demo:
//寫入文件
private void writeFile(String filePath) {
if (TextUtils.isEmpty(filePath))
return;
try {
File file = new File(filePath);
FileOutputStream fileOutputStream = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);
String writeContent = "hello world\n";
bos.write(writeContent.getBytes());
bos.flush();
bos.close();
} catch (Exception e) {
}
}
//從文件讀取
private void readFile(String filePath) {
if (TextUtils.isEmpty(filePath))
return;
try {
File file = new File(filePath);
FileInputStream fileInputStream = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fileInputStream);
byte[] readContent = new byte[1024];
int readLen = 0;
while (readLen != -1) {
readLen = bis.read(readContent, 0, readContent.length);
if (readLen > 0) {
String content = new String(readContent);
Log.d("test", "read content:" + content.substring(0, readLen));
}
}
fileInputStream.close();
} catch (Exception e) {
}
}
可以看出,通過FileInputStream/FileOutputStream構(gòu)造函數(shù)傳入File對象即可實現(xiàn)文件讀寫,而File對象的構(gòu)造依賴于文件的存放路徑,因此重點在于如何獲取文件的路徑。
分別說明各個目錄下文件的讀寫:
1、讀寫files目錄下文件
#Context.java
public abstract File getFilesDir();
使用方式:
private String getFilePath(Context context) {
//獲取files根目錄
File fileDir = context.getFilesDir();
//獲取文件
File myFile = new File(fileDir, "myFile");
return myFile.getAbsolutePath();
}
context.getFilesDir()的結(jié)果是返回files目錄:
/data/user/0/com.fish.myapplication/files/
拿到對應(yīng)文件的File對象后,構(gòu)造相應(yīng)的輸入輸出流即可實現(xiàn)對該文件的讀寫。可以看出,過程雖然簡單但是有點枯燥,因此Google將這些步驟封裝好了,直接返回對應(yīng)文件的FileOutputStream/FileInputStream:
#Context.java
public abstract FileInputStream openFileInput(String name)
throws FileNotFoundException;
public abstract FileOutputStream openFileOutput(String name, @FileMode int mode)
throws FileNotFoundException;
其中name 表示文件名,mode表示訪問權(quán)限。
2、讀寫cache目錄下文件
與讀取files目錄相似:
#Context.java
public abstract File getCacheDir();
context.getCacheDir()的結(jié)果是返回cache目錄:
/data/user/0/com.fish.myapplication/cache/
3、讀寫shared_prefs目錄下文件
SharedPreferences 提供了簡易的快速持久化數(shù)據(jù)的方案。
private void testSP(String fileName, String key, String value) {
if (TextUtils.isEmpty(fileName) || TextUtils.isEmpty(key) || TextUtils.isEmpty(value))
return;
//構(gòu)造SP文件
SharedPreferences sp = getSharedPreferences(fileName, MODE_PRIVATE);
//寫入SP
sp.edit().putString(key, value).commit();
//讀取SP
String myValue = sp.getString(key, "");
}
其內(nèi)部也是使用了輸入輸出流,以寫入SP文件為例:
#SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
...
//構(gòu)造輸出流
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
...
}
4、讀寫數(shù)據(jù)庫目錄下文件
創(chuàng)建數(shù)據(jù)庫:
MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(v.getContext(), "myDB", null, 10);
myDB是數(shù)據(jù)庫文件名。打開數(shù)據(jù)庫的相應(yīng)表,即可讀寫數(shù)據(jù)。
獲取數(shù)據(jù)庫文件路徑:
#Context.java
Context.public abstract File getDatabasePath(String name);
獲取結(jié)果如下:
/data/user/0/com.fish.myapplication/databases/myDB
5、讀寫code_cache目錄下文件
#Context.java API>=21
public abstract File getCodeCacheDir();
獲取結(jié)果如下:
/data/user/0/com.fish.myapplication/code_cache/
以上是分別列舉了各個子目錄/文件的獲取方式,如果想獲取:/data/user/0/com.fish.myapplication/,可通過:
#Context.java
public abstract File getDataDir();
該方法需要API>=24。
3、外部存儲
外部存儲分為兩部分:自帶外部存儲和擴(kuò)展外部存儲(外置SD卡)
A、自帶外部存儲存儲
存放位置
存儲的根目錄是:"/"。
根目錄下幾個需要關(guān)注的目錄:
/data/
/sdcard/
/storage/
其中/data/目錄前面已經(jīng)分析過。
/sdcard/是軟鏈接,指向/storage/self/primary
而/storage/下有幾個目錄:
/storage/self/primary/是軟鏈接,指向/storage/emulated/0/
也就是說/sdcard/、/storage/self/primary/ 真正指向的是/storage/emulated/0/
存儲內(nèi)容
如上圖所示,/sdcard/目錄下的子目錄看起來都比較眼熟。
這些子目錄分為分為三部分:
第一部分:共享存儲空間
也就是所有App共享的部分,比如相冊、音樂、鈴聲、文檔等。
共享存儲空間按文件類型又分為兩部分:
1、媒體文件
- DCIM/ 和 Pictures/-->存儲圖片
- DCIM/、Movies/ 和 Pictures-->存儲視頻
- Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/-->存儲音頻文件
- Download/-->下載的文件
2、文檔和其它文件
Documents-->存儲如.pdf類型等文件
第二部分:App外部私有目錄
- Android/data/--->存儲各個App的外部私有目錄
與內(nèi)部存儲類似,命名方式是:Android/data/xx------>xx指應(yīng)用的包名。
如:/sdcard/Android/data/com.fish.myapplication
第三部分:其它目錄
比如各個App在/sdcard/目錄下創(chuàng)建的目錄,如支付寶創(chuàng)建的目錄:alipy/,微博創(chuàng)建的目錄:com.sina.weibo/,qq創(chuàng)建的目錄:com.tencent.mobileqq/等。
訪問方式
與訪問內(nèi)部存儲文件類似,外部存儲也可以通過構(gòu)造輸入輸出流訪問文件。
讀寫共享存儲空間
視頻、圖片等可能分散存儲在各個不同的目錄里,如果想要獲取所有的圖片地址,那么得需要遍歷不同的目錄尋找,效率顯而易見的低。Android 將視頻、圖片等信息存儲在數(shù)據(jù)庫里,每當(dāng)某個App想要訪問這些共享的媒體文件時只需要查找數(shù)據(jù)庫對應(yīng)的表,讀取符合條件的行,找出每個媒體的文件路徑等信息。
App查詢共享存儲空間的媒體方式是:通過ContentProvider訪問。
訪問媒體文件
以查詢圖片為例:
private void getImagePath(Context context) {
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);
while(cursor.moveToNext()) {
String imagePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));
}
}
查詢到圖片的地址,當(dāng)然就可以展示圖片了。
訪問文檔和其它文件
Storage Access Framework 簡稱SAF:存儲訪問框架
以查看.pdf文件為例:
private void startSAF() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");
startActivityForResult(intent, 100);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100) {
Uri uri = data.getData();
}
}
SAF實際上就是調(diào)用系統(tǒng)提供的選擇器,選中后在onActivityResult(xx)里接收結(jié)果,拿到Uri后當(dāng)然就可以讀寫對應(yīng)的文件了。
讀寫App外部私有目錄
剛開始并沒有自己App的包名。
調(diào)用如下方法后:
private void testAppDir(Context context) {
//4個基本方法
File fileDir = context.getExternalFilesDir(null);
//API>=19
File[] fileList = context.getExternalFilesDirs(null);
File cacheDir = context.getExternalCacheDir();
//API>=19
File[] cacheList = context.getExternalCacheDirs();
//指定目錄,自動生成對應(yīng)的子目錄
File fileDir2 = context.getExternalFilesDir(Environment.DIRECTORY_DCIM);
}
再查看目錄樹:
可以看出再/sdcard/Android/data/目錄下生成了com.fish.myapplication/目錄,該目錄下有兩個子目錄分別是:files/、cache/。當(dāng)然也可以選擇創(chuàng)建其它目錄。
2、App卸載的時候,兩者都會被清除。
讀寫其它目錄
只要拿到根目錄,就可以遍歷尋找其它子目錄/文件。
private void testOtherDir(Context context) {
File rootDir = Environment.getExternalStorageDirectory();
}
返回的rootDir路徑:/storage/emulated/0/。
B、擴(kuò)展外部存儲(外置SD卡)
存儲位置
當(dāng)給設(shè)備插入SD卡后,查看其目錄:
/sdcard/ 依然指向/storage/self/primary,繼續(xù)來看/storage/:
可以看出,多了sdcard1,軟鏈接指向了/storage/77E4-07E7/。
存儲內(nèi)容
取決于SD卡上裝了什么東西。
訪問方式
還記得上面獲取外部存儲-App私有目錄方式嗎?
File[] fileList = context.getExternalFilesDirs(null);
返回File對象數(shù)組,當(dāng)有多個外部存儲時候,存儲在數(shù)組里。
返回的數(shù)組有兩個元素,一個是自帶外部存儲存儲,另一個是剛插入的SD卡。
拿到路徑后,當(dāng)然就可以訪問相應(yīng)的文件了。
4、易混淆點說明
以上分別闡述了內(nèi)部存儲、自帶外部存儲、擴(kuò)展外部存儲等,這幾者關(guān)系如下:
其中比較容易混淆的是:
內(nèi)部存儲與外部存儲里的App私有目錄,兩者命名風(fēng)格很像。
不同點:
/data/data/com.fish.myapplication/ 位于內(nèi)部存儲,一般用于存儲容量較小的,私密性較強的文件。而/sdcard/Android/data/com.fish.myapplication/ 位于外部存儲,作為App私有目錄,一般用于存儲容量較大的文件,即使刪除了也不影響App正常功能。
相同點:
1、屬于App專屬,App自身訪問兩者無需任何權(quán)限。
2、App卸載后,兩者皆被刪除。
3、兩者目錄下增加的文件最終會被統(tǒng)計到"設(shè)置->存儲和緩存"里。
另外,常見的在設(shè)置里的"存儲與緩存"項:
當(dāng)點擊"Clear cache" 時:
內(nèi)部存儲/data/data/com.fish.myapplication/cache/、 /data/data/com.fish.myapplication/code_cache/目錄會被清空
外部存儲/sdcard/Android/data/com.fish.myapplication/cache/ 會被清空
當(dāng)點擊"Clear storage" 時:
內(nèi)部存儲/data/data/com.fish.myapplication/下除了lib/,其余子目錄皆被刪除
外部存儲/sdcard/Android/data/com.fish.myapplication/被清空
接下來將分析Android 10.0 11 存儲適配。
本文基于Android 10.0。