日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

App磁盘沙盒工具实践

發布時間:2023/12/14 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 App磁盘沙盒工具实践 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄介紹

  • 01.磁盤沙盒的概述
    • 1.1 項目背景說明
    • 1.2 沙盒作用
    • 1.3 設計目標
  • 02.Android存儲概念
    • 2.1 存儲劃分介紹
    • 2.2 機身內部存儲
    • 2.3 機身外部存儲
    • 2.4 SD卡外部存儲
    • 2.5 總結和梳理下
  • 03.方案基礎設計
    • 3.1 整體架構圖
    • 3.2 UML設計圖
    • 3.3 關鍵流程圖
    • 3.4 接口設計圖
    • 3.5 模塊間依賴關系
  • 04.一些技術要點說明
    • 4.1 使用隊列管理Fragment棧
    • 4.2 File文件列表
    • 4.3 不同版本訪問權限
    • 4.4 訪問文件操作
    • 4.5 10和11權限說明
    • 4.6 分享文件給第三方
    • 4.7 打開圖片資源
    • 4.8 為何需要FileProvider
    • 4.9 跨進程IPC通信
  • 05.其他設計實踐說明
    • 5.1 性能設計
    • 5.2 穩定性設計
    • 5.3 debug依賴設計

01.磁盤沙盒的概述

1.1 項目背景說明

  • app展示在數據量多且刷新頻繁的情況下,為提升用戶體驗,通常會對上次已有數據做內存緩存或磁盤緩存,以達到快速展示數據的目的。緩存的數據變化是否正確、緩存是否起到對應作用是QA需要重點測試的對象。
  • android緩存路徑查看方法有哪些呢?將手機打開開發者模式并連接電腦,在pc控制臺輸入cd /data/data/目錄,使用adb主要是方便測試(刪除,查看,導出都比較麻煩)。
  • 如何簡單快速,傻瓜式的查看緩存文件,操作緩存文件,那么該項目小工具就非常有必要呢!采用可視化界面讀取緩存數據,方便操作,直觀也簡單。

1.2 沙盒作用

  • 可以通過該工具查看緩存文件
    • 快速查看data/data/包名目錄下的緩存文件。
    • 快速查看/sdcard/Android/data/包名下存儲文件。
  • 對緩存文件處理
    • 支持查看file文件列表數據,打開緩存文件查看數據詳情。還可以刪除緩存對應的文件或者文件夾,并且友好支持分享到外部。
    • 能夠查看緩存文件修改的信息,修改的時間,緩存文件的大小,獲取文件的路徑等等。都是在可視化界面上處理。

1.3 設計目標

  • 可視化界面展示
  • 多種處理文件操作
    • 針對file文件夾,或者file文件,長按可以出現彈窗,讓測試選擇是否刪除文件。
    • 點擊file文件夾,則拿到對應的文件列表,然后展示。點擊file直到是具體文件(文本,圖片,db,json等非file文件夾)跳轉詳情。
  • 一鍵接入該工具
    • FileExplorerActivity.startActivity(MainActivity.this);
    • 開源項目地址:https://github.com/yangchong211/YCAndroidTool

02.Android存儲基本概念

2.1 存儲劃分介紹

  • 存儲劃分介紹
    • 手機空間存儲劃分為兩部分:1、機身存儲;2、SD卡外部存儲
    • 機身存儲劃分為兩部分:1、內部存儲;2、外部存儲
  • 機身內部存儲
    • 放到data/data目錄下的緩存文件,一般使用adb無法查看該路徑文件,私有的。程序卸載后,該目錄也會被刪除。
  • 機身外部存儲
    • 放到/storage/emulated/0/目錄下的文件,有共享目錄,還有App外部私有目錄,還有其他目錄。App卸載的時候,相應的app創建的文件也會被刪除。
  • SD卡外部存儲
    • 放到sd庫中目錄下文件,外部開放的文件,可以查看。

