前言
存儲適配系列文章:
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卡來擴展存儲空間。
如上圖,手機自身的存儲空間筐骇,稱之為機身存儲债鸡,在Android 4.4 之前作為內(nèi)部存儲使用。當(dāng)然內(nèi)部存儲空間一般是不夠用的铛纬,所以需要通過插入外置SD卡來擴充存儲空間娘锁,這當(dāng)做外部存儲。
Android 4.4之后
在Android 4.4 之后(含)饺鹃,手機機身存儲擴大了:
如上圖莫秆,機身存儲劃分為兩部分:
1、內(nèi)部存儲
2悔详、外部存儲
當(dāng)然镊屎,依然可以插入SD卡來擴充存儲空間,這部分的存儲空間稱為擴展的外部存儲空間茄螃。只是現(xiàn)在機身存儲都比較大缝驳,很少插入SD卡了。
接下來將以Android 4.4 之后的存儲劃分來分析具體的存儲方案归苍。
2用狱、內(nèi)部存儲
存放位置
回想一下平時使用的持久化方案:
1、SharedPreferences---->適用于存儲小文件
2拼弃、數(shù)據(jù)庫---->存儲結(jié)構(gòu)比較復(fù)雜的大文件
以上這些文件都是默認放在內(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)的目標設(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)進行寫入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、外部存儲
外部存儲分為兩部分:自帶外部存儲和擴展外部存儲(外置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、擴展外部存儲(外置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)部存儲废睦、自帶外部存儲伺绽、擴展外部存儲等,這幾者關(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忌傻。