Android 10/11 應用分區存儲適配實踐

2021-02-06 鴻洋

作者:椎鋒陷陳

連結:

https://www.jianshu.com/p/af9903069ebe

本文由作者授權發布。

正式進入文章之前,我們必須對當前最新Android系統版本的功能特性和影響應用的行為變更有個大致的了解。

為了讓用戶能更好地管理自己的文件並減少混亂,Android 10 引入了稱為分區存儲的隱私權變更,即以 Android 10及更高版本為目標平臺的應用,在默認情況下,只能看到本應用專有的目錄(/sdcard/Android/data/{package_name}/,使用 getExternalFilesDir() 訪問)以及特定類型的媒體(照片、視頻、音頻等,使用 MediaStore 來訪問)。

如果應用嘗試打開此目錄之外的文件,則會發生錯誤(即使擁有 READ_EXTERNAL_STORAGE 權限)。

開發者在應用完全兼容分區存儲之前,可以通過添加 requestLegacyExternalStorage 清單屬性,暫時選擇停用分區存儲。

https://developer.android.google.cn/reference/android/R.attr#requestLegacyExternalStorage

但當將應用更新為以 Android 11 為目標平臺後,系統會忽略該屬性,即會強制執行分區存儲。因此,在以 Android 11 為目標平臺之前,開發者需將數據遷移到與分區存儲兼容的目錄。

<manifest ... >
  <!-- This attribute is "false" by default on apps targeting
       Android 10 or higher. -->
  <application android:requestLegacyExternalStorage="true" ... >
    ...
  </application>
</manifest>

對於如何遷移,Android開發者平臺提供了以下建議的遷移步驟:

1.檢查應用的工作文件是否位於 /sdcard/ 目錄或其任何子目錄中。
2. 將任何私有應用文件從 /sdcard/ 下的當前位置移至 getExternalFilesDir() 方法所返回的目錄。
3.將任何共享的非媒體文件從 /sdcard/ 下的當前位置移至 Downloads/ 目錄的應用專用子目錄。
4.從 /sdcard/ 目錄中移除應用的舊存儲目錄。

那麼,具體到我們實際項目中該如何實踐呢?

首先,Android使用的文件系統區分為兩個存儲區域:內部存儲空間和外部存儲空間。

我們通過比較兩個選項之間的異同點,來了解它們的特點。

共同點


1.都包括用於存儲持久性文件和緩存數據的兩種目錄。
2.如果用戶卸載應用,系統會移除保存在應用專屬存儲空間中的文件。由於這一特點,我們不應使用此存儲空間保存那些應該獨立於應用之外的內容。
3.不需要任何系統權限即可讀取和寫入這些目錄中的文件(使用分區存儲的應用對自己創建的文件始終擁有讀/寫權限,無論文件是否位於應用的專有目錄內)。
4.當設備的內部存儲空間不足時,Android 可能會刪除緩存數據目錄下的文件以回收空間。因此,請在讀取前檢查緩存文件是否存在。


不同點內部存儲空間

1.系統會阻止其他應用訪問我們應用的內部存儲空間
2.(Android 10及更高版本設備)系統會對這些位置進行加密。
3.這些目錄的空間通常比較小。在寫入之前,應用應查詢設備上的可用空間。
基於以上特點,可以看出內部存儲空間非常適合存儲只有應用本身才能訪問的敏感數據。


外部存儲空間

1.(Android 9或更低版本設備)其他應用可以在具有適當權限的情況下訪問我們應用的外部存儲空間。
2.(Android 10及更高版本設備)啟用分區存儲後,應用將無法訪問屬於其他應用的應用專屬目錄。
3.位於用戶可能能夠移除的物理卷上,因此在嘗試讀取或寫入之前,需要驗證該卷是否可訪問。

我繪製了一張雙對比圖,可以更直觀地看一下:

有了以上的知識儲備,我們就可以著手開始數據遷移前的一些準備了:

1.理清現有項目中使用到文件存儲的業務及其對應的存儲目錄,區分哪些文件是需要保留遷移的,哪些是可以丟棄的。

以一款即時通訊APP為例,聊天記錄中的頭像、圖片、語音、視頻等緩存數據是最重要的,直接影響APP的可用性,必然需要保留和遷移。


而像啟動圖這類與用戶關聯性不強的數據,則可以選擇性丟棄,犧牲掉部分用戶體驗,從而減少遷移的數據量。