2.2 機身內部存儲

  • 想一下平時使用的持久化方案:這些文件都是默認放在內部存儲里。
    • SharedPreferences---->適用于存儲小文件
    • 數據庫---->存儲結構比較復雜的大文件
  • 如果包名為:com.yc.helper,則對應的內部存儲目錄為:/data/data/com.yc.helper/
    • 第一個"/“表示根目錄,其后每個”/"表示目錄分割符。內部存儲里給每個應用按照其包名各自劃分了目錄
    • 每個App的內部存儲空間僅允許自己訪問(除非有更高的權限,如root),程序卸載后,該目錄也會被刪除。
  • 機身內部存儲一般存儲那些文件呢?大概有以下這些
    • cache–>存放緩存文件
    • code_cache–>存放運行時代碼優化等產生的緩存
    • databases–>存放數據庫文件
    • files–>存放一般文件
    • lib–>存放App依賴的so庫 是軟鏈接,指向/data/app/ 某個子目錄下
    • shared_prefs–>存放 SharedPreferences 文件
  • 那么怎么通過代碼訪問到這些路徑的文件呢?代碼如下所示context.getCacheDir().getAbsolutePath() context.getCodeCacheDir().getAbsolutePath() //databases 直接通過getDatabasePath(name)獲取 context.getFilesDir().getAbsolutePath() //lib,暫時還不知道怎么獲取該路徑 //shared_prefs 直接通過SharedPreferences獲取

2.3 機身外部存儲

  • 存放位置,主要有那些?如下所示,根目錄下幾個需要關注的目錄:
    • /data/ 這個是前面說的私有文件
    • /sdcard/ /sdcard/是軟鏈接,指向/storage/self/primary
    • /storage/ /storage/self/primary/是軟鏈接,指向/storage/emulated/0/
  • 也就是說/sdcard/、/storage/self/primary/ 真正指向的是/storage/emulated/0/
    • 下面這個是用adb查看 /storage/emulated/0 路徑資源
    a51x:/storage $ ls emulated self a51x:/storage $ cd emulated/ a51x:/storage/emulated $ ls ls: .: Permission denied 1|a51x:/storage/emulated $ cd 0 a51x:/storage/emulated/0 $ ls //省略 /storage/emulated/0 下的文件
  • 然后來看下 /storage/emulated/0/ 存儲的資源有哪些?如下,分為三部分:
  • 第一種:共享存儲空間
    • 也就是所有App共享的部分,比如相冊、音樂、鈴聲、文檔等:
    • DCIM/ 和 Pictures/–>存儲圖片
    • DCIM/、Movies/ 和 Pictures–>存儲視頻
    • Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/–>存儲音頻文件
    • Download/–>下載的文件
    • Documents–>存儲如.pdf類型等文件
  • 第二種:App外部私有目錄
    • Android/data/—>存儲各個App的外部私有目錄。
    • 與內部存儲類似,命名方式是:Android/data/xx------>xx指應用的包名。如:/sdcard/Android/data/com.yc.helper
  • 第三種:其它目錄
    • 比如各個App在/sdcard/目錄下創建的目錄,如支付寶創建的目錄:alipay/,高德創建的目錄:amap/,騰訊創建的目錄:com.tencent.xx/等。
  • 那么怎么通過代碼訪問到這些路徑的文件呢?代碼如下所示
    • 第一種:通過ContentProvider訪問,共享存儲空間中的圖片,視頻,音頻,文檔等資源
    • 第二種:可以看出再/sdcard/Android/data/目錄下生成了com.yc.helper/目錄,該目錄下有兩個子目錄分別是:files/、cache/。當然也可以選擇創建其它目錄。App卸載的時候,兩者都會被清除。
    context.getExternalCacheDir().getAbsolutePath(); context.getExternalFilesDir(null).getAbsolutePath();
    • 第三種:只要拿到根目錄,就可以遍歷尋找其它子目錄/文件。

2.4 SD卡外部存儲

  • 當給設備插入SD卡后,查看其目錄:/sdcard/ —> 依然指向/storage/self/primary,繼續來看/storage/,可以看出,多了sdcard1,軟鏈接指向了/storage/77E4-07E7/。
  • 訪問方式,跟獲取外部存儲-App私有目錄方式一樣。File[] fileList = context.getExternalFilesDirs(null);
    • 返回File對象數組,當有多個外部存儲時候,存儲在數組里。返回的數組有兩個元素,一個是自帶外部存儲存儲,另一個是插入的SD卡。

