<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          相冊適配 Android 11 繞的那些彎路!

          共 8559字,需瀏覽 18分鐘

           ·

          2021-04-17 14:44

          劉望舒 專注于大前端和Java領(lǐng)域的個人技術(shù)號
          公眾號回復(fù)Android加入安卓技術(shù)群


          作者:Android_ZzT

          https://juejin.cn/post/6924270961889902599

          一、背景

          最近公司中的相冊組件被業(yè)務(wù)方反饋了新問題,在 targetSdk=30 的 Android 10 手機上運行相冊,縮略圖會加載不出來,于是就開啟了這次的趟坑之路。

          定位問題

          首先,我在相冊Demo中把 targetSdk 設(shè)置到 30, 然后在 Android 10 測試機上運行,發(fā)現(xiàn)縮略圖完美的顯示了出來。

          很懵逼,為啥相同的代碼 demo 上正常,業(yè)務(wù)方的 app 不正常?

          一定是有什么配置不一樣,才導(dǎo)致了這樣的結(jié)果。

          經(jīng)過了各種找不同 ...

          我發(fā)現(xiàn),demo 的 AndroidManifest.xml 中多了一個屬性

          <application
            android:requestLegacyExternalStorage="true"
            ...>

          于是,正式開啟了我的適配之路...

          二、requestLegacyExternalStorage 是什么?

          通過翻查官方文檔,大概知道了這個屬性的意思:在配置targetSdk >= 29,應(yīng)用搭載在Android 10及以上版本的手機運行時,可以暫時停用「分區(qū)存儲」

          1.「分區(qū)存儲」又是什么?

          分區(qū)存儲

          為了讓用戶更好地管理自己的文件并減少混亂,以 Android 10(API 級別 29)及更高版本為目標(biāo)平臺的應(yīng)用在默認情況下被賦予了對外部存儲空間的分區(qū)訪問權(quán)限(即分區(qū)存儲)。此類應(yīng)用只能訪問外部存儲空間上的應(yīng)用專屬目錄,以及本應(yīng)用所創(chuàng)建的特定類型的媒體文件。

          在搭載 Android 9(API 級別 28)或更低版本的設(shè)備上,只要其他應(yīng)用具有相應(yīng)的存儲權(quán)限,任何應(yīng)用都可以訪問外部存儲空間中的應(yīng)用專屬文件。為了讓用戶更好地管理自己的文件并減少混亂,以 Android 10(API 級別 29)及更高版本為目標(biāo)平臺的應(yīng)用在默認情況下被授予了對外部存儲空間的分區(qū)訪問權(quán)限(即分區(qū)存儲)。啟用分區(qū)存儲后,應(yīng)用將無法訪問屬于其他應(yīng)用的應(yīng)用專屬目錄。

          這是摘自官方文檔的一段話,我們可以把「分區(qū)存儲」簡單解釋為,Android 10 開啟分區(qū)存儲后,你的應(yīng)用在有權(quán)限的情況下也無法隨便訪問其他外部存儲空間中的公有文件夾

          2.「分區(qū)存儲」會造成什么影響?

          比如在App中展示相冊縮略圖的時候,我們會把 filepath 傳給圖片加載框架去幫助渲染縮略圖,像這樣

          ImageLoader.load(imageView, Uri.fromFile(path);

          這里的 path 一般為 sdcard/DCIM/...,這明顯為外部存儲空間中的文件夾,且不是應(yīng)用專屬文件,這時在圖片加載框架層就會拋出異常java.io.FileNotFoundException

          假如你用的是 Glide,會在圖中的代碼位置拋出異常

          三、Android 11 中 requestLegacyExternalStorage 屬性失效

          在繼續(xù)翻閱官方文檔后,又得知了一個信息:

          注意:當(dāng)您將應(yīng)用更新為以 Android 11(API 級別 30)為目標(biāo)平臺后,如果應(yīng)用在搭載 Android 11 的設(shè)備上運行,系統(tǒng)會忽略 requestLegacyExternalStorage 屬性,因此您的應(yīng)用必須做好支持分區(qū)存儲并為這些設(shè)備上的用戶遷移應(yīng)用數(shù)據(jù)的準(zhǔn)備。

          這段信息,簡單可以理解為 requestLegacyExternalStorage=true 只能解燃眉之急,到了 Android 11 上,還是要做適配工作。

          這也成功為我走上彎路,埋下了伏筆 ...

          四、開始走彎路

          1. 只適配 Android 10 (不推薦)

          在Manifest中添加

          <application
            android:requestLegacyExternalStorage="true"
            ...>

          我們剛才知道了,如果應(yīng)用在 Android 11 的設(shè)備上運行,系統(tǒng)會忽略 requestLegacyExternalStorage 屬性,強制開啟分區(qū)存儲。可能還是會出現(xiàn)異常(此處我并沒有真正用 Android 11 的機器驗證)。所以我默認認為,requestLegacyExternalStorage=true 只能解近憂,但不解本質(zhì)問題。

          2. 放棄 File path,使用 Uri

          前文已經(jīng)提到,我們用訪問 File path 的方式加載縮略圖,會拋出 java.io.FileNotFoundException

          那么,官方推薦我們怎么做呢?大致如下三步

          1. 獲取媒體數(shù)據(jù) id
          2. 獲取縮略圖 uri
          3. 用 uri 加載縮略圖
          val projection = arrayOf(
              MediaStore.Video.Media._ID,
              MediaStore.Video.Media.DISPLAY_NAME,
              MediaStore.Video.Media.DURATION,
              MediaStore.Video.Media.SIZE
          )

          ...

          val query = ContentResolver.query(
              MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
              projection,
              selection,
              selectionArgs,
              sortOrder
          )
          query?.use { cursor ->

            media.id = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
            
            ...
            
            media.thumbnailUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, media.id)
          }

          // Load thumbnail of a specific media item.
          val thumbnail: Bitmap =
                  applicationContext.contentResolver.loadThumbnail(
                  media.thumbnailUri, Size(640480), null)

          完整代碼,可參考 developer.android.com/training/da…

          由于這個變動涉及到數(shù)據(jù)源的變化,改動點非常多,并且還要用 if else 區(qū)分版本,所以寫了很多膠水代碼 ...

          但是,最終還是成功在 targetSdk=29 Android 10 的手機上成功顯示出了縮略圖。

          3. 新問題又出現(xiàn)

          相冊的圖片預(yù)覽功能也不能用了,經(jīng)過排查,發(fā)現(xiàn)是一樣的問題,膠水代碼已經(jīng)寫好,都在射程范圍內(nèi)。于是,用了半小時又改掉了圖片預(yù)覽的問題。

          正當(dāng)我興奮地覺得馬上要完工的時候,點了一下視頻預(yù)覽 ... 好吧,看到了熟悉卻又令人絕望的錯誤信息,依賴的播放器庫拋出了熟悉的異常 java.io.FileNotFoundException open failed: EACCES (Permission denied)。播放器中也是通過 file path 傳給 ffmpeg 進行播放的,但在初始化播放器的時候就因為沒有權(quán)限就直接掛了。

          4. 繞彎想方案

          首先,我找到了播放器的開發(fā)同學(xué)進行溝通,能否用傳遞 uri 或者 FileDescriptor 的方式進行初始化。得到了幾個不太友好的結(jié)論:

          1. 傳 uri 到 Native 層,content://media/external/images/media/{media_id},這種 Uri Native 層貌似無法打開(沒再細查有沒有辦法
          2. 傳 fd 到 Native 層,可能會涉及 java 層 fd 被 Native 引用,然后無法釋放的問題,如果要釋放還需要開放釋放 fd 的接口
          3. 除了相冊,還有很多地方在將 File path 傳到 Native 層

          然后,開始想怎么能繞過這個問題,大概找到了 2個 不靠譜的方案:

          1. 因為不能訪問公有目錄,那么可以先 copy file 到私有目錄(產(chǎn)品可能要罵街了

          2. 請求 MANAGE_EXTERNAL_STORAGE 權(quán)限

            這是一個有意思的權(quán)限,官方是這樣說的

            絕大多數(shù)需要共享存儲空間訪問權(quán)限的應(yīng)用都可以遵循共享媒體文件和共享非媒體文件方面的最佳做法。但是,某些應(yīng)用的核心用例需要廣泛訪問設(shè)備上的文件,但無法采用注重隱私保護的存儲最佳做法高效地完成這些操作。對于這些情況,Android 提供了一種名為“所有文件訪問權(quán)限”的特殊應(yīng)用訪問權(quán)限

            這段話里說的某些應(yīng)用,比如「殺毒應(yīng)用」「文件瀏覽器」,需要掃描 sdcard 的所有文件,如果沒有權(quán)限就沒法正常工作(很明顯,我們的App不是

            另外,對于這個權(quán)限的描述很有意思,長這樣

          如果我是用戶,看到了一個不需要這些權(quán)限的App卻申請了這種權(quán)限,無疑是一種勸退(產(chǎn)品又要罵街了

          5.冷靜下來,再看文檔

          做到第4步的時候,我開始意識到,很有可能繞彎路了,往常的適配工作還沒有這么變態(tài)過。于是我又查了一些資料,找到了這個視頻,https://www.youtube.com/watch?v=RjyYCUW-9tY&feature=youtu.be

          視頻中對我們有用的信息大概是這樣,在 Android 10 的時候,很多開發(fā)者都反應(yīng)了類似的問題,在使用一些 native 的庫時,無法使用 File Api,造成了很多困難。于是,在 Android 11 中,又做了兼容,又可以通過 Java File Api 的方式訪問媒體庫文件了(此時的我不知道是不是應(yīng)該高興,Android 確實比蘋果爸爸對開發(fā)者好)

          后來,我又仔細的翻了翻官方文檔,確實找到了一小段不起眼的文字

          使用直接文件路徑和原生庫訪問文件

          為了幫助您的應(yīng)用更順暢地使用第三方媒體庫,Android 11 允許您使用除 MediaStore API 之外的 API 通過直接文件路徑訪問共享存儲空間中的媒體文件。其中包括:

          • File API。
          • 原生庫,例如 fopen()。

          五、結(jié)論

          好吧...

          繞了一個大圈后,得到了幾個結(jié)果:

          1. 膠水代碼可能是白寫了,在 targetSdk=29 運行在 Android 10 的應(yīng)用上, requestLegacyExternalStorage 屬性完全夠用了(枉我開始我還鄙視它
          2. Android 11 的時候也不需要適配啥了,雖然 requestLegacyExternalStorage 屬性失效,但相冊里通過 File Api 訪問的只是媒體庫文件,不會有任何問題。
          3. 如果 App 中有通過 File Api 訪問外部存儲共有目錄的代碼,還是要需做適配的,至于怎么去做本文就不再討論了

          教訓(xùn)

          繞了一圈之后,得出兩個教訓(xùn):

          1. 適配新版本的時候,最好先用真機測試一下,萬一完美運行就不用適配了
          2. 認真讀文檔、認真讀文檔、認真讀文檔

          Glide 加載縮略圖

          最后,說個與適配不太相干的話題,只想看適配內(nèi)容的朋友可以先跳過了。

          我在適配的過程中也跟了一下 glide 加載縮略圖的流程,也搞清了一些問題,順便分享給大家

          1. 為什么向 Glide 傳 content-uri 不會出錯,傳 file path 會報錯?

          上文剛才介紹過,官方提供的獲取相冊縮略圖的做法是

          // Load thumbnail of a specific media item.
          val thumbnail: Bitmap =
                  applicationContext.contentResolver.loadThumbnail(
                  media.thumbnailUri, Size(640480), null)

          但是我們平時開發(fā),大多都直接用圖片加載框架,比如 Glide

          Glide
            .with(imageView)
            .asBitmap()
            .load(uri) //或者 file path
            .into()
          復(fù)制代碼

          在我們沒適配 Android 10 的時候,傳 file path 會拋出異常,這我們之前已經(jīng)解釋了。適配之后我們傳入了 content://media/external/images/media/{media_id} 給 Glide,Glide 又是怎么識別的然后加載出 bitmap 的呢?我?guī)е鴨栴}跟蹤了一下 Glide 加載圖片的過程的源碼,這里我們直接先說結(jié)論。

          StreamLocalUriFetcher

            private InputStream loadResourceFromUri(Uri uri, ContentResolver contentResolver)
                throws FileNotFoundException 
          {
              switch (URI_MATCHER.match(uri)) {
                case ID_CONTACTS_CONTACT:
                  return openContactPhotoInputStream(contentResolver, uri);
                case ID_CONTACTS_LOOKUP:
                case ID_LOOKUP_BY_PHONE:
                  // If it was a Lookup uri then resolve it first, then continue loading the contact uri.
                  uri = ContactsContract.Contacts.lookupContact(contentResolver, uri);
                  if (uri == null) {
                    throw new FileNotFoundException("Contact cannot be found");
                  }
                  return openContactPhotoInputStream(contentResolver, uri);
                case ID_CONTACTS_THUMBNAIL:
                case ID_CONTACTS_PHOTO:
                case UriMatcher.NO_MATCH:
                default:
                  return contentResolver.openInputStream(uri);
              }
            }

          uri 經(jīng)過匹配邏輯走到了 default 分支,使用 contentResolver.openInputStream(uri) 的方式來讀取 bitmap,既然是通過系統(tǒng)的 contentResolver 獲取,那一定是沒問題的。

          2. 淺談 Glide 加載圖片流程

          img

          這是我簡單總結(jié)的 Glide 加載圖片的流程,不做詳細解釋了,簡單介紹一下圖中的關(guān)鍵元素:

          1. 綠圈是時序
          2. 黃色方塊代表輸入、輸出
          3. 粗實線框代表類
          4. 細實線框代表關(guān)鍵方法
          5. 虛線代表方法屬于哪個類

          圖中的過程就是這段代碼運行的過程

          Glide
            .with(imageView)
            .asBitmap()
            .load(uri) //或者 file path
            .into()

          參考

          1. Android 存儲用例和最佳做法
          2. Android 11 中的存儲機制更新
          3. 拖不得了,Android11真的要來了,最全適配實踐指南奉上


          ·················END·················

          推薦閱讀

          ? 耗時2年,Android進階三部曲第三部《Android進階指北》出版!

          ? 『BATcoder』做了多年安卓還沒編譯過源碼?一個視頻帶你玩轉(zhuǎn)!

          ? 『BATcoder』是時候下載Android11系統(tǒng)源碼和內(nèi)核源碼了!

          ? 重生!進階三部曲第一部《Android進階之光》第2版 出版!

          BATcoder技術(shù)群,讓一部分人先進大廠

          你好,我是劉望舒,騰訊云最具價值專家TVP,著有暢銷書《Android進階之光》《Android進階解密》《Android進階指北》,蟬聯(lián)四屆電子工業(yè)出版社年度優(yōu)秀作者,谷歌開發(fā)者社區(qū)特邀講師。

          前華為面試官,現(xiàn)大廠技術(shù)負責(zé)人。


          想要加入 BATcoder技術(shù)群,公號回復(fù)Android 即可。

          為了防止失聯(lián),歡迎關(guān)注我的小號


          更文不易,點個“在看”支持一下??
          瀏覽 45
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲热情在线 | 日逼播放| 国产又爽 又黄 免费网站在线观看 | 国产一线二线www | 夜夜骚视频网 |