2.根據應用業務特點,設計新的文件目錄層次架構


a.將步驟1中理清的存儲目錄根據業務特點,進一步劃分到持久性文件目錄和緩存文件目錄

還是以即時通訊APP為例,為保證聊天記錄中的圖片、語音、視頻等文件可後續不定期查看,需要長期保存,因此這類型的文件需要存儲到持久性文件目錄。而像表情雨(關鍵詞觸發飄落動畫)等資源文件,以壓縮包形式下載後解壓,過程中產生的一些過渡文件,就可以放到緩存文件目錄,交由系統的清除策略管理。

b.《訪問應用專屬文件》一文中提到,為確保系統能正確處理媒體文件,建議開發者使用 DIRECTORY_PICTURES 等 API 常量作為預定義的子目錄名稱。因此我們將第一層作為預留給這些子目錄,並參考這種形式,我們應用本身的數據也使用了自定義的 DIRECTORY_DATA 常量來建立目錄。

c.基於應用本身的用戶體系,需要建立不同用戶的專有目錄,方便進行基於用戶粒度的緩存文件管理;

d.與用戶關聯性不強的資源,放置在公用的目錄,避免重複下載多套資源,浪費流量與存儲空間;

e.在用戶的專屬目錄或公用目錄下,再建立不同業務對應的存儲目錄

以下是我們公司正在開發的,基於以上描述所繪製的項目文件目錄層次架構圖:

一切準備就緒之後,下面就以我提供的Demo為主要參考,開始核心的數據遷移步驟了。
先縱覽一下核心的幾個類:

OldStorageManager:舊版存儲管理器。文件保存在 /sdcard/ 目錄中,需要適配。


ScopedStorageManager:分區存儲管理類。工具類,封裝了【訪問應用專屬內部/外部存儲空間的緩存/持久性文件目錄】、【從舊版存儲位置遷移現有文件】等公共方法,與業務剝離。


TestStorageManager:業務存儲管理類,負責具體業務下的文件存儲和數據遷移,與業務耦合。

示例代碼可以在GitHub上下載。

https://github.com/madchan/MigrateDataDemo

具體實現的步驟如下:


1.檢查 /sdcard/ 目錄中應用的舊版存儲根目錄是否存在

2.列入需要處理的舊版存儲目錄,可能包括:

a.位於/sdcard/的Test目錄下,不需要遷移直接刪除的子目錄
b.位於/sdcard/的Test目錄下,需要保留並遷移的子目錄
c.包含以上目錄的父目錄,該目錄下的子目錄保留並遷移之後,需要刪除該目錄
d.  /sdcard/ 目錄中應用的舊版存儲根目錄,需要移除

3.從舊版存儲位置遷移現有文件,建議將此工作放在應用升級後的重新啟動階段,監聽數據遷移情況並在啟動頁提供遷移進度顯示。

TestStorageManager.kt

class TestStorageManager {

