來自 https://developer.android.google.cn/preview/privacy/storage
Android 11 中的存儲機制更新
Android 11 進一步增強了平臺功能,為外部存儲設備上的應用和用戶數據提供了更好的保護。預覽版引入了多項去年在 Android 開發者峰會上宣布的增強功能,例如可主動選擇啟用的媒體原始文件路徑訪問機制、面向媒體的批量修改操作,以及存儲訪問框架的界面更新。
為了幫助開發者輕松過渡到使用分區存儲,該平臺為開發者引入了進一步的改進。如需詳細了解如何根據應用的用例遷移應用以使用分區存儲,請參閱本頁的分區存儲部分、Android 存儲用例和最佳做法指南,以及標題為 Android 存儲常見問題解答的媒體文章。
我們一如既往地誠邀您提供反饋,幫助我們完善下一版 Android。請使用問題跟蹤器向我們發送反饋意見。
強制執行分區存儲
為了給開發者更多時間進行測試,以 Android 10(API 級別 29)為目標平臺的應用仍可請求 requestLegacyExternalStorage
屬性。應用可以利用此標記暫時停用與分區存儲相關的變更,例如授予對不同目錄和不同類型的媒體文件的訪問權限。當您將應用更新為以 Android 11 為目標平臺后,系統會忽略 requestLegacyExternalStorage
標記。
保持與 Android 10 的兼容性
如果應用在 Android 10 設備上運行時選擇退出分區存儲,建議您繼續在應用的清單文件中將 requestLegacyExternalStorage
設為 true
。這樣,應用就可以在運行 Android 10 的設備上繼續按預期運行。
將數據遷移到使用分區存儲時可見的目錄
如果您的應用使用舊版存儲模型且之前以 Android 10 或更低版本為目標平臺,您可能會將數據存儲到啟用分區存儲模型后您的應用無法訪問的目錄中。在以 Android 11 為目標平臺之前,請將數據遷移到與分區存儲兼容的目錄。在大多數情況下,您可以將數據遷移到您的應用專用目錄。
如果您有需要遷移的數據,當用戶升級到以 Android 11 為目標平臺的新版應用時,可以保留舊版存儲模型。這樣,用戶就可以保留對您的應用之前用于保存數據的目錄中存儲的應用數據的訪問權限。如需啟用舊版存儲模型以進行升級,請在應用的清單中將 preserveLegacyExternalStorage
屬性設為 true
。
注意:大多數應用都不需要使用 preserveLegacyExternalStorage
。此標記僅適用于這樣一種情況:您將應用數據遷移到了與分區存儲兼容的位置,并且希望用戶在更新您的應用時保留對數據的訪問權限。使用此標記會導致更難以測試分區存儲對您應用的用戶有何影響,因為當用戶更新您的應用時,它會繼續使用舊版存儲模型。
如果您使用 preserveLegacyExternalStorage
,舊版存儲模型只在用戶卸載您的應用之前保持有效。如果用戶在搭載 Android 11 的設備上安裝或重新安裝您的應用,那么無論 preserveLegacyExternalStorage
的值是什么,您的應用都無法停用分區存儲模型。
測試分區存儲
如需在您的應用中啟用分區存儲,而不考慮應用的目標 SDK 版本和清單標記值,請啟用以下應用兼容性標記:
-
DEFAULT_SCOPED_STORAGE
(默認情況下,對所有應用處于啟用狀態) -
FORCE_ENABLE_SCOPED_STORAGE
(默認情況下,對所有應用處于停用狀態)
如需停用分區存儲而改用舊版存儲模型,請取消設置這兩個標記。
管理設備存儲空間
在 Android 11 上,使用分區存儲模型的應用只能訪問自身的應用專用緩存文件。如果您的應用需要管理設備存儲空間,請執行以下操作:
通過調用
ACTION_MANAGE_STORAGE
intent 操作檢查可用空間。-
如果設備上的可用空間不足,請提示用戶同意讓您的應用清除所有緩存。為此,請調用
ACTION_CLEAR_APP_CACHE
intent 操作。注意:
ACTION_CLEAR_APP_CACHE
intent 操作會嚴重影響設備的電池續航時間,并且可能會從設備上移除大量的文件。
外部存儲設備上的應用專用目錄
在 Android 11 上,應用無法在外部存儲設備上創建自己的應用專用目錄。如需訪問系統為您的應用提供的目錄,請調用 getExternalFilesDirs()
。
媒體文件訪問權限
為了在保證用戶隱私的同時可以更輕松地訪問媒體,Android 11 增加了以下功能。
執行批量操作
為實現各種設備之間的一致性并增加用戶便利性,Android 11 向 MediaStore
API 中添加了多種方法。對于希望簡化特定媒體文件更改流程(例如在原位置編輯照片)的應用而言,這些方法尤為有用。
添加的方法如下:
<dl style="box-sizing: inherit; margin: 0px; padding: 0px;">
<dt style="box-sizing: inherit; font: 700 16px/24px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;">createWriteRequest()
</dt>
<dd style="box-sizing: inherit; margin: 16px 0px; padding: 0px 0px 0px 40px;">用戶向應用授予對指定媒體文件組的寫入訪問權限的請求。</dd>
<dt style="box-sizing: inherit; font: 700 16px/24px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;">createFavoriteRequest()
</dt>
<dd style="box-sizing: inherit; margin: 16px 0px; padding: 0px 0px 0px 40px;">用戶將設備上指定的媒體文件標記為“收藏”的請求。對該文件具有讀取訪問權限的任何應用都可以看到用戶已將該文件標記為“收藏”。</dd>
<dt style="box-sizing: inherit; font: 700 16px/24px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;">createTrashRequest()
</dt>
<dd style="box-sizing: inherit; margin: 16px 0px; padding: 0px 0px 0px 40px;">
用戶將指定的媒體文件放入設備垃圾箱的請求。垃圾箱中的內容會在系統定義的時間段后被永久刪除。
注意:如果您的應用是設備 OEM 的預安裝圖庫應用,您可以將文件放入垃圾箱而不顯示對話框。如需執行該操作,請直接將 IS_TRASHED
設置為 1
。</dd>
<dt style="box-sizing: inherit; font: 700 16px/24px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;">createDeleteRequest()
</dt>
<dd style="box-sizing: inherit; margin: 16px 0px; padding: 0px 0px 0px 40px;">
用戶立即永久刪除指定的媒體文件(而不是先將其放入垃圾箱)的請求。
</dd>
</dl>
系統在調用以上任何一個方法后,會構建一個 PendingIntent
對象。應用調用此 intent 后,用戶會看到一個對話框,請求用戶同意應用更新或刪除指定的媒體文件。
例如,以下是構建 createWriteRequest()
調用的方法:
<devsite-selector scope="auto" active="kotlin" ready="" style="box-sizing: inherit; pointer-events: auto; visibility: visible; background: rgb(255, 255, 255); border: 1px solid rgb(232, 234, 237); display: block; font: 14px/20px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;"><devsite-tabs role="tablist" connected="" style="box-sizing: inherit; display: flex; -webkit-box-flex: 1; flex-grow: 1; height: 48px; max-width: none; position: relative; border-bottom: 1px solid rgb(232, 234, 237);"><tab role="tab" aria-selected="true" aria-controls="tabpanel-kotlin" tab="kotlin" id="kotlin" active="" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">KOTLIN</tab><tab role="tab" aria-selected="false" aria-controls="tabpanel-java" tab="java" id="java" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">JAVA</tab></devsite-tabs> <devsite-code style="box-sizing: inherit; clear: both; display: block; margin: 0px -23px; overflow: hidden; position: relative; direction: ltr !important;"><pre class="lang-kotlin" translate="no" dir="ltr" is-upgraded="" style="box-sizing: inherit; background: rgb(241, 243, 244); color: rgb(55, 71, 79); font: 14px/20px "Roboto Mono", monospace; padding: 24px 24px 24px 23px; direction: ltr !important; text-align: left !important; margin: 0px; overflow-x: auto; position: relative;">val urisToModify = /* A collection of content URIs to modify. */ val editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify) // Launch a system prompt requesting user permission for the operation. startIntentSenderForResult(editPendingIntent.intentSender, <var style="box-sizing: inherit; color: rgb(236, 64, 122); -webkit-font-smoothing: auto; font-weight: 700;">EDIT_REQUEST_CODE</var>, null, 0, 0, 0) </pre></devsite-code></devsite-selector>
評估用戶的響應,然后繼續操作,或者在用戶不同意時向用戶說明您的應用為何需要獲取權限:
<devsite-selector scope="auto" active="kotlin" ready="" style="box-sizing: inherit; pointer-events: auto; visibility: visible; background: rgb(255, 255, 255); border: 1px solid rgb(232, 234, 237); display: block; font: 14px/20px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;"><devsite-tabs role="tablist" connected="" style="box-sizing: inherit; display: flex; -webkit-box-flex: 1; flex-grow: 1; height: 48px; max-width: none; position: relative; border-bottom: 1px solid rgb(232, 234, 237);"><tab role="tab" aria-selected="true" aria-controls="tabpanel-kotlin" tab="kotlin" id="kotlin" active="" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">KOTLIN</tab><tab role="tab" aria-selected="false" aria-controls="tabpanel-java" tab="java" id="java" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">JAVA</tab></devsite-tabs> <devsite-code style="box-sizing: inherit; clear: both; display: block; margin: 0px -23px; overflow: hidden; position: relative; direction: ltr !important;"><pre class="lang-kotlin" translate="no" dir="ltr" is-upgraded="" style="box-sizing: inherit; background: rgb(241, 243, 244); color: rgb(55, 71, 79); font: 14px/20px "Roboto Mono", monospace; padding: 24px 24px 24px 23px; direction: ltr !important; text-align: left !important; margin: 0px; overflow-x: auto; position: relative;">override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { ... when (requestCode) { <var style="box-sizing: inherit; color: rgb(236, 64, 122); -webkit-font-smoothing: auto; font-weight: 700;">EDIT_REQUEST_CODE</var> -> if (resultCode == Activity.RESULT_OK) { /* Edit request granted; proceed. / } else { / Edit request not granted; explain to the user. */ } } } </pre></devsite-code></devsite-selector>
您可以對 createFavoriteRequest()
、createTrashRequest()
和 createDeleteRequest()
使用相同的通用模式。
使用直接文件路徑和原生庫訪問文件
為了幫助您的應用更順暢地使用第三方媒體庫,Android 11 允許您使用除 MediaStore
API 之外的 API 訪問共享存儲空間中的媒體文件。不過,您也可以轉而選擇使用以下任一 API 直接訪問媒體文件:
-
File
API。 - 原生庫,例如
fopen()
。
如果您的應用沒有任何存儲權限,您可以使用直接文件路徑訪問歸因于您的應用的媒體文件。如果您的應用具有 READ_EXTERNAL_STORAGE
權限,則可以使用直接文件路徑訪問所有媒體文件,無論這些文件是否歸因于您的應用。
如果您直接訪問媒體文件,建議您在應用的清單文件中將 requestLegacyExternalStorage
設置為 true
以停用分區存儲。這樣,您的應用就可以在搭載 Android 10 的設備上正常工作。
性能
當您使用直接文件路徑依序讀取媒體文件時,其性能與 MediaStore
API 相當。
但是,當您使用直接文件路徑隨機讀取和寫入媒體文件時,進程的速度可能最多會慢一倍。在此類情況下,我們建議您改為使用 MediaStore
API。
媒體庫中的可用值
當您訪問現有媒體文件時,您可以使用您的邏輯中 DATA
列的值。這是因為,此值包含有效的文件路徑。但是,不要假設文件始終可用。請準備好處理可能發生的任何基于文件的 I/O 錯誤。
另一方面,如需創建或更新媒體文件,請勿使用 DATA
列的值。請改用 DISPLAY_NAME
和 RELATIVE_PATH
列的值。
訪問其他應用中的數據
為了保護用戶隱私,Android 11 進一步限制您的應用訪問其他應用的私有目錄。
訪問內部存儲設備上的數據目錄
<devsite-selector scope="auto" active="變更詳情" ready="" style="box-sizing: inherit; pointer-events: auto; visibility: visible; background: rgb(255, 255, 255); border: 1px solid rgb(232, 234, 237); display: block; font: 14px/20px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;"><devsite-tabs role="tablist" connected="" style="box-sizing: inherit; display: flex; -webkit-box-flex: 1; flex-grow: 1; height: 48px; max-width: none; position: relative; border-bottom: 1px solid rgb(232, 234, 237);"><tab role="tab" aria-selected="true" aria-controls="tabpanel-變更詳情" tab="變更詳情" id="change-details" active="" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">變更詳情</tab><tab role="tab" aria-selected="false" aria-controls="tabpanel-如何切換" tab="如何切換" id="how-to-toggle" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">如何切換</tab></devsite-tabs>
變更名稱:APP_DATA_DIRECTORY_ISOLATION
變更 ID:143937733
</devsite-selector>
Android 9(API 級別 28)開始限制哪些應用可使其內部存儲設備上數據目錄中的文件可由其他應用進行全局訪問。以 Android 9 或更高版本為目標平臺的應用不能使其數據目錄中的文件全局可訪問。
Android 11 在此限制的基礎上進行了擴展。如果您的應用以 Android 11 為目標平臺,則不能訪問其他任何應用的數據目錄中的文件,即使其他應用以 Android 8.1(API 級別 27)或更低版本為目標平臺且已使其數據目錄中的文件全局可讀也是如此。
訪問外部存儲設備上的應用專用目錄
在 Android 11 上,應用無法再訪問外部存儲設備中的任何其他應用的專用于特定應用的目錄中的文件。
文檔訪問限制
為讓開發者有時間進行測試,以下與存儲訪問框架 (SAF) 相關的變更只有在應用以 Android 11 為目標平臺時才會生效。
訪問目錄
您無法再使用 ACTION_OPEN_DOCUMENT_TREE
intent 操作請求訪問以下目錄:
- 內部存儲卷的根目錄。
- 設備制造商認為可靠的各個 SD 卡卷的根目錄,無論該卡是模擬卡還是可移除的卡。可靠的卷是指應用在大多數情況下可以成功訪問的卷。
-
Download
目錄。
訪問文件
您無法再使用 ACTION_OPEN_DOCUMENT_TREE
或 ACTION_OPEN_DOCUMENT
intent 操作請求用戶從以下目錄中選擇單獨的文件:
-
Android/data/
目錄及其所有子目錄。 -
Android/obb/
目錄及其所有子目錄。
測試變更
如需測試此行為更改,請執行以下操作:
- 通過
ACTION_OPEN_DOCUMENT
操作調用 intent。檢查Android/data/
和Android/obb/
目錄是否均不顯示。 - 執行以下某項操作:
- 啟用
RESTRICT_STORAGE_ACCESS_FRAMEWORK
應用兼容性標記。 - 以 Android 11 為目標平臺。
- 啟用
- 通過
ACTION_OPEN_DOCUMENT_TREE
操作調用 intent。檢查Download
目錄是否已顯示,以及與目錄關聯的操作按鈕是否呈灰顯狀態。
權限
Android 11 引入了與存儲權限相關的以下變更。
以任何版本為目標平臺
[圖片上傳失敗...(image-f14c5a-1598409913523)]
<figcaption style="box-sizing: inherit; font-size: 14px; margin-top: -4px;">圖 1. 應用使用分區存儲并請求 READ_EXTERNAL_STORAGE
權限時顯示的對話框。</figcaption>
不管應用的目標 SDK 版本是什么,以下變更均會在 Android 11 中生效:
存儲運行時權限已重命名為文件和媒體。
-
如果您的應用未停用分區存儲并且請求
READ_EXTERNAL_STORAGE
權限,用戶會看到不同于 Android 10 的對話框。該對話框表明您的應用正在請求訪問照片和媒體,如圖 1 所示。用戶可以在系統設置中查看哪些應用具有
READ_EXTERNAL_STORAGE
權限。在設置 > 隱私 > 權限管理器 > 文件和媒體頁面上,具有該權限的每個應用都列在允許存儲所有文件下。注意:如果您的應用以 Android 11 為目標平臺,請記住,對“所有文件”的這種訪問權限是只讀訪問權限。如需使用此應用讀取和寫入共享的存儲空間中的所有文件,需要具有所有文件訪問權限。
以 Android 11 為目標平臺
如果應用以 Android 11 為目標平臺,那么 WRITE_EXTERNAL_STORAGE
權限和 WRITE_MEDIA_STORAGE
特許權限將不再提供任何其他訪問權限。
請注意,在搭載 Android 10(API 級別 29)或更高版本的設備上,您的應用可以提供明確定義的媒體集合,例如 MediaStore.Downloads
,而無需請求任何存儲相關權限。詳細了解如何在處理應用中的媒體文件時僅請求必要的權限。
所有文件訪問權限
絕大多數需要共享存儲空間訪問權限的應用都可以遵循分區存儲最佳做法,例如存儲訪問框架或 MediaStore API。但是,某些應用的核心用例需要廣泛訪問設備上的文件,但無法采用注重隱私保護的存儲最佳做法高效地完成這些操作。
例如,防病毒應用的主要用例可能需要定期掃描不同目錄中的許多文件。如果此掃描需要反復的用戶交互,讓其使用系統文件選擇器選擇目錄,可能就會帶來糟糕的用戶體驗。其他用例(如文件管理器應用、備份和恢復應用以及文檔管理應用)可能也需要考慮類似情況。
應用可通過執行以下操作,向用戶請求名為“所有文件訪問權限”的特殊應用訪問權限:
- 在清單中聲明
MANAGE_EXTERNAL_STORAGE
權限。 - 使用
ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
intent 操作將用戶引導至一個系統設置頁面,在該頁面上,用戶可以為您的應用啟用以下選項:授予所有文件的管理權限。
如需確定您的應用是否已獲得 MANAGE_EXTERNAL_STORAGE
權限,請調用 Environment.isExternalStorageManager()
。
MANAGE_EXTERNAL_STORAGE
權限會授予以下權限:
-
對共享存儲空間中的所有文件的讀寫訪問權限。
注意:
/sdcard/Android/media
? 目錄是共享存儲空間的一部分。 對
MediaStore.Files
表的內容的訪問權限。對 USB On-The-Go (OTG) 驅動器和 SD 卡的根目錄的訪問權限。
-
除
/Android/data/
、/sdcard/Android
和/sdcard/Android
的大多數子目錄外,對所有內部存儲目錄?的寫入權限。此寫入權限包括文件路徑訪問權限。獲得此權限的應用仍然無法訪問屬于其他應用的應用專用目錄,因為這些目錄在存儲卷上顯示為
Android/data/
的子目錄。
當應用具有 MANAGE_EXTERNAL_STORAGE
權限時,它可以使用 MediaStore
API 或文件路徑訪問這些額外的文件和目錄。但是,當您使用存儲訪問框架時,只有在您不具備 MANAGE_EXTERNAL_STORAGE
權限也能訪問文件或目錄的情況下才能訪問文件或目錄。
為測試啟用
如需了解“所有文件訪問權限”這項權限對您的應用有何影響,您可以為了測試目的啟用該權限。為此,請在連接到測試設備的計算機上運行以下命令:
<devsite-code style="box-sizing: inherit; clear: both; display: block; margin: 16px 0px; overflow: hidden; position: relative; direction: ltr !important;"><pre class="none devsite-terminal" translate="no" dir="ltr" is-upgraded="" style="box-sizing: inherit; background: rgb(241, 243, 244); color: rgb(55, 71, 79); font: 14px/20px "Roboto Mono", monospace; padding: 24px; direction: ltr !important; text-align: left !important; margin: 0px; overflow-x: auto; position: relative;">adb shell appops set --uid <var translate="no" style="box-sizing: inherit; color: rgb(236, 64, 122); -webkit-font-smoothing: auto; font-weight: 700;">PACKAGE_NAME</var> MANAGE_EXTERNAL_STORAGE allow
</pre></devsite-code>
Google Play 通知 [圖片上傳失敗...(image-48e432-1598409913522)]
此部分為在 Google Play 上發布應用的開發者提供通知。
為了限制對共享存儲的廣泛訪問,Google Play 商店已更新其政策,用來評估以 Android 11 為目標平臺且通過 MANAGE_EXTERNAL_STORAGE
權限請求“所有文件訪問權限”的應用。
僅當您的應用無法有效利用更有利于保護隱私的 API(如存儲訪問框架或 Media Store API)時,您才能請求 MANAGE_EXTERNAL_STORAGE
權限。此外,應用對此權限的使用必須在允許的使用情形范圍內,并且必須與應用的核心功能直接相關。如果您的應用包含與以下示例類似的用例,很可能允許您的應用請求 MANAGE_EXTERNAL_STORAGE
權限:
- 文件管理器
- 備份和恢復
- 防病毒應用
- 文檔管理應用
出于新型冠狀病毒肺炎 (COVID-19) 方面的考慮,在 2021 年初之前,以 Android 11 為目標平臺且需要 MANAGE_EXTERNAL_STORAGE
權限的應用無法上傳到 Google Play。這包括新應用以及現有應用的更新。如需了解詳情,請閱讀政策幫助中心內更新后的政策。
注意:只有在您的應用以 Android 11 為目標平臺且請求 MANAGE_EXTERNAL_STORAGE
權限時,此上傳限制才會對其產生影響。
目前,如果您認為自己的應用需要管理外部存儲權限,建議您暫時不要將目標 SDK 更新為 Android 11。如果您的應用以 Android 10 為目標平臺,不妨考慮使用 requestLegacyExternalStorage
標記。
<devsite-page-rating position="footer" selected-rating="0" hover-rating-star="0" style="box-sizing: inherit; display: block; border-top: 1px solid rgb(218, 220, 224); margin: 16px -40px -40px; padding: 31px 40px 40px; text-align: center; color: rgb(32, 33, 36); font-family: Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">此頁內容對您有幫助嗎?</devsite-page-rating>