安卓APP的儲存目錄+ FileProvider,總結持久化資料的技巧
安卓儲存目錄分為 內部儲存 和 外部儲存。 內部儲存的目錄為 /data/ 目錄, 其中 內部儲存 在未root的手機上是無法檢視的。
要了解APP的儲存目錄結構,我們先從 app開始安裝時談起。
一、apk在安裝時,涉及到目錄
在系統開始安裝一個apk時,系統會先將apk檔案複製到 data/app/ 目錄下,再解析apk資訊,然後dexopt優化操作。
內部儲存在未root的手機上雖然無法檢視的,但在Android Studio 3.1.4 中可以藉助 Device File Explorer 工具來檢視 內部儲存空間。 這個工具在 AS 的右下角, 如果不小心移除了,可以來下面 右邊 這張圖裡找到。
以MIUI 8.2為例, 這些不同包名的內部儲存了兩大塊內容,一個so檔案;另一個是oat檔案。
so和oat 這兩部分檔案是系統能執行此app的基礎,app的機器程式碼都儲存在這裡。 為安全著想,沒有root的手機即使藉助 Device File Explorer 也不能檢視 oat裡儲存的 odex檔案。
1.1、oat歷史介紹
ART和Dalvik都算是一種Android執行時環境,或者叫做虛擬機器,用來解釋dex型別檔案。但是ART是安裝時解釋,Dalvik是執行時解釋。 4.4 以前的版本使用的是 dalvik 虛擬機器,在4.4版本上,兩種執行時環境共存,可以相互切換,但是在5.0+,Dalvik虛擬機器則被徹底的丟棄,全部採用ART。
dalvik: 在5.0 以前 的 Dalvik時代,app每次啟動時,系統都需要通過 即時編譯器jit(Just-In-Time實時編譯) 將dex檔案或odex翻譯成能被虛擬機器載入的native code , 最終產物是相同名稱的 dey檔案(表示這是一個優化過的dex),這樣使得 dalvik 虛擬機器的 app啟動速度很慢。
ART: 5.0 及之後的 ART虛擬機器中,完全拋棄了dalvik的JIT, 使用了AOT直接在安裝時將其完全翻譯成native code.這一技術的引入,使得虛擬機器執行指令的速度又一重大提升。
oat: 是 AOT 在安裝apk時 生成的 native code,對應的檔案字尾為 *.oat(實際上是一個自定義的elf檔案,裡面包含的都是本地機器指令)), o是optimize(優化)的縮寫,a是android的縮寫,t是runTime的意思,oat 是在apk安裝時通過dexopt工具將dex檔案優化成二進位制格式的檔案,然後再通過AOT(Ahead-Of-Time 預先編譯)生成 能被art虛擬機器執行的機器 嗎,從而加快app的啟動速度。
在4.4版本上,兩種執行時環境共存,可以相互切換,但是在5.0+,Dalvik虛擬機器則被徹底的丟棄,全部採用ART。
二、apk在執行時,涉及到目錄
上面我們介紹了 安裝apk 後的相關目錄,在我們執行一個app後,程式碼裡所涉及到的檔案 如 資料庫、sp、webview快取等都儲存到了 data/data/包名/ 目錄下。
"data/app/包名" 和 "data/data/包名" 都是應用私有目錄,只允許應用內部訪問, 其它應用的程序是無法訪問的。
注意:當用戶解除安裝 App 時,系統自動刪除 data/data 目錄下對應包名的資料夾及其內容。
具體目錄 如下圖:
三、sdk所提供的方法與內部儲存的對應關係
Android SDK 提供有如下方法可以獲取並操作 內部儲存 空間下應用 私有目錄檔案 的方法,都位於 Application Context 中,供開發者直接呼叫:getFilesDir()、getCacheDir()、deleteFile()、fileList()、Environment.getDataDirectory();
四、外部儲存目錄
外部儲存目錄的路徑為 "Android/data/包名" ,主要是考慮到內部儲存空間容量有限,普通使用者不能直接直觀地檢視目錄檔案等其他原因,Android 在外部儲存空間中也提供有特殊目錄供應用存放私有檔案。
一般裝置都有內建 SD 卡,同時也提供外部 SD 卡拓展,可能對應路徑的目錄名有所差異。
值得注意的是,與內部儲存空間的應用私有目錄不同的是:
第一,預設情況下,系統並不會自動建立外部儲存空間的應用私有目錄。只有在應用需要的時候,開發人員通過 SDK 提供的 API 建立該目錄資料夾和操作資料夾內容。
第二,自 Android 7.0 開始,系統對外部儲存目錄中 應用私有目錄的訪問許可權進一步限制。其他 App 無法通過 file:// 這種形式的 Uri 直接讀寫 非自己app外部私有目錄下的檔案內容,而是需要通過 FileProvider 訪問。(關於這個內容,接下來再寫一篇文章專門說說 7.0 的適配問題,歡迎關注我的微信公眾號:安卓筆記俠。)
第三,宿主 App 可以直接讀寫內部儲存空間中的應用私有目錄;而在 4.4 版本開始,宿主 App 才可以直接讀寫外部儲存空間中的應用私有目錄,使開發人員無需在 Manifest 檔案中或者動態申請外部儲存空間的檔案讀寫許可權。
而相同點在於:同屬於應用私有目錄,當用戶解除安裝 App 時,系統也會自動刪除外部儲存空間下的對應 App 私有目錄資料夾及其內容。
同樣,Android SDK 中也提供有便捷的 API 供開發人員直接操作外部儲存空間下的應用私有目錄:getExternalFilesDir()、getExternalCacheDir()、Environment.getExternalStorageDirectory();
區別是,在4.4之後通過 Environment 訪問外部 儲存空間時需要讀寫儲存卡許可權。
注意:對於外部儲存空間下的應用私有目錄檔案,由於普通使用者可以自由修改和刪除,開發人員在使用時,一定要做好判空處理和異常捕獲,防止應用崩潰退出!
五、外部儲存中的公有空間
注意:訪問外部儲存空間 的 非應用私有目錄 時記得申請讀寫許可權!
外部儲存空間已經為使用者預設分類出一些公共目錄。開發人員可以通過 Environment 類提供的方法直接獲取相應目錄的絕對路徑,Environment.getExternalStoragePublicDirectory(String type);
傳遞不同的 type 引數型別即可:Envinonment 類提供諸多 type 引數的常量,比如:
DIRECTORY_MUSIC:Music
DIRECTORY_MOVIES:Movies
DIRECTORY_PICTURES:Pictures
DIRECTORY_DOWNLOADS:Download
以第一個常量為例,音樂類別的公共目錄絕對路徑為:/storage/emulated/0/Music。如果你使用檔案管理器開啟裝置的外部儲存空間的話,均可以看到這些公共目錄資料夾。
六、外部儲存中的 其它目錄
一般來說,利用兩種應用私有目錄和公共目錄便能夠儲存應用中需要儲存的資料和檔案。如果這些還不夠的話,那一定是你的開發姿勢不對。在 Code Review 的前提下,如果還是不夠的話,還可以在外部儲存空間自由建立其他目錄,通過這個方式獲取外部儲存空間的絕對路徑,然後操作檔案:Environment.getExternalStorageDirectory();
七、需要注意的地方
使用應用私有目錄儲存應用相關資料,使用公共目錄儲存應用無關資料(共享資料)。無論哪種情況,都需要做好資料分類儲存,便於清除等統一管理。隨便開啟手機上的幾個應用,不難發現,很多應用都包含一個清理快取的功能。事實上,開發人員清理的就是應用相關資料,也就是應用私有目錄下的檔案。
考慮到外部儲存空間上的內容可能被使用者手動刪除,或者解除安裝拓展 SD 卡等不可控因素,操作前記得使用 Environment 類提供的 API 方法判斷容量是否充足、檔案是否存在等情況,做好異常捕獲,減少應用崩潰率。相信這一定是一個良好的習慣。
八、補充7.0 FileProvider的適配
在Android 7.0以前, 可以使用file://uri的方式訪問外部儲存中的 其它應用 的私有目錄的檔案,但是這有個問題,就是即使不是你自身應用產生的檔案,只要知道對方的uri則就可以呼叫到,這樣在安全性上就產生了風險。
所以Android 7.0後新增了對檔案跨程序訪問的限制,這個限制會造成,如果使用file://uri的方式訪問,則會出現android.os.FileUriExposedException的異常。
FileProvider 的註冊 有兩大步驟
1、 在 manifest.xml 中註冊
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.yourname"
android:exported="false"
android:grantUriPermissions="true">
...
</provider>
...
</application>
2、在 res/xml 目錄下 新增 共享目錄標識 檔案
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
...
</paths>
其中 paths 的 標籤 可以配置多組,每一組也有多種選擇,具體規則如下:<files-path>
:內部儲存空間應用私有目錄下的 files/ 目錄,等同於 Context.getFilesDir()
所獲取的目錄路徑;<cache-path>
:內部儲存空間應用私有目錄下的 cache/ 目錄,等同於 Context.getCacheDir()
所獲取的目錄路徑;<external-path>
:外部儲存空間根目錄,等同於 Environment.getExternalStorageDirectory()
所獲取的目錄路徑;<external-files-path>
:外部儲存空間應用私有目錄下的 files/ 目錄,等同於 Context.getExternalFilesDir(null)
所獲取的目錄路徑;<external-cache-path>
:外部儲存空間應用私有目錄下的 cache/ 目錄,等同於 Context.getExternalCacheDir();
可以看出,這五種子元素基本涵蓋內外儲存空間所有目錄路徑,包含應用私有目錄。同時,每個子元素都擁有 name 和 path 兩個屬性。
其中,path 屬性用於指定當前子元素所代表目錄下需要共享的子目錄名稱。
注意:path 屬性值不能使用具體的獨立檔名,只能是目錄名。
而 name 屬性用於給 path 屬性所指定的子目錄名稱取一個別名。後續生成 content:// URI 時,會使用這個別名代替真實目錄名。這樣做的目的,很顯然是為了提高安全性。
如果我們需要分享的檔案位於同級別目錄下不同的子目錄中,就需要新增多個子元素逐一指定要分享的檔案目錄,或者共享他們通用的父目錄也行。
新增完共享目錄後,再在 <provider>
元素中使用 <meta-data>
元素將 res/xml 中的 path 檔案與註冊的 FileProvider 連結起來:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.yourname"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/yourfilename" />
</provider>
經過這兩大步,我們已經完成了 FileProvider 註冊。 這樣其它 app 就能 使用 我們app FileProvider 所代表的目錄下的檔案。 具體使用的步驟大概如下 :
1、FileProvider.getUriForFile 構造 contentUri
2、申請 uri 訪問許可權
3、startActivity 來啟動此 contentUri;
舉幾個例子,7.0 及以後的 跨程序訪問檔案都需要通過 FileProvider來實現,
a、7.0的 apk 更新功能: apk安裝程序需要訪問你的app外部儲存中私有目錄下的apk檔案
File apkFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_sample.apk");
Uri apkUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID+".myprovider", apkFile);
Intent installIntent = new Intent(Intent.ACTION_VIEW);
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive");
startActivity(installIntent);
b、呼叫系統拍照,並儲存到外部儲存目錄中: 相機程序 需要將圖片資料寫入到 你的app外部儲存中私有目錄下 的 檔案中
String filePath = Environment.getExternalStorageDirectory() + "/images/"+System.currentTimeMillis()+".jpg";
File outputFile = new File(filePath);
if (!outputFile.getParentFile().exists()) {
outputFile.getParentFile().mkdir();
}
Uri contentUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID + ".myprovider", outputFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
startActivityForResult(intent, REQUEST_TAKE_PICTURE);