   companion object {

       /** 子目錄-公共 */
       private const val SUB_DIRECTORY_UNIVERSAL = "Universal"
       /** 子目錄-特定用戶 */
       private lateinit var SUB_DIRECTORY_SPECIFIC_USER : String

       @JvmStatic
       fun init(specificUser : String) {
           SUB_DIRECTORY_SPECIFIC_USER = specificUser
       }

       /**
        * 從舊版存儲位置遷移現有文件
        */
       @JvmStatic
       fun migrateExistingFilesFromLegacyStorageDir(listener: ScopedStorageManager.ProgressListener) {
           // 舊版存儲位置已不復存在,不需要處理
           if(!OldStorageManager.getOldStorageRootDir().exists()){
               listener.onFinish()
               return
           }

           // 列入需要遷移的舊版存儲目錄
           var map = linkedMapOf(
                   // 需要保留並遷移的目錄
                   OldStorageManager.getAvatarStorageDir() to getAvatarStorageDir(),
                   OldStorageManager.getMessageThumbnailStorageDir() to getMessageThumbnailStorageDir(),
                   OldStorageManager.getMessageImageStorageDir() to getMessageImageStorageDir(),
                   OldStorageManager.getMessageAudioStorageDir() to getMessageAudioStorageDir(),
                   OldStorageManager.getMessageVideoStorageDir() to getMessageVideoStorageDir(),
                   // 不需要遷移直接刪除的目錄
                   OldStorageManager.getSplashStorageDir() to null
                   )

           // 最後移除應用的舊存儲目錄
           map[OldStorageManager.getOldStorageRootDir()] = null

           ScopedStorageManager.migrateExistingFilesFromLegacyStorageDir(map, listener)
       }

       /**
        * 頭像
        */
       @JvmStatic
       fun getAvatarStorageDir() = ScopedStorageManager.getExternalStorageDir(
           BaseApplication.getContext(), null, "$SUB_DIRECTORY_UNIVERSAL/Avatar")

       /**
        * 消息-縮略圖
        * 包含圖片、視頻等
        */
       @JvmStatic
       fun getMessageThumbnailStorageDir() = ScopedStorageManager.getExternalStorageDir(
           BaseApplication.getContext(), null, "$SUB_DIRECTORY_UNIVERSAL/Message/Thumbnail")

       /**
        * 消息-原圖
        */
       @JvmStatic
       fun getMessageImageStorageDir() = ScopedStorageManager.getExternalStorageDir(
           BaseApplication.getContext(), null, "$SUB_DIRECTORY_SPECIFIC_USER/Message/Image")

       /**
        * 消息-語音
        */
       @JvmStatic
       fun getMessageAudioStorageDir() = ScopedStorageManager.getExternalStorageDir(
           BaseApplication.getContext(), null, "$SUB_DIRECTORY_SPECIFIC_USER/Message/Audio")

       /**
        * 消息-視頻
        */
       @JvmStatic
       fun getMessageVideoStorageDir() = ScopedStorageManager.getExternalStorageDir(
           BaseApplication.getContext(), null, "$SUB_DIRECTORY_SPECIFIC_USER/Message/Video")

       /**
        * 閃屏圖
        */
       @JvmStatic
       fun getSplashStorageDir() = ScopedStorageManager.getExternalStorageDir(BaseApplication.getContext(), null, "$SUB_DIRECTORY_UNIVERSAL/Splash")
   }

}

ScopedStorageManager.kt

/**
 * 從舊版存儲位置遷移現有文件
 * @param dirMap 目錄Map
 * @param listener 遷移進度監聽器
 */
fun migrateExistingFilesFromLegacyStorageDir(dirMap : Map<File, File?>, listener: ProgressListener) {
    Observable.create(ObservableOnSubscribe<Int> { emitter ->
        var totalSize = 0L
        // 計算需要遷移的總文件大小
        for((src, destDir) in dirMap.entries){
            if(!src.exists()){
                LogUtil.w("源文件或目錄[${src.name}]不存在,不計入統計")
                continue
            }

            if(destDir == null || !destDir.exists()){
                LogUtil.w("目標目錄[(${destDir?.name}]為空或不存在,不計入統計")
                continue
            }

            if(!destDir.isDirectory) {
                LogUtil.w("destDir[${destDir?.name}]非目錄,不計入統計")
                continue
            }

            totalSize += FileUtils.sizeOf(src)
        }
        emitter.onNext(0)   // 遷移開始

        LogUtil.d("需遷移的文件總大小 totalSize = ${FileUtils.byteCountToDisplaySize(totalSize)}")

        var migratedSize = 0L   // 已遷移的文件大小
        for ((src, destSir) in dirMap.entries) {
            if(!src.exists()) {
                LogUtil.w("源文件或目錄[${src.name}]不存在,不執行遷移")
                continue
            }

            if(src.isDirectory) {
                for (file in src.listFiles()){
                    destSir?.let {
                        if(file.isDirectory){
                            FileUtils.copyDirectoryToDirectory(file, destSir)
                            LogUtil.d("遷移目錄[${file.name}]至目錄[${destSir.name}]...")
                        } else {
                            FileUtils.copyFileToDirectory(file, destSir)
                            LogUtil.d("遷移文件[${file.name}]至目錄[${destSir.name}]...")
                        }

                        migratedSize += FileUtils.sizeOf(file)

                        LogUtil.d("已遷移數據大小 migratedSize = ${FileUtils.byteCountToDisplaySize(migratedSize)}")

                        val progress = (migratedSize * 100 / totalSize.toFloat()).toInt();
                        LogUtil.d("遷移進度 progress = $progress")

                        emitter.onNext(progress)    // 回調遷移進度
                    }
                }
            } else {
                destSir?.let {
                    FileUtils.copyFileToDirectory(src, destSir)
                    LogUtil.d("遷移文件[${src.name}]至目錄[${destSir.name}]...")

                    migratedSize += FileUtils.sizeOf(src)
                    LogUtil.d("已遷移數據大小 migratedSize = ${FileUtils.byteCountToDisplaySize(migratedSize)}")

                    val progress = (migratedSize * 100 / totalSize.toFloat()).toInt();
                    LogUtil.d("遷移進度 progress = $progress")

                    emitter.onNext(progress)
                }
            }

            LogUtil.d("遷移完成,刪除文件或目錄:[${src.name}]")
            FileUtils.deleteQuietly(src)
        }
        emitter.onNext(100) // 遷移完成
    })
            .distinct()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                when(it) {
                    0 -> listener.onStart()
                    100 -> listener.onFinish()
                    else -> listener.onProgress(it.toLong())
                }
            }, {
                t -> LogUtil.e("從舊版存儲位置遷移現有文件出錯:${t.message}")
                t.printStackTrace()
                listener.onError()
            })
}

