帶你徹底搞懂 Android 存儲(chǔ)?。ńㄗh收藏)
BAT
作者:小魚(yú)人愛(ài)編程
鏈接:https://www.jianshu.com/p/93c9f5e2d2a7
在持久化數(shù)據(jù)的時(shí)候,一般都是選擇存入到文件里,本篇將著重分析Android 存儲(chǔ)相關(guān)的知識(shí), 通過(guò)本篇文章,你將了解到:
存儲(chǔ)劃分 內(nèi)部存儲(chǔ) 外部存儲(chǔ) 易混淆點(diǎn)說(shuō)明
1. 存儲(chǔ)劃分
1.1 Android 4.4 之前
在A(yíng)ndroid 4.4 之前,由于硬件發(fā)展受限,手機(jī)自身的存儲(chǔ)空間有限,需要通過(guò)外置SD卡來(lái)擴(kuò)展存儲(chǔ)空間。
如上圖,手機(jī)自身的存儲(chǔ)空間,稱(chēng)之為機(jī)身存儲(chǔ),在A(yíng)ndroid 4.4 之前作為內(nèi)部存儲(chǔ)使用。當(dāng)然內(nèi)部存儲(chǔ)空間一般是不夠用的,所以需要通過(guò)插入外置SD卡來(lái)擴(kuò)充存儲(chǔ)空間,這當(dāng)做外部存儲(chǔ)。
1.2 Android 4.4之后
在A(yíng)ndroid 4.4 之后(含),手機(jī)機(jī)身存儲(chǔ)擴(kuò)大了:
如上圖,機(jī)身存儲(chǔ)劃分為兩部分:內(nèi)部存儲(chǔ)和外部存儲(chǔ)
當(dāng)然,依然可以插入SD卡來(lái)擴(kuò)充存儲(chǔ)空間,這部分的存儲(chǔ)空間稱(chēng)為擴(kuò)展的外部存儲(chǔ)空間。只是現(xiàn)在機(jī)身存儲(chǔ)都比較大,很少插入SD卡了。
接下來(lái)將以Android 4.4 之后的存儲(chǔ)劃分來(lái)分析具體的存儲(chǔ)方案。
2. 內(nèi)部存儲(chǔ)
2.1 存儲(chǔ)位置
回想一下平時(shí)使用的持久化方案:
-
SharedPreferences:適用于存儲(chǔ)小文件 -
數(shù)據(jù)庫(kù):存儲(chǔ)結(jié)構(gòu)比較復(fù)雜的大文件
以上這些文件都是默認(rèn)放在內(nèi)部存儲(chǔ)里。"/" 表示根目錄,內(nèi)部存儲(chǔ)里給每個(gè)應(yīng)用按照其包名各自劃分了目錄,假設(shè)App的包名為:com.fish.myapplication那么該文件在內(nèi)部存儲(chǔ)里的目錄為:/data/user/0/com.fish.myapplication/
第一個(gè)"/"表示根目錄,其后每個(gè)"/"表示目錄分割符。"0" 表示是第一個(gè)用戶(hù),后續(xù)添加了多用戶(hù)則生成相應(yīng)的用戶(hù)目錄:
如上圖,新增了兩個(gè)用戶(hù),生成的目錄分別是:"11"、"12"。目前來(lái)說(shuō),很少開(kāi)啟多用戶(hù)的。一般來(lái)說(shuō),adb shell 里是沒(méi)有權(quán)限查看/data目錄的。若要查看內(nèi)部存儲(chǔ),通常是通過(guò) Android Studio側(cè)邊欄Device File Explorer選擇對(duì)應(yīng)的目標(biāo)設(shè)備查看。
同樣的,如果包名為:com.fish.myapplication,則對(duì)應(yīng)的內(nèi)部存儲(chǔ)目錄為:/data/data/com.fish.myapplication//data/user/0/com.fish.myapplication/ 會(huì)將值轉(zhuǎn)換到/data/data/com.fish.myapplication/路徑下。每個(gè)App的內(nèi)部存儲(chǔ)空間僅允許自己訪(fǎng)問(wèn)(除非有更高的權(quán)限,如root),程序卸載后,該目錄也會(huì)被刪除。
2.2 存儲(chǔ)內(nèi)容
除了SharedPreferences、數(shù)據(jù)庫(kù)文件,內(nèi)部存儲(chǔ)還存放了哪些文件呢?為方便起見(jiàn),只查看/data/data/目錄下的。
剛開(kāi)始有只有兩個(gè)空目錄。當(dāng)進(jìn)行寫(xiě)入SharedPreferences,創(chuàng)建數(shù)據(jù)庫(kù)、寫(xiě)入文件等操作后新增了幾個(gè)目錄:
大致介紹一下以上目錄作用:
| 目錄 | 用途 |
|---|---|
| cache | 存放緩存文件 |
| code_cache | 存放運(yùn)行時(shí)代碼優(yōu)化等產(chǎn)生的緩存 |
| databases | 存放數(shù)據(jù)庫(kù)文件 |
| files | 存放一般文件 |
| shared_prefs | 存放 SharedPreferences 文件 |
| lib | 存放App依賴(lài)的so庫(kù) 是軟鏈接,指向/data/app/ 某個(gè)子目錄下 |
2.3 訪(fǎng)問(wèn)方式
既然知道了各類(lèi)文件存儲(chǔ)的目錄,那么如何讀寫(xiě)這些文件呢?我們知道在Java 的世界里,操作文件有兩種方式:字符流和字節(jié)流
以字節(jié)流為為例,一個(gè)簡(jiǎn)單的讀取寫(xiě)入文件Demo:
//寫(xiě)入文件
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) {
}
}
可以看出,通過(guò) FileInputStream/FileOutputStream 構(gòu)造函數(shù)傳入 File 對(duì)象即可實(shí)現(xiàn)文件讀寫(xiě),而 File 對(duì)象的構(gòu)造依賴(lài)于文件的存放路徑,因此重點(diǎn)在于如何獲取文件的路徑。分別說(shuō)明各個(gè)目錄下文件的讀寫(xiě):
2.3.1 讀寫(xiě)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/
拿到對(duì)應(yīng)文件的File對(duì)象后,構(gòu)造相應(yīng)的輸入輸出流即可實(shí)現(xiàn)對(duì)該文件的讀寫(xiě)??梢钥闯?,過(guò)程雖然簡(jiǎn)單但是有點(diǎn)枯燥,因此Google將這些步驟封裝好了,直接返回對(duì)應(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表示訪(fǎng)問(wèn)權(quán)限。
2.3.2 讀寫(xiě)cache目錄下文件
與讀取files目錄相似:
#Context.java
public abstract File getCacheDir();
context.getCacheDir()的結(jié)果是返回cache目錄:
/data/user/0/com.fish.myapplication/cache/
2.3.3 讀寫(xiě)shared_prefs目錄下文件
SharedPreferences 提供了簡(jiǎn)易的快速持久化數(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);
//寫(xiě)入SP
sp.edit().putString(key, value).commit();
//讀取SP
String myValue = sp.getString(key, "");
}
其內(nèi)部也是使用了輸入輸出流,以寫(xiě)入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();
...
}
2.3.4 讀寫(xiě)數(shù)據(jù)庫(kù)目錄下文件
創(chuàng)建數(shù)據(jù)庫(kù):
MyDatabaseHelper myDatabaseHelper = new MyDatabaseHelper(v.getContext(), "myDB", null, 10);
myDB是數(shù)據(jù)庫(kù)文件名。打開(kāi)數(shù)據(jù)庫(kù)的相應(yīng)表,即可讀寫(xiě)數(shù)據(jù)。
獲取數(shù)據(jù)庫(kù)文件路徑:
#Context.java
Context.public abstract File getDatabasePath(String name);
獲取結(jié)果如下:
/data/user/0/com.fish.myapplication/databases/myDB
2.3.5 讀寫(xiě)code_cache目錄下文件
#Context.java API>=21
public abstract File getCodeCacheDir();
獲取結(jié)果如下:
/data/user/0/com.fish.myapplication/code_cache/
以上是分別列舉了各個(gè)子目錄/文件的獲取方式,如果想獲?。?code style="">/data/user/0/com.fish.myapplication/,可通過(guò):
#Context.java
public abstract File getDataDir();
該方法需要 API>=24。
3. 外部存儲(chǔ)
外部存儲(chǔ)分為兩部分:自帶外部存儲(chǔ)和擴(kuò)展外部存儲(chǔ)(外置SD卡)
3.1 自帶外部存儲(chǔ)存儲(chǔ)
3.1.1 存儲(chǔ)位置
自帶外部存儲(chǔ)的根目錄是:"/"。
根目錄下幾個(gè)需要關(guān)注的目錄:
-
/data/ -
/sdcard/ -
/storage/
其中/data/目錄前面已經(jīng)分析過(guò)。
/sdcard/是軟鏈接,指向/storage/self/primary
而/storage/下有幾個(gè)目錄:
/storage/self/primary/ 是軟鏈接,指向/storage/emulated/0/
也就是說(shuō)/sdcard/、/storage/self/primary/ 真正指向的是/storage/emulated/0/
3.1.2 存儲(chǔ)內(nèi)容
自帶外部存儲(chǔ)主要有以下內(nèi)容:
如上圖所示,/sdcard/目錄下的子目錄看起來(lái)都比較眼熟。這些子目錄分為分為三部分:
第一部分:共享存儲(chǔ)空間
也就是所有App共享的部分,比如相冊(cè)、音樂(lè)、鈴聲、文檔等。共享存儲(chǔ)空間按文件類(lèi)型又分為兩部分:
-
媒體文件
| 目錄 | 用途 |
|---|---|
| DCIM/ 和 Pictures/ | 存儲(chǔ)圖片 |
| DCIM/、Movies/ 和 Pictures | 存儲(chǔ)視頻 |
| Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/ | 存儲(chǔ)音頻文件 |
| Download/ | 下載的文件 |
-
文檔和其它文件
| 目錄 | 用途 |
|---|---|
| Documents | 存儲(chǔ)如.pdf類(lèi)型等文件 |
第二部分:App外部私有目錄
| 目錄 | 用途 |
|---|---|
| Android/data/ | 存儲(chǔ)各個(gè)App的外部私有目錄,與內(nèi)部存儲(chǔ)類(lèi)似,命名方式是:Android/data/xx(xx指應(yīng)用的包名)。如:/sdcard/Android/data/com.fish.myapplication |
Android/data/--->存儲(chǔ)各個(gè)App的外部私有目錄 與內(nèi)部存儲(chǔ)類(lèi)似,命名方式是:Android/data/xx------>xx指應(yīng)用的包名。如:/sdcard/Android/data/com.fish.myapplication
第三部分:其它目錄
比如各個(gè)App在/sdcard/目錄下創(chuàng)建的目錄,如支付寶創(chuàng)建的目錄:alipy/,微博創(chuàng)建的目錄:com.sina.weibo/,qq創(chuàng)建的目錄:com.tencent.mobileqq/等。
3.1.3 訪(fǎng)問(wèn)方式
與訪(fǎng)問(wèn)內(nèi)部存儲(chǔ)文件類(lèi)似,外部存儲(chǔ)也可以通過(guò)構(gòu)造輸入輸出流訪(fǎng)問(wèn)文件。
讀寫(xiě)共享存儲(chǔ)空間
視頻、圖片等可能分散存儲(chǔ)在各個(gè)不同的目錄里,如果想要獲取所有的圖片地址,那么得需要遍歷不同的目錄尋找,效率顯而易見(jiàn)的低。Android 將視頻、圖片等信息存儲(chǔ)在數(shù)據(jù)庫(kù)里,每當(dāng)某個(gè)App想要訪(fǎng)問(wèn)這些共享的媒體文件時(shí)只需要查找數(shù)據(jù)庫(kù)對(duì)應(yīng)的表,讀取符合條件的行,找出每個(gè)媒體的文件路徑等信息。
App查詢(xún)共享存儲(chǔ)空間的媒體方式是:通過(guò)ContentProvider訪(fǎng)問(wèn)。
-
訪(fǎng)問(wèn)媒體文件
以查詢(xún)圖片為例:
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));
}
}
查詢(xún)到圖片的地址,當(dāng)然就可以展示圖片了。
-
訪(fǎng)問(wèn)文檔和其它文件Storage Access Framework 簡(jiǎn)稱(chēng)SAF:存儲(chǔ)訪(fǎng)問(wèn)框架
以查看.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實(shí)際上就是調(diào)用系統(tǒng)提供的選擇器,選中后在 onActivityResult(xx) 里接收結(jié)果,拿到Uri 后當(dāng)然就可以讀寫(xiě)對(duì)應(yīng)的文件了。
讀寫(xiě)App外部私有目錄
剛開(kāi)始并沒(méi)有自己App的包名。
調(diào)用如下方法后:
private void testAppDir(Context context) {
//4個(gè)基本方法
File fileDir = context.getExternalFilesDir(null);
//API>=19
File[] fileList = context.getExternalFilesDirs(null);
File cacheDir = context.getExternalCacheDir();
//API>=19
File[] cacheList = context.getExternalCacheDirs();
//指定目錄,自動(dòng)生成對(duì)應(yīng)的子目錄
File fileDir2 = context.getExternalFilesDir(Environment.DIRECTORY_DCIM);
}
再查看目錄樹(shù):
可以看出再/sdcard/Android/data/目錄下生成了com.fish.myapplication/目錄,該目錄下有兩個(gè)子目錄分別是:files/、cache/。當(dāng)然也可以選擇創(chuàng)建其它目錄。
App卸載的時(shí)候,兩者都會(huì)被清除。
讀寫(xiě)其它目錄
只要拿到根目錄,就可以遍歷尋找其它子目錄/文件。
private void testOtherDir(Context context) {
File rootDir = Environment.getExternalStorageDirectory();
}
返回的rootDir路徑:/storage/emulated/0/。
3.2 擴(kuò)展外部存儲(chǔ)(外置SD卡)
3.2.1 存儲(chǔ)位置
當(dāng)給設(shè)備插入SD卡后,查看其目錄:
/sdcard/ 依然指向 /storage/self/primary,繼續(xù)來(lái)看/storage/:
可以看出,多了sdcard1,軟鏈接指向了 /storage/77E4-07E7/。
3.2.2 存儲(chǔ)內(nèi)容
取決于SD卡上裝了什么東西。
3.2.3 訪(fǎng)問(wèn)方式
還記得上面獲取外部存儲(chǔ)-App私有目錄方式嗎?
File[] fileList = context.getExternalFilesDirs(null);
返回File對(duì)象數(shù)組,當(dāng)有多個(gè)外部存儲(chǔ)時(shí)候,存儲(chǔ)在數(shù)組里。
返回的數(shù)組有兩個(gè)元素,一個(gè)是自帶外部存儲(chǔ)存儲(chǔ),另一個(gè)是剛插入的SD卡。拿到路徑后,當(dāng)然就可以訪(fǎng)問(wèn)相應(yīng)的文件了。
4. 易混淆點(diǎn)說(shuō)明
以上分別闡述了內(nèi)部存儲(chǔ)、自帶外部存儲(chǔ)、擴(kuò)展外部存儲(chǔ)等,這幾者關(guān)系如下:
其中比較容易混淆的是:內(nèi)部存儲(chǔ)與外部存儲(chǔ)里的App私有目錄,兩者命名風(fēng)格很像。
4.1 不同點(diǎn)
/data/data/com.fish.myapplication/ 位于內(nèi)部存儲(chǔ),一般用于存儲(chǔ)容量較小的,私密性較強(qiáng)的文件。而/sdcard/Android/data/com.fish.myapplication/ 位于外部存儲(chǔ),作為App私有目錄,一般用于存儲(chǔ)容量較大的文件,即使刪除了也不影響App正常功能。
4.2 相同點(diǎn)
-
屬于A(yíng)pp專(zhuān)屬,App自身訪(fǎng)問(wèn)兩者無(wú)需任何權(quán)限。 -
App卸載后,兩者皆被刪除。 -
兩者目錄下增加的文件最終會(huì)被統(tǒng)計(jì)到"設(shè)置->存儲(chǔ)和緩存"里。
另外,常見(jiàn)的在設(shè)置里的"存儲(chǔ)與緩存"項(xiàng):
當(dāng)點(diǎn)擊"Clear cache" 時(shí):
-
內(nèi)部存儲(chǔ) /data/data/com.fish.myapplication/cache/、/data/data/com.fish.myapplication/code_cache/目錄會(huì)被清空 -
外部存儲(chǔ) /sdcard/Android/data/com.fish.myapplication/cache/會(huì)被清空
當(dāng)點(diǎn)擊"Clear storage" 時(shí):
-
內(nèi)部存儲(chǔ) /data/data/com.fish.myapplication/下除了lib/,其余子目錄皆被刪除 -
外部存儲(chǔ) /sdcard/Android/data/com.fish.myapplication/被清空
注:該功能慎用,因?yàn)闀?huì)刪除用戶(hù)數(shù)據(jù)庫(kù),SP文件等,相當(dāng)于重置了App
推薦閱讀
? 耗時(shí)2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!
BATcoder技術(shù)群,讓一部分人先進(jìn)大廠(chǎng)
大家好,我是劉望舒,騰訊TVP,著有三本業(yè)內(nèi)知名暢銷(xiāo)書(shū),連續(xù)四年蟬聯(lián)電子工業(yè)出版社年度優(yōu)秀作者,谷歌開(kāi)發(fā)者社區(qū)特邀講師,百度百科收錄的高級(jí)技術(shù)專(zhuān)家。
前華為技術(shù)專(zhuān)家,現(xiàn)大廠(chǎng)技術(shù)負(fù)責(zé)人。
想要加入 BATcoder技術(shù)群,公號(hào)回復(fù)BAT 即可。
為了防止失聯(lián),歡迎關(guān)注我的小號(hào)
微信改了推送機(jī)制,真愛(ài)請(qǐng)星標(biāo)本公號(hào)