2.5 總結和梳理下

  • Android存儲有三種:手機內部存儲、手機自帶外部存儲、SD卡擴展外部存儲等。
  • 內部存儲與外部存儲里的App私有目錄
    • 相同點:
      • 1、屬于App專屬,App自身訪問兩者無需任何權限。
      • 2、App卸載后,兩者皆被刪除。
      • 3、兩者目錄下增加的文件最終會被統計到"設置->存儲和緩存"里。
    • 不同點:
      • /data/data/com.yc.helper/ 位于內部存儲,一般用于存儲容量較小的,私密性較強的文件。
      • 而/sdcard/Android/data/com.yc.helper/ 位于外部存儲,作為App私有目錄,一般用于存儲容量較大的文件,即使刪除了也不影響App正常功能。
  • 在設置里的"存儲與緩存"項,有清除數據和清除緩存,兩者有何區別?
    • 當點擊"清除數據" 時:
      • 內部存儲/data/data/com.yc.helper/cache/、 /data/data/com.yc.helper/code_cache/目錄會被清空
      • 外部存儲/sdcard/Android/data/com.yc.helper/cache/ 會被清空
    • 當點擊"清除緩存" 時:
      • 內部存儲/data/data/com.yc.helper/下除了lib/,其余子目錄皆被刪除
      • 外部存儲/sdcard/Android/data/com.yc.helper/被清空
      • 這種情況,相當于刪除用戶sp,數據庫文件,相當于重置了app

04.一些技術要點說明