完成了數據遷移之後,我們就要著手開始對舊有的使用到文件存儲的業務及其對應的存儲目錄進行改造了,以確保能夠正確訪問到遷移後的文件,使應用正常的業務不受影響。


此處以縮略圖和適配為例:

private fun convertVideo(holder: BaseViewHolder, item: Message) {
    val video = JSONUtil.fromJson(item.content, VideoContent::class.java)
    val view = convertThumbnail(holder, video.thumbnail)
    holder.setVisible(R.id.play_button, true)
    view.setOnClickListener {
//            VideoPlayActivity.startActivity(context, File(OldStorageManager.getMessageVideoStorageDir(), video.compressed).absolutePath)
        VideoPlayActivity.startActivity(context, File(TestStorageManager.getMessageVideoStorageDir(), video.compressed).absolutePath)
    }
}

private fun convertThumbnail(holder: BaseViewHolder, thumbnail: String) : View{
    val viewStub = holder.getView<ViewStub>(R.id.thumbnail_view_stub)
    val view =
        (if (viewStub.parent != null) viewStub.inflate() else holder.getView(R.id.thumbnail_layout)) as View
    val imageView = view.findViewById<ImageView>(R.id.thumbnail)
    Glide.with(context)
//            .load(File(OldStorageManager.getMessageThumbnailStorageDir(), thumbnail))
        .load(File(TestStorageManager.getMessageThumbnailStorageDir(), thumbnail))
        .override(500, 500)
        .centerCrop()
        .into(imageView)
    return view
}

準備:

1.一臺裝有舊版本App的手機,積累的緩存文件足夠多(最好超過1G)

測試流程:

1.檢查設備下的文件管理-內部存儲-Test文件夾是否存在
2.覆蓋安裝新版本
3.App啟動之後/閃屏圖顯示之前,是否有數據遷移的進度條顯示
4.進度條完整跑完,正常顯示啟動圖並進入主頁面
5.設備下的文件管理/內部存儲/Test文件夾是否已刪除
6.設備下的文件管理-內部存儲-Android-data-{package_name}-file下的文件目錄結構是否與上方繪製的架構圖一致。
7.App各項業務功能是否正常使用

文件存儲規範建立之後,當有新的業務需要建立單獨文件目錄時,可以遵循以下規律決定存放的位置:

至此,Android 10 應用分區存儲適配實踐就已全部完成。

可以明顯感受到,近年的Android系統迭代正逐漸往更關於隱私、也更規範的方向發展,雖然每一次的系統適配都是一次不可避免的陣痛,但一個成熟系統的發展必然要經歷從混亂無序到規範有序的過程,規範建立之後,以後就再也不用飽受碎片化帶來的諸多痛苦了。

那麼,就從今天開始,和我一起通過以上文章完成Android 10 應用分區存儲的適配吧。

參考文章

管理分區外部存儲訪問
https://developer.android.google.cn/training/data-storage/files/external-scoped
將文件保存到外部存儲
https://developer.android.google.cn/training/data-storage/files/external
數據和文件存儲概覽
https://developer.android.google.cn/training/data-storage
選擇內部或外部存儲空間
https://developer.android.google.cn/training/data-storage/files#InternalVsExternalStorage
訪問應用專屬文件
https://developer.android.google.cn/training/data-storage/app-specific
用於數據存儲的應用兼容性功能
https://developer.android.google.cn/training/data-storage/compatibility

最後推薦一下我做的網站,玩Android: wanandroid.com ,包含詳盡的知識體系、好用的工具,還有本公眾號文章合集,歡迎體驗和收藏!

推薦閱讀:

掃一掃 關注我的公眾號

如果你想要跟大家分享你的文章,歡迎投稿~

┏(^0^)┛明天見!

相關焦點

  • Android 10(Q)/11(R) 分區存儲適配
    為此,Google 終於下定決心在 Android 10 中引入了分區存儲,對權限進行場景的細分,按需索取,並在 Android 11 中進行了進一步的調整。Android 存儲分區情況 Android 中存儲可以分為兩大類:私有存儲和共享存儲私有存儲 (Private Storage) : 每個應用在都擁有自己的私有目錄,其它應用看不到,彼此也無法訪問到該目錄:內部存儲私有目錄 (/data/data/packageName);外部存儲私有目錄 (/sdcard/Android/data/packageName),共享存儲 (Shared
  • Android11(30)/Android10(29)分區存儲適配-相關接口
    為了讓用戶更好地管理文件並減少混亂,Android 10(API 級別 29)引入了分區存儲。分區存儲是應用只能看到本應用特定的目錄下的文件(通過 Context.getExternalFilesDir() 訪問),公共目錄下的媒體文件(通過MediaStore訪問),以及存儲訪問框架返回的文件,不能像以前為所欲為了。
  • 一文帶你了解適配Android 11分區存儲
    為了讓用戶更好地控制自己的文件並減少混亂,Android 10針對應用推出的一個新的存儲範例,新的存儲模型會讓以 Android 10(API 級別 29)及更高版本為目標平臺的應用在默認情況下被賦予了對外部存儲設備的分區訪問權限,即分區存儲(scoped storage)。分區存儲改變了應用在設備的外部存儲設備中存儲和訪問文件的方式。
  • Android 10、11分區存儲適配踩坑總結
    /   作者簡介   /本篇文章來自乎如馮虛御風的投稿,文章分享了他研究Android分區存儲適配相關的經驗總結,相信會對大家有所幫助!同時也感謝作者貢獻的精彩文章。為了解決文件混亂的問題,以及讓用戶能夠更好地控制自己的文件和更好的保護用戶隱私,Google從Android Q版本開始修改了外部存儲權限。這種外部存儲的新特性被稱為分區存儲(Scoped Storage)。官方翻譯稱為分區存儲,我們一般稱為沙盒模式。至於為什麼這麼叫,大概是因為iOS一直都是這麼叫的吧。
  • Android 10、11 存儲完全適配!(建議收藏)
    這麼看來,導致目錄結構很亂,而且App卸載後,對應的目錄並沒有刪除,於是就是遺留了很多"垃圾"文件,久而久之不處理,用戶的存儲空間越來越小。有關targetSdkVersion 作用請移步:targetSdkVersion、compileSdkVersion、minSdkVersion作用與區別如果第二種方法也不能使用,則還有第三種方法。
  • Android 11 最全適配實踐指南 | 開發者說·DTalk
    (打⭐的格外注意哦)此模塊的修改內容只針對targetSdkVersion 30或者以上才生效。分區存儲強制執行⭐「對外部存儲目錄的訪問僅限於應用專屬目錄,以及應用已創建的特定類型的媒體。」關於分區存儲,在Android 10就已經推行了,簡單的說,就是應用對於文件的讀寫只能在沙盒環境,也就是屬於自己應用的目錄裡面讀寫。其他媒體文件可以通過MediaStore進行訪問。但是在 Android 10 的時候,Google 還是為開發者考慮,留了一手。
  • Android 存儲空間的最佳實踐 (下)
    分區存儲改變了應用在外置存儲中保存和訪問文件的方式,為了幫您遷移應用並支持分區存儲,我們概括了常見用例的最佳實踐並分享給大家。在我們過去的文章推送裡已經向您介紹了處理媒體類文件的常見用例和最佳實踐,本篇將繼續帶您了解處理非媒體文件的用例和最佳實踐,供您參考。這部分內容描述了處理非媒體文件的一些常見用例,並概要說明了應用可以使用的方法。
  • 相冊適配 Android 11 繞的那些彎路
    通過翻查官方文檔,大概知道了這個屬性的意思:在配置targetSdk >= 29,應用搭載在Android 10及以上版本的手機運行時,可以暫時停用「分區存儲」1.「分區存儲」又是什麼?分區存儲為了讓用戶更好地管理自己的文件並減少混亂,以 Android 10(API 級別 29)及更高版本為目標平臺的應用在默認情況下被賦予了對外部存儲空間的分區訪問權限(即分區存儲)。此類應用只能訪問外部存儲空間上的應用專屬目錄,以及本應用所創建的特定類型的媒體文件。
  • Android 存儲空間的最佳實踐 (上)
    為了提高文件的規整程度並讓用戶可以更好地控制他們的文件,Android 10 為應用引入了名為 "分區存儲" 的新範式。
  • 乾貨 | 攜程 Android 10適配踩坑指南
    在Android 10 版本中,官方的改動較大,相應的開發者適配成本還是很高的。基於前期調研,我們主要基於以下幾方面進行Android 10的適配:Android X分區存儲設備ID明文HTTP限制AndroidX 對原始 Android Support庫進行了重大改進,後者現在已不再維護。
  • Android 11 來襲,一起來適配吧
    存儲機制更新Scoped Storage(分區存儲)具體適配方法和去年的Android 10 適配攻略中的沒有太大區別。不過需要注意的是,應用targetSdkVersion >= 30,強制執行分區存儲機制。
  • 一文掌握 Android 11 適配攻略
    Android 10 適配攻略中的沒有太大區別。不過需要注意的是,應用 targetSdkVersion >= 30,強制執行分區存儲機制。之前在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true" 的適配方式已不起作用。
  • Android 11 適配,看這篇就夠了!
    一、存儲機制更新Scoped Storage(分區存儲)具體適配方法和去年的Android 10 適配攻略中的沒有太大區別。https://weilu.blog.csdn.net/article/details/104513170不過需要注意的是,應用targetSdkVersion >= 30,強制執行分區存儲機制。之前在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"的適配方式已不起作用。
  • Android 存儲進化:分區存儲
    Android 10之前,Android的文件存儲現象就像個垃圾桶,但凡app取得了存儲空間的讀寫權限WRITE_EXTERNAL_STORAGE,就可以肆意創建文件,難以管理。用戶體驗也特別差,打開文件管理器,會發現,想找個具體的文件根本無從下手。1.1 分區存儲原則為了更好地管理自己的文件並減少混亂,加強隱私保護,Android Q開始引入了分區存儲機制。
  • 隱私策略更新 | Android 11 應用兼容性適配
    在本文中,我們將以下面四個最佳實踐作為切入點,助力您的應用設計與時俱進,並計劃開始進行兼容性測試。處理內容 URI 分享遞增式權限申請在前臺訪問敏感數據使用可重置的標識符隨著 Android 11 中軟體包可見性的策略更新,目標 API 級別為 30 的應用對設備上已安裝的其它軟體包默認僅擁有受限的可見性。
  • Android 10 適配攻略,你適配了嗎?
    相比較去年的寫的Android 9適配,這次Android 10的內容有點多。沒想到寫了我整整兩天,吐血中。。。老規矩,首先將我們項目中的targetSdkVersion改為 29。說明在Android 10之前的版本上,我們在做文件的操作時都會申請存儲空間的讀寫權限。
  • Android端權限隱私的合規化處理實踐
    具體實踐一.Android各版本對權限的適配處理1.1 早期的註冊權限Android6.0(SDK版本為23)之前的版本,安裝App頁面會列出當前app所註冊的所有權限,無同意與否按鈕,只有安裝和取消,開發App時只需要在清單文件中註冊所需的對應權限即可:<uses-permission android:name="android.permission.READ_PHONE_STATE
  • Android 11來了,快!扶我起來
    分區存儲還記得在適配安卓10的時候設置requestLegacyExternalStorage為true來修改外部存儲空間視圖模型(true為 Legacy View,false 為 Filtered View)嗎?
  • RxHttp 完美適配Android 10/11 上傳/下載/進度監聽
    隨著Android 11的正式發布,適配Android 10/11 分區存儲就更加的迫切了,因為Android 11開始,將強制開啟分區存儲,我們就無法再以絕對路徑的方式去讀寫非沙盒目錄下的文件
  • Android 11 中的存儲機制更新
    Android 11 開發者預覽版裡加入了更多改進,以幫助開發者更好地適應這些權限修改。在 Google Play 上發布的大部分應用都會請求 (READ_EXTERNAL_STORAGE) 存儲權限,來做一些諸如在 SD 卡中存儲文件或者讀取多媒體文件等常規操作。這些應用可能會在磁碟中存儲大量文件,即使應用被卸載了還會依然存在。另外,這些應用還可能會讀取其他應用的一些敏感文件數據。