4.1 使用隊列管理Fragment棧

  • 該磁盤沙盒file工具頁面的組成部分是這樣的
    • FileExplorerActivity + FileExplorerFragment(多個,file列表頁面) + TextDetailFragment(一個,file詳情頁面)
  • 針對磁盤file文件列表FileExplorerFragment頁面,點擊file文件item
    • 如果是文件夾則是繼續打開跳轉到file文件列表FileExplorerFragment頁面,否則跳轉到文件詳情頁面
  • 處理任務棧返回邏輯。舉個例子現在列表FileExplorerFragment當作B,文件詳情頁面當作C,宿主Activity當作A。也就是說,點擊返回鍵,依次關閉了fragment直到沒有,回到宿主activity頁面。再次點擊返回鍵,則關閉activity!
    • 可能存在的任務棧是:打開A1->打開B1->打開C1
    • 那么點擊返回鍵按鈕,返回關閉的順序則是:關閉C1->關閉B1->關閉A1
  • Fragment回退棧處理方式
    • 第一種方案:創建一個棧(先進后出),打開一個FileExplorerFragment列表頁面(push一個fragment對象到隊列中),關閉一個列表頁面(remove最上面那個fragment對象,然后調用FragmentManager中popBackStack操作關閉fragment)
    • 第二種方案:通過fragmentManager獲取所有fragment對象,返回一個list,當點擊返回的時候,調用popBackStack移除最上面一個
  • 具體處理該場景中回退邏輯
    • 首先定義一個雙端隊列ArrayDeque,用來存儲和移除元素。內部使用數組實現,可以當作棧來使用,功能非常強大。
    • 當開啟一個fragment頁面的時候,調用push(相當于addFirst在棧頂添加元素)來存儲fragment對象。代碼如下所示public void showContent(Class<? extends Fragment> target, Bundle bundle) {try {Fragment fragment = target.newInstance();if (bundle != null) {fragment.setArguments(bundle);}FragmentManager fm = getSupportFragmentManager();FragmentTransaction fragmentTransaction = fm.beginTransaction();fragmentTransaction.add(android.R.id.content, fragment);//push等同于addFirst,添加到第一個mFragments.push(fragment);//add等同于addLast,添加到最后//mFragments.add(fragment);fragmentTransaction.addToBackStack("");//將fragment提交到任務棧中fragmentTransaction.commit();} catch (InstantiationException exception) {FileExplorerUtils.logError(TAG + exception.toString());} catch (IllegalAccessException exception) {FileExplorerUtils.logError(TAG + exception.toString());} }
    • 當關閉一個fragment頁面的時候,調用removeFirst(相當于彈出棧頂的元素)移除fragment對象。代碼如下所示@Override public void onBackPressed() {if (!mFragments.isEmpty()) {Fragment fragment = mFragments.getFirst();if (fragment!=null){//移除最上面的一個mFragments.removeFirst();}super.onBackPressed();//如果fragment棧為空,則直接關閉activityif (mFragments.isEmpty()) {finish();}} else {super.onBackPressed();} }/*** 回退fragment任務棧操作* @param fragment fragment*/ public void doBack(Fragment fragment) {if (mFragments.contains(fragment)) {mFragments.remove(fragment);FragmentManager fm = getSupportFragmentManager();//回退fragment操作fm.popBackStack();if (mFragments.isEmpty()) {//如果fragment棧為空,則直接關閉宿主activityfinish();}} }

4.2 File文件列表

  • 獲取文件列表,主要包括,data/data/包名目錄下的緩存文件。/sdcard/Android/data/包名下存儲文件。/*** 初始化默認文件。注意:加External和不加(默認)的比較* 相同點:1.都可以做app緩存目錄。2.app卸載后,兩個目錄下的數據都會被清空。* 不同點:1.目錄的路徑不同。前者的目錄存在外部SD卡上的。后者的目錄存在app的內部存儲上。* 2.前者的路徑在手機里可以直接看到。后者的路徑需要root以后,用Root Explorer 文件管理器才能看到。** @param context 上下文* @return 列表*/ private List<File> initDefaultRootFileInfos(Context context) {List<File> fileInfos = new ArrayList<>();//第一個是文件父路徑File parentFile = context.getFilesDir().getParentFile();if (parentFile != null) {fileInfos.add(parentFile);}//路徑:/data/user/0/com.yc.lifehelper//第二個是緩存文件路徑File externalCacheDir = context.getExternalCacheDir();if (externalCacheDir != null) {fileInfos.add(externalCacheDir);}//路徑:/storage/emulated/0/Android/data/com.yc.lifehelper/cache//第三個是外部file路徑File externalFilesDir = context.getExternalFilesDir((String) null);if (externalFilesDir != null) {fileInfos.add(externalFilesDir);}//路徑:/storage/emulated/0/Android/data/com.yc.lifehelper/filesreturn fileInfos; }

4.3 不同版本訪問權限

  • Android 6.0 之前訪問方式
    • Android 6.0 之前是無需申請動態權限的,在AndroidManifest.xml 里聲明存儲權限。就可以訪問共享存儲空間、其它目錄下的文件。
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  • Android 6.0 之后的訪問方式
    • Android 6.0 后需要動態申請權限,除了在AndroidManifest.xml 里聲明存儲權限外,還需要在代碼里動態申請。
    //申請權限 if (ContextCompat.checkSelfPermission(mActivity,Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(mActivity,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE); }

4.4 訪問文件操作

  • 權限申請成功后,即可對自帶外部存儲之共享存儲空間和其它目錄進行訪問。分別以共享存儲空間和其它目錄為例,闡述訪問方式:
  • 訪問媒體文件(共享存儲空間)。目的是拿到媒體文件的路徑,有兩種方式獲取路徑:
    • 以圖片為例,假設圖片存儲在/sdcard/Pictures/目錄下。路徑:/storage/emulated/0/Pictures/yc.png,拿到路徑后就可以解析并獲取Bitmap。
    //獲取目錄:/storage/emulated/0/ File rootFile = Environment.getExternalStorageDirectory(); String imagePath = rootFile.getAbsolutePath() + File.separator + Environment.DIRECTORY_PICTURES + File.separator + "yc.png"; Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
    • 通過MediaStore獲取路徑
    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));Bitmap bitmap = BitmapFactory.decodeFile(imagePath);break; }
    • 還有一種不直接通過路徑訪問的方法,通過MediaStore獲取Uri。與直接拿到路徑不同的是,此處拿到的是Uri。圖片的信息封裝在Uri里,通過Uri構造出InputStream,再進行圖片解碼拿到Bitmap
    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()) {//獲取唯一的idlong id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));//通過id構造UriUri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);openUri(uri);break;} }
  • 訪問文檔和其它文件(共享存儲空間)。
    • 直接構造路徑。與媒體文件一樣,可以直接構造路徑訪問。
  • 訪問其它目錄
    • 直接構造路徑。與媒體文件一樣,可以直接構造路徑訪問。
  • 總結一下共同點
    • 訪問目錄/文件可通過如下兩個方法:1、通過路徑訪問。路徑可以直接構造也可以通過MediaStore獲取。 2、通過Uri訪問。Uri可以通過MediaStore或者SAF(存儲訪問框架,通過intent調用startActivity訪問)獲取。

4.5 10和11權限說明

  • Android10權限改變
    • 比如能夠直接在/sdcard/目錄下創建目錄/文件。可以看出/sdcard/目錄下,如淘寶、qq、qq瀏覽器、微博、支付寶等都自己建了目錄。
    • 這么看來,導致目錄結構很亂,而且App卸載后,對應的目錄并沒有刪除,于是就是遺留了很多"垃圾"文件,久而久之不處理,用戶的存儲空間越來越小。
  • 之前文件創建弊端如下
    • 卸載App也不能刪除該目錄下的文件
    • 在設置里"清除數據"或者"清除緩存"并不能刪除該目錄下的文件
    • App可以隨意修改其它目錄下的文件,如修改別的App創建的文件等,不安全
  • 為什么要在/sdcard/目錄下新建app存儲的目錄
    • 此處新建的目錄不會被設置里的App存儲用量統計,讓用戶"看起來"自己的App占用的存儲空間很小。還有就是方便操作文件
  • Android 10.0訪問變更
    • Google在Android 10.0上重拳出擊了。引入Scoped Storage。簡單來說有好幾個版本:作用域存儲、分區存儲、沙盒存儲。分區存儲原理:
    • 1、App訪問自身內部存儲空間、訪問外部存儲空間-App私有目錄不需要任何權限(這個與Android 10.0之前一致)
    • 2、外部存儲空間-共享存儲空間、外部存儲空間-其它目錄 App無法通過路徑直接訪問,不能新建、刪除、修改目錄/文件等
    • 3、外部存儲空間-共享存儲空間、外部存儲空間-其它目錄 需要通過Uri訪問

4.6 分享文件給第三方

  • 這里直接說分享內部文件給第三方,大概的思路如下所示:
    • 第一步:先判斷是否有讀取文件的權限,如果沒有則申請;如果有則進行第二步;
    • 第二步:先把文件轉移到外部存儲文件,為何要這樣操作,主要是解決data/data下目前文件無法直接分享問題,因此需要將目標文件拷貝到外部路徑
    • 第三步:通過intent發送,FileProvider拿到對應路徑的uri,最后調用startActivity進行分享文件。
  • 大概的代碼如下所示if (ContextCompat.checkSelfPermission(mActivity,Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(mActivity,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE); } else {//先把文件轉移到外部存儲文件File srcFile = new File(mFile.getPath());String newFilePath = AppFileUtils.getFileSharePath() + "/fileShare.txt";File destFile = new File(newFilePath);//拷貝文件,將data/data源文件拷貝到新的目標文件路徑下boolean copy = AppFileUtils.copyFile(srcFile, destFile);if (copy) {//分享boolean shareFile = FileShareUtils.shareFile(mActivity, destFile);if (shareFile) {Toast.makeText(getContext(), "文件分享成功", Toast.LENGTH_SHORT).show();} else {Toast.makeText(getContext(), "文件分享失敗", Toast.LENGTH_SHORT).show();}} else {Toast.makeText(getContext(), "文件保存失敗", Toast.LENGTH_SHORT).show();} }

4.7 打開圖片資源

  • 首先判斷文件,是否是圖片資源,如果是圖片資源,則跳轉到打開圖片詳情。目前只是根據文件的后綴名來判斷(對文件名稱以.進行裁剪獲取后綴名)是否是圖片。if (FileExplorerUtils.isImage(fileInfo)) {Bundle bundle = new Bundle();bundle.putSerializable("file_key", fileInfo);showContent(ImageDetailFragment.class, bundle); }
  • 打開圖片跳轉詳情,這里面為了避免打開大圖OOM,因此需要對圖片進行壓縮,目前該工具主要是內存壓縮和尺寸縮放方式。大概的原理如下
    • 例如,我們的原圖是一張 2700 * 1900 像素的照片,加載到內存就需要 19.6M 內存空間,但是,我們需要把它展示在一個列表頁中,組件可展示尺寸為 270 * 190,這時,我們實際上只需要一張原圖的低分辨率的縮略圖即可(與圖片顯示所對應的 UI 控件匹配),那么實際上 270 * 190 像素的圖片,只需要 0.2M 的內存即可。這個采用縮放比壓縮。
    • 加載圖片,先加載到內存,再進行操作嗎,可以如果先加載到內存,好像也不太對,這樣只接占用了 19.6M + 0.2M 2份內存了,而我們想要的是,在原圖不加載到內存中,只接將縮放后的圖片加載到內存中,可以實現嗎?
    • 進行內存壓縮,要將BitmapFactory.Options的inJustDecodeBounds屬性設置為true,解析一次圖片。注意這個地方是核心,這個解析圖片并沒有生成bitmap對象(也就是說沒有為它分配內存控件),而僅僅是拿到它的寬高等屬性。
    • 然后將BitmapFactory.Options連同期望的寬度和高度一起傳遞到到calculateInSampleSize方法中,就可以得到合適的inSampleSize值了。這一步會壓縮圖片。之后再解析一次圖片,使用新獲取到的inSampleSize值,并把inJustDecodeBounds設置為false,就可以得到壓縮后的圖片了。

4.8 為何需要FileProvider

4.8.1 文件共享基礎概念
  • 了解文件共享的基礎知識
    • 提到文件共享,首先想到就是在本地磁盤上存放一個文件,多個應用都可以訪問它,如下:

    • 理想狀態下只要知道了文件的存放路徑,那么各個應用都可以讀寫它。比如相冊里的圖片或者視頻存放目錄:/sdcard/DCIM/、/sdcard/Pictures/ 、/sdcard/Movies/。

  • 文件共享方式是如何理解
    • 一個常見的應用場景:應用A里檢索到一個文件yc.txt,它無法打開,于是想借助其它應用打開,這個時候它需要把待打開的文件路徑告訴其它應用。對應案例就是,把磁盤文件分享到qq。
    • 這就涉及到了進程間通信。Android進程間通信主要手段是Binder,而四大組件的通信也是依靠Binder,因此我們應用間傳遞路徑可以依靠四大組件。
4.8.2 7.0前后對文件處理方式
  • Android 7.0 之前使用,傳遞路徑可以通過UriIntent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); //通過路徑,構造Uri。設置Intent,附帶Uri,然后通過intent跨進程通信 Uri uri = Uri.fromFile(new File(external_filePath)); intent.setData(uri); startActivity(intent);
    • 接收方在收到Intent后,拿出Uri,通過:filePath = uri.getEncodedPath() 拿到發送方發送的原始路徑后,即可讀寫文件。然而此種構造Uri方式在Android7.0(含)之后被禁止了,若是使用則拋出異常,異常是FileUriExposedException。
    • 這種方式缺點如下:第一發送方傳遞的文件路徑接收方完全知曉,一目了然,沒有安全保障;第二發送方傳遞的文件路徑接收方可能沒有讀取權限,導致接收異常。
  • Android 7.0(含)之后如何解決上面兩個缺點問題
    • 對第一個問題:可以將具體路徑替換為另一個字符串,類似以前密碼本的感覺,比如:"/storage/emulated/0/com.yc.app/yc.txt" 替換為"file/yc.txt",這樣接收方收到文件路徑完全不知道原始文件路徑是咋樣的。那么會導致另一個額外的問題:接收方不知道真實路徑,如何讀取文件呢?
    • 對第二個問題既然不確定接收方是否有打開文件權限,那么是否由發送方打開,然后將流傳遞給接收方就可以了呢?
    • Android 7.0(含)之后引入了FileProvider,可以解決上述兩個問題。
4.8.3 FileProvider應用與原理
  • 第一步,定義自定義FileProvider并且注冊清單文件public class ExplorerProvider extends FileProvider {}<!--既然是ContentProvider,那么需要像Activity一樣在AndroidManifest.xml里聲明:--> <!--android:authorities 標識ContentProvider的唯一性,可以自己任意定義,最好是全局唯一的。--> <!--android:name 是指之前定義的FileProvider 子類。--> <!--android:exported="false" 限制其他應用獲取Provider。--> <!--android:grantUriPermissions="true" 授予其它應用訪問Uri權限。--> <!--meta-data 囊括了別名應用表。--> <!--android:name 這個值是固定的,表示要解析file_path--> <!--android:resource 自己定義實現的映射表--> <providerandroid:name="com.yc.toolutils.file.ExplorerProvider"android:authorities="${applicationId}.fileExplorerProvider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_explorer_provider" /> </provider>
  • 第二步,添加路徑映射表
    • 在/res/ 下建立xml 文件夾,然后再創建對應的映射表(xml),最終路徑如下:/res/xml/file_explorer_provider.xml。
    <paths><!--FileProvider需要讀取映射表。--><external-cache-path name="external_cache" path="." /><cache-path name="cache" path="." /><external-path name="external_path" path="." /><files-path name="files_path" path="." /><external-files-path name="external_files_path" path="." /><root-path name="root_path" path="." /> </paths>
  • 第三步,使用ExplorerProvider來跨進程通信交互
    • 如何解決第一個問題,讓接收方看不到具體文件的路徑?如下所示,下面構造后,第三方應用收到此Uri后,并不能從路徑看出我們傳遞的真實路徑,這就解決了第一個問題。
    public static boolean shareFile(Context context, File file) {boolean isShareSuccess;try {if (null != file && file.exists()) {Intent share = new Intent(Intent.ACTION_SEND);//此處可發送多種文件String absolutePath = file.getAbsolutePath();//通過擴展名找到mimeTypeString mimeType = getMimeType(absolutePath);share.setType(mimeType);Uri uri;//判斷7.0以上if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//第二個參數表示要用哪個ContentProvider,這個唯一值在AndroidManifest.xml里定義了//若是沒有定義MyFileProvider,可直接使用FileProvider替代String authority = context.getPackageName() + ".fileExplorerProvider";uri = FileProvider.getUriForFile(context,authority, file);} else {uri = Uri.fromFile(file);}//content://com.yc.lifehelper.fileExplorerProvider/external_path/fileShare.txt//content 作為scheme;//com.yc.lifehelper.fileExplorerProvider 即為我們定義的 authorities,作為host;LogUtils.d("share file uri : " + uri);String encodedPath = uri.getEncodedPath();//external_path/fileShare.txt//如此構造后,第三方應用收到此Uri后,并不能從路徑看出我們傳遞的真實路徑,這就解決了第一個問題://發送方傳遞的文件路徑接收方完全知曉,一目了然,沒有安全保障。LogUtils.d("share file uri encode path : " + encodedPath);share.putExtra(Intent.EXTRA_STREAM, uri);share.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//賦予讀寫權限share.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);Intent intent = Intent.createChooser(share, "分享文件");//交由系統處理context.startActivity(intent);isShareSuccess = true;} else {isShareSuccess = false;}} catch (Exception e) {e.printStackTrace();isShareSuccess = false;}return isShareSuccess; }
    • 如何解決第二個問題,發送方傳遞的文件路徑接收方可能沒有讀取權限,導致接收異常?通過FileProvider.getUriForFile為入口查看源碼,應用間通過IPC機制,最后調用了openFile()方法,而FileProvider重寫了該方法。

4.9 跨進程IPC通信

  • A應用(該demo)通過構造Uri,通過intent調用B(分享到QQ)
    • 應用A將path構造為Uri:應用A在啟動的時候,會掃描AndroidManifest.xml 里的 FileProvider,并讀取映射表構造為一個Map。
    • 還是以/storage/emulated/0/com.yc.lifehelper.fileExplorerProvider/external_path/fileShare.txt 為例,當調用 FileProvider.getUriForFile(xx)時,遍歷Map,找到最匹配條目,最匹配的即為external_file。因此會用external_file 代替原始路徑,最終形成的Uri為:content://com.yc.lifehelper.fileExplorerProvider/external_path/fileShare.txt
  • B應用(QQ)通過Uri構造輸入流,將Uri解析成具體的路徑
    • 應用B通過Uri(A傳遞過來的),解析成具體的file文件。先將Uri分離出external_file/fileShare.txt,然后通過external_file 從Map里找到對應Value 為:/storage/emulated/0/com.yc.lifehelper.fileExplorerProvider/,最后將fileShare.txt拼接,形成的路徑為:/storage/emulated/0/com.yc.lifehelper.fileExplorerProvider/external_path/fileShare.txt
  • 現在來梳理整個流程:
    • 1、應用A使用FileProvider通過Map(映射表)將Path轉為Uri,通過IPC 傳遞給應用B。
    • 2、應用B使用Uri通過IPC獲取應用A的FileProvider。
    • 3、應用A使用FileProvider通過映射表將Uri轉為Path,并構造出文件描述符。
    • 4、應用A將文件描述符返回給應用B,應用B就可以讀取應用A發送的文件了。
  • 整個交互流程圖如下

05.其他設計實踐說明

5.1 性能設計

  • 這個暫無,因為是小工具,主要是在debug環境下依賴使用。代碼邏輯并不復雜,不會影響App的性能。

5.2 穩定性設計

  • 修改文件說明
    • 目前,針對文本文件,比如緩存的json數據,存儲在文本文件中,之前測試說讓該工具支持修改屬性,考慮到修改json比較復雜,因此這里只是實現可以刪除文本文件,或者修改文件名稱的功能。
    • 針對圖片文件,可以打開且進行了圖片壓縮,僅僅支持刪除圖片文件操作。
    • 針對sp存儲的數據,是xml,這里可視化展示sp的數據,目前可以支持修改sp數據,測試童鞋這方便操作簡單,提高某些場景的測試效率。
  • 為何不支持修改json
    • 讀取文本文件,是一行行讀取,修改數據編輯數據麻煩,而且修改完成后對json數據合法性判斷也比較難處理。因此這里暫時不提供修改緩存的json數據,測試如果要看,可以通過分享到外部qq查看文件,或者直接查看,避免臟數據。

5.3 debug依賴設計

  • 建議在debug下使用
    • 在小工具放到debug包名下,依賴使用。或者在gradle依賴的時候區分也可以。如下所示:
    //在app包下依賴 apply from: rootProject.file('buildScript/fileExplorer.gradle')/*** 沙盒file工具配置腳本*/ println('gradle file explorer , init start') if (!isNeedUseExplorer()) {println('gradle file explorer , not need file explorer')return } println('gradle file isNeedUseExplorer = ture')dependencies {// 依賴implementation('com.github.jacoco:runtime:0.0.23-SNAPSHOT') }//過濾,只在debug下使用 def isNeedUseJacoco() {Map<String, String> map = System.getenv()if (map == null) {return false}//拿到編譯后的 BUILD_TYPE 和 CONFIG。具體看 BuildConfig 生成類的代碼boolean hasBuildType = map.containsKey("BUILD_TYPE")boolean hasConfig = map.containsKey("CONFIG")println 'gradle file explorer isNeedUseExplorer hasBuildType =====>' + hasBuildType + ',hasConfig = ' + hasConfigString buildType = "debug"String config = "debug"if (hasBuildType) {buildType = map.get("BUILD_TYPE")}if (hasConfig) {config = map.get("CONFIG")}println 'gradle file explorer isNeedUseExplorer buildType =====>' + buildType + ',config = ' + configif (buildType.toLowerCase() == "debug" && config.toLowerCase() == "debug" && isNotUserFile()) {println('gradle file explorer debug used')return true}println('gradle file explorer not use')//如果是正式包,則不使用沙盒file工具return false }static def isNotUserFile() {//在debug下默認沙盒file工具,如果你在debug下不想使用沙盒file工具,則設置成falsereturn true }

總結

以上是生活随笔為你收集整理的App磁盘沙盒工具实践的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。