title: Glide圖片載入框架 author: 書蟲 tags:
- 工作記錄 categories:
- 工作 date: 2021-01-08 13:58:00
結論
- 最佳化記憶體表現最簡單、有效的方案是修復記憶體洩露。然後才是考慮框架層的最佳化。因為修復一個洩露,比如閱讀頁的洩露,記憶體回收會非常明顯。
“小的洩漏將擊沉一艘大船。”
框架層能最佳化的地方大部分都嘗試過了,所以在制定新的最佳化前要慎重。
多考慮為什麼以前沒有用這個最佳化方案,很可能這個最佳化方案會帶來更多的問題,而不是效能上的收益。
如果非要繼續最佳化的話,推薦從下面這些地方入手。
- 考慮使用硬體點陣圖,硬體點陣圖僅需要一半於其他點陣圖配置的記憶體。硬體點陣圖可避免繪製時上傳紋理導致的記憶體抖動。(目前這個功能是禁用的,禁用的方式是GlideModuleImpl#applyOptions#disallowHardwareConfig;以及format(DecodeFormat.PREFER_RGB_565)https://muyangmin.github.io/glide-docs-cn/doc/hardwarebitmaps.html
- 把所有外掛中使用url載入的改成使用view載入 那些使用url載入但是無法修改成view載入的,新增clear的操作。在bitmap使用結束後clear一下及時回收資源。
針對於硬體點陣圖以及&&ARGB_8888做了下測試。發現效果差。
實驗組:DecodeFormat預設是ARGE_8888,允許使用硬體點陣圖,url方式載入使用的是SimpleTarget,記憶體的容量是預設值的1/2,
記憶體情況是,開啟一系列頁面之後,記憶體維持在700M的高位,然後GC後下降到500M。
對照組:DecodeFormat預設是RGE_565,不允許使用硬體點陣圖,url方式載入使用的是SimpleTarget,記憶體的容量是預設值的1/2,
記憶體情況是,開啟一系列頁面之後,記憶體維持在550M的高位,然後GC後下降到500M。
Glide定製實現方案
生命週期管理
Glide預設支援Activity和fragment宣告週期監聽。
android.support.v4.app.FragmentActivity
android.support.v4.app.Fragment
使用的是SupportRequestManagerFragment來管理宣告週期
android.app.Activity
android.app.Fragment
使用的是RequestManagerFragment來管理宣告週期,目前RequestManagerFragment該類已經Deprecated。
但是這兩種方法都不適用於iReader的Fragment框架。
針對iReader的Fragment框架定製
思路:主動呼叫ActivityFragmentLifecycle來通知註冊的listener回撥生命週期相關的方法來觸發記憶體回收。
主要的定製流程
把Application轉換成Fragment或者Activity進而正常使用Fragment生命週期的註冊監聽的邏輯。
為了實現這個目的,需要事先了解Glide管理生命週期的原理,梳理如下: 建立了一個SupportRequestManagerFragment,它是一個Fragment 這個Fragment構造方法中自動初始化一個ActivityFragmentLifecycle型別的成員變數 ActivityFragmentLifecycle負責管理該Fragment頁面的監聽器,每個Fragment頁面的監聽器有且僅有兩個 這兩個監聽器是RequestManager和DefaultConnectivityMonitor。前者負責取消請求、回收資源;後者負責監聽網路狀態變化後重啟請求 瞭解了Glide管理生命週期的原理,定製的思路就明確了。只要把來自於CoverFragmentManger的Fragment的生命週期方法回撥橋接給ActivityFragmentLifecycle即可,這裡要保證是同一個ActivityFragmentLifecycle物件。 透過ActivityBase和BaseFragment的生命週期回撥來呼叫ActivityFragmentLifecycle的回撥方法,ActivityFragmentLifecycle內部自動分發給監聽器執行
針對於主TAB做特別處理
需要對主TAB(MainTabFragment)做特別處理,因為主TAB不會執行onDestroy,而如果直接使用這個Fragment為context載入圖片的話,會導致書城或其他用到BookStoreFragment的TAB記憶體不會及時回收。記憶體不會及時回收的原因再次解釋一下:
從上面可以看出一個頁面對應於一個SupportRequestManagerFragment,也就對應於一個ActivityFragmentLifecycle以及RequestManager和DefaultConnectivityMonitor。 所以想要即時回收資源就要找到對應的SupportRequestManagerFragment。
由於請求圖片時傳的是MainTabFragment,所以onStop或onDestroy時需要傳MainTabFragment而不是BookStoreFragment
所以給書城的BookStoreFragment載入圖片時,不能使用MainTabFragment而是要使用對應的BookStoreFragment。這樣當viewpager回收這個BookStoreFragment的時候,就會及時回收記憶體了
那就要找到這個BookStoreFragment。由於從MainTabFragment到特定的BookStoreFragment中間巢狀了很多層,需要一層層剝離出來。
剝離出來是比較麻煩的,具體參見下面的程式碼
效果驗證:
測試發現,該方案和Fragment框架不相容,不相容原因參見wiki中的技術分享部分的PPT。
所以廢棄了該方案,使用了Glide的新特性:clearOnDetach。它的作用如下:
當{@link View}從其{@link android.view.Window} 分離時,清除{@link View}的{@link Request},
並在重新連線{@link View}時重新啟動{@link Request}。
<p>這是實驗性API,將來的版本中可能會刪除它。
<p>使用此方法可以透過允許Glide在滾動檢視中切換螢幕或交換介面卡時更急於清除資源來節省記憶體。
但是它也大大增加影象將不在記憶體中的機率,如果使用者隨後返回螢幕中先前載入影象的位置。
是否發生這種情況將取決於新螢幕中載入的影象和記憶體快取的大小。
增加記憶體快取的大小可以改善此行為,但在很大程度上抵消了使用此功能來降低記憶體帶來的好處。
<p>請謹慎使用此方法,並測量您的記憶體使用情況,以確保它實際上是在提高您所關心的那個記憶體指標,比如降低記憶體開銷,或者提高快取命中率。於是我就開啟了這個功能,所以view方式載入的圖片都預設開啟了這個功能。測試階段沒發現問題。 但是出於穩定性考慮,全量的時候我就關閉了。留給外掛,尤其是書城外掛這種recycleview滾動需要頻繁attach、detach的場景下自行最佳化使用。 後續外掛中可以單獨開啟這個功能。在BitmapCustomViewTarget7400這個類的子類的構造方法中新增一行:clearOnDetach()
專案中給url方式(非view繫結的方式)載入的時候,嘗試使用了PreloadTarget,來取代以前的SimpleTarget方式。 這樣做的原因是因為,原先使用SimpleTarget方式需要做clear的處理。當業務使用完bitmap之後,需要clear掉這部分資源。這個clear的操作需要業務層新增,改動量較大。 所以使用了PreloadTarget來替代它。PreloadTarget支援自動clear。它會在載入bitmap成功後自動clear掉自己的情況。 於是這個功能隨著beta上線了一次。 線上發現如下異常情況:
java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@ff4a146
android.graphics.BaseCanvas.throwIfCannotDraw(BaseCanvas.java:66)
android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:277)
android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:88)
android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:548)
android.widget.ImageView.onDraw(ImageView.java:1434)
android.view.View.draw(View.java:21460)
android.view.View.updateDisplayListIfDirty(View.java:20337)
android.view.View.draw(View.java:21192)
android.view.ViewGroup.drawChild(ViewGroup.java:4388)
android.view.ViewGroup.dispatchDraw(ViewGroup.java:4149)所以PreloadTarget的方案和現階段的專案結合後雖然會及時清除資源,但是會導致異常。Canvas: trying to use a recycled bitmap。
理想情況下可以這麼做:
- 把url載入方式改成view載入方式,這樣Glide會自動管理clear掉view的bitmap資源。這個改動量挺大的,而且有些業務是無法修改的。
- 如果1無法實現。退而求其次,可以繼續使用SimpleTarget,但是這樣就需要在業務層使用完畢後,及時clear掉bitmap資源,這其實做的是Glide應該做的事。這個改動量也挺大的,而且難點是要一個個的梳理出來,全部改一邊,而且個別情況下,找不到使用完畢的時機無法清理。
- 如果1和2都無法實現。只能是使用SimpleTarget,然後不做clear的處理。這樣會導致申請的bitmap逐漸積累起來,等待activity的onDestroy的時候清理一次,或者trimOnSize的時候清理。無法做到及時清理。而且還會導致載入圖片的request請求所佔用的資源無法及時清理掉。
通用的最佳化方法
- context儘量傳Fragment,獲取不到Fragment則傳Activity,最後才是application
- 能繫結控制元件的就傳控制元件,Glide會根據view的Width、Height按需載入bitmap
- 解碼格式儘量不使用高畫質圖格式,比如使用Bitmap.Config.RGB_565
- 解決記憶體洩露問題,避免導致記憶體無法及時回收的情況
針對專案的最佳化方法: 上文中根據支援Fragment生命週期回撥也是一項重要的效能最佳化,有利於Glide及時回收記憶體。
效能最佳化很重要的一部分是記憶體最佳化。
記憶體最佳化的時候,先完成了下文中的最佳化點1、2、3。
即便這三個最佳化點都支援了,但是發現native層的記憶體還是不能及時回收。native層的記憶體一般都在100MB以上,加上Graphic層的記憶體也在100MB以上。整體記憶體也就不可能低於300MB。
native層和Graphic層的記憶體佔用如果不能大幅減少的話,記憶體的整體開銷就會較大。(native層是android 8.0+以上儲存bitmap的地方)
要想減少native層和Graphic層記憶體,不能直接回收,只能間接回收,要考慮到bitmap在java層的快取,也就是LruResourceCache(MemoryCache),要降低就是這部分記憶體。
在我的Google Pixel 3A手機上,使用Glide預設的快取大小(2個螢幕且每個畫素4個位元組)大約是17MB。也就是LruResourceCache(MemoryCache)是17MB。
很顯然這裡存放的並不是bitmap佔用的真實記憶體,這裡放的是bitmap的屬性資訊。但是如果能及時回收掉這裡的bitmap的,那也就會間接釋放掉native層的記憶體了。
基於此做了最佳化點4.
做了最佳化點4之後發現native層的記憶體確實會降低,比較明顯,但是會導致記憶體抖動明顯。所以trimToSize(maxSize/8)不適合,應該調大一些,比如trimToSize(maxSize/4)。
另外,最佳化點5也是為了儘可能減少native層的記憶體,但是最佳化點5做不到框架層統一封裝處理,只能在業務層主動處理。
疑問:
- 既然置於後臺的時候Glide可以明顯釋放一部分記憶體,那參考它使用的方式,主動在Fragment#onDestroy的時候呼叫trimOnSize是不是也可以釋放記憶體?
- 置於後臺的時候,為什麼即便是Glide也不能明顯回收掉native層的記憶體?
- Fragment在onDestroy的時候會主動釋放記憶體,那為什麼記憶體下降不明顯呢?
最佳化點1:根據控制元件的尺寸決定載入圖片的尺寸
- 首先獲取的是呼叫傳入的overrideWidth、overrideHeight的值
- 如果這兩個值有效,則進入載入流程
- 如果這兩個值無效,比如是-1,則嘗試獲取尺寸,具體步驟是(以獲取width為例)
- 透過view獲取到LayoutParams.width,padding值和view.getWidth
- 先判斷LayoutParams.width - padding是否有效,有效則返回
- 判斷view.getWidth - padding是否有效,有效則返回
- 如果均無效,這是普遍情況,剛載入的時候LayoutParams.width和view.getWidth這些值都是0,則進入等待下次獲取過程
- 測量是透過ViewTreeObserver.addOnPreDrawListener的方式,在onPreDraw方法中觸發的再次獲取,獲取過程從步驟a開始
- 測試發現,專案中能獲取控制元件真實尺寸的情況大都是在onPreDraw方法中觸發的再次獲取的
- 以前封裝框架的時候沒有把view的LayoutParams引數帶上,LayoutParams為空,導致載入的bitmap是原始尺寸
- 這次給這些方法加上了Deprecated標籤,不推薦使用
- 看是否根據控制元件大小載入bitmap,需要看到如下日誌才可以證明是按照控制元件大小載入的
- 用TAG CustomViewTarget過濾日誌,需要列印:
V/CustomViewTarget: OnGlobalLayoutListener called attachStateListener=com.bumptech.glide.request.target.CustomViewTarget$SizeDeterminer$SizeDeterminerLayoutListener@667a3d9- 用TAG GlideRequest過濾日誌,需要列印:
V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5328.244957999999 this: 103137153- 注意步驟b中的寬高的值
- 用TAG Downsampler過濾日誌,需要列印:
V/Downsampler: Calculated target [330x413] for source [728x910], sampleSize: 2, targetDensity: 1769323234, density: 1949254399, density multiplier: 0.9076923支援按照控制元件尺寸載入bitmap後,用TAG GlideRequest過濾日誌,列印的日誌如下
2020-09-23 11:33:52.559 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 69.489642 this: 239600185
2020-09-23 11:33:57.871 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5374.561889 this: 241593214
2020-09-23 11:33:57.874 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5352.17246 this: 157280429
2020-09-23 11:33:57.876 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5352.557355999999 this: 39924192
2020-09-23 11:33:57.877 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5328.010947 this: 129224862
2020-09-23 11:33:57.878 17935-17935/? V/GlideRequest: onSizeReady width=904, height=413 , Got onSizeReady in 5328.244957999999 this: 103137153支援按照控制元件尺寸載入bitmap後,用TAG DownSampler過濾日誌,列印的日誌如下
V/Downsampler: Calculated target [330x413] for source [728x910], sampleSize: 2, targetDensity: 1769323234, density: 1949254399, density multiplier: 0.9076923
V/Downsampler: Calculated target [904x1130] for source [728x910], sampleSize: 1, targetDensity: 2147483647, density: 1729389506, density multiplier: 1.2417582如果不支援按照控制元件尺寸載入bitmap,用TAG GlideRequest過濾日誌,列印的日誌如下
2020-09-23 12:04:37.598 20840-20840/com.chaozh.iReaderFree V/GlideRequest: onSizeReady width=-2147483648, height=-2147483648 , Got onSizeReady in 0.053542 this: 175187022
2020-09-23 12:04:37.598 20840-20840/com.chaozh.iReaderFree V/GlideRequest: onSizeReady width=-2147483648, height=-2147483648 , Got onSizeReady in 0.019688 this: 49803077
2020-09-23 12:04:37.600 20840-20840/com.chaozh.iReaderFree V/GlideRequest: onSizeReady width=-2147483648, height=-2147483648 , Got onSizeReady in 0.010104 this: 170736321透過日誌看到了如下奇葩情況
- 要載入的bitmap尺寸比原圖大
Calculated target [904x1602] for source [578x1024], sampleSize: 1, targetDensity: 2147483647, density: 1373059236, density multiplier: 1.5640138- 傳進來的尺寸是預設值,
Calculated target [-1x-1] for source [-1x-1], sampleSize: 1, targetDensity: 0, density: 0, density multiplier: 1.0最佳化點2:滑動、拋動列表時暫停載入、停止之後恢復載入
RecyclerView在上下滑動的時候,會複用holder,進而複用holder中的view。
由於Glide會把view和載入請求繫結,所以在滑動recyclerView的時候,就會出現複用的view需要先解綁原來的請求,然後繫結新的請求的情況。
在解綁原來的請求的時候,Glide會把這部分資源釋放掉,把bitmap放到LruCache中。這是Glide本身就支援的功能。
不僅限於RecyclerView,其實是view複用的場景都適用。
滑動列表時暫停載入的機制和上面描述的view複用的場景不同,但是會有相互的影響。滑動列表時暫停載入的原理是這樣的。
滑動列表時,會不停的bindHolder進而產生一系列的載入請求。Glide內部使用陣列把這些請求管理起來了。
如果列表處於滑動、拋動狀態,則標記一個暫停載入的flag。此時,一些列的家在請求都不會被真正的處理,而是放在佇列裡處於等待狀態。
當列表處於停止狀態時,則標記一個恢復載入的flag。此時會遍歷列表,把等待狀態的請求恢復。
注意,滑動列表暫停載入,停止滑動恢復載入的功能只對載入中的請求有效,已經載入完畢的請求不受影響。
這個功能啟用之後有個問題。
滑動列表會暫停載入,所以滑動過程中看到的被複用的holder的圖片其實是預設圖。因為被複用的holder在onViewRecycled方法中清理掉了bitmap。
而滑動時又不會載入bitmap,導致繪製的是預設圖。
只有停止滑動列表後才會載入真正的圖片,如果載入的圖片來自記憶體快取,則不會做動畫而是直接出現。這樣從預設圖直接顯示成真實圖,會導致視覺上的閃現。
這2情況的視覺效果都是無法接受的。
基於此對Glide做了定製。
定製思路:
啟用“滑動列表暫停載入”功能後,滑動列表允許繼續載入(定製前不會繼續載入)。
不過這種情況下僅允許從快取(記憶體快取 + 磁碟快取)中載入。
等列表滑動結束後,載入圖片允許從快取+網路載入。
最佳化點3:CustomTarget導致的記憶體無快取
不推薦使用這種方法:
public void get(String requestUrl, final ZyImageLoaderListener listener, int maxWidth, int maxHeight, android.graphics.Bitmap.Config config)
這種呼叫方式效能很差,
- 因為沒有繫結view,所以無法獲取控制元件大小,導致只能載入原圖
- context沒有傳,也就無法監控生命週期,也就沒有銷燬的觸發時機,導致customTarget載入bitmap不會放回到LruCache。
- 不放到LruCache中,就無法被複用
- 每次都會新建,但是不會被複用
定製之後出現新的問題:
問題是Glide.with()傳入Fragment時,內部會用到handler,而我們的Fragment框架定製的原因,導致獲取Fragment.mHost(FragmentHostCallback)的handler是null。
為了支援這個功能就需要修改mHost,影響較大。
另外在效能上這裡使用了BitmapCustomViewTarget7400#clearOnDetach方法代替本功能。殊途同歸,效果一樣。
最佳化點4:在Fragment的onDestroy時,及時給MemoryCache呼叫trimToSize(maxSize/2),及時回收記憶體
該方案依賴於Fragment生命週期繫結,目前不支援Fragment生命週期繫結。支援activity生命週期繫結,有一定效果。
最佳化點5:有些圖片,只展示一次的那種,使用glide可以設定跳過快取,skipMemoryCache,用完就給它回收(未啟用)
比如彈窗圖片。
圖片被快取起來之後如果不能複用,就會佔用cache的空間,cache裡是強引用。正常情況下佔用的記憶體是不會被釋放的。
最佳化點6:Glide自帶功能(功能介紹)
Glide內部註冊了ComponentCallbacks2介面,這個功能是從Glide版本4.0+開始支援的。Glide初始化的時候會主動註冊該介面。
當APP置於後臺時,系統會呼叫onTrimMemory(TRIM_MEMORY_UI_HIDDEN)方法,Glide會響應這個方法把釋放記憶體。
trimToSize(getMaxSize() / 2);
下面是效果圖:
記憶體從開啟時的380MB下降到了270MB。
最佳化點7:專案中使用VolleyLoader載入的老程式碼,快取會放到Volley的快取中
目前這部分的快取大小被進一步減小了
final int maxMemory = Math.min(activityManager.getMemoryClass() * MB, Integer.MAX_VALUE);
if(maxMemory < 64 * MB) {
return MB >> 1;
} else if(maxMemory < 128 * MB) {
return MB;
} else if(maxMemory < 256 * MB) {
return MB << 1;
} else {
return 3 * MB;
}maxMemory和一螢幕所佔記憶體誰小用誰。
最佳化點8:廣告外掛載入圖片最佳化(有專門的PPT分享,去本站其他目錄尋找)
- 程式碼上,目前的載入沒有傳控制元件,沒有傳尺寸,導致載入原圖,甚至有的是載入螢幕寬、高的圖
- 下圖是有問題的程式碼截圖
- 明明有ImageView但是載入圖片時卻不傳給Glide使用,導致Glide無法做最佳化
- 寬、高傳的太大
- 圖片解碼格式傳的是高畫質的,一畫素會佔用4個位元組,有必要這麼高畫質嗎,是否有最佳化空間
- 使用的時候沒有封裝統一的方法
廣告外掛存在問題的原因分析:
- 廣告在使用bitmap時有特殊需求:載入bitmap + 結果回撥 + 呼叫先後順序(圖片載入成功之後再顯示控制元件,正常情況下是先顯示控制元件再載入圖片)
- 使用過程中沒來記得考慮載入圖片最佳化問題,而選擇了效能差的使用方式
最佳化:
- 新增BitmapUtil類,統一封裝載入圖片的方法
- 新增載入方法,滿足這種特殊需求:載入bitmap + 結果回撥
- 按照固定尺寸載入:把view傳給Glide
- 支援限制載入圖片的最大寬、高,避免某張廣告圖片過大導致的OOM
最佳化點9:各個廣告位載入圖片的情況統計分析
限制可載入的廣告圖片的最大寬高720*1440 底通之類的廣告,限制圖片的最大高度是48dp,由於存在不同的寬高比的問題,所以只能指定高度的最大值,而無法指定寬度的最大值
底通廣告,控制元件尺寸已知,尺寸小,但是載入的廣告圖片是1280 × 720
插頁廣告,控制元件尺寸已知,尺寸中等,但是載入的廣告圖片是1280 × 720
其他廣告,控制尺寸未知,尺寸中等,但是載入的廣告圖片是1280 × 720
以上尺寸的圖片最佳化後尺寸是720 x 405
更多示例:
底通廣告最佳化後:
BitmapTransformation: transform from (width, height)=(1080, 1920) to (74, 133)
BitmapTransformation: transform from (width, height)=(654, 933) to (93, 133)
BitmapTransformation: transform from (width, height)=(640, 960) to (88, 133)
最佳化前,解碼階段的日誌情況:
Decoded [1280x720] RGB_565 (3686400) from [1280x720] image/jpeg with inBitmap [1280x720] RGB_565 (3686400) for [-2147483648x-2147483648], sample size: 1, density: 0, target density: 0, thread: glide-source-thread-3, duration: 13.797189
Decoded [1280x720] RGB_565 (3686400) from [1280x720] image/jpeg with inBitmap [1280x720] RGB_565 (3686400) for [-2147483648x-2147483648], sample size: 1, density: 0, target density: 0, thread: glide-disk-cache-thread-0, duration: 16.571147
Decoded [1280x720] RGB_565 (3686400) from [1280x720] image/jpeg with inBitmap [1280x720] RGB_565 (3686400) for [-2147483648x-2147483648], sample size: 1, density: 0, target density: 0, thread: glide-disk-cache-thread-0, duration: 20.53021
Decoded [1280x720] RGB_565 (3686400) from [1280x720] image/jpeg with inBitmap [1280x720] RGB_565 (3686400) for [-2147483648x-2147483648], sample size: 1, density: 0, target density: 0, thread: glide-source-thread-0, duration: 24.960732目前不支援的最佳化點
UnionPagesFetcher不支援根據控制元件大小按需載入。因為裡面有兩個控制元件,動態顯示,無法提前預知。
UnionPagesFetcher是閱讀頁插頁、段間廣告的模版,該模版載入完圖片之後供段間、插頁廣告顯示
UnionPagesFetcher里載入圖片時,由於控制元件的尺寸無法提前確定,需要載入完圖片之後,根據圖片是大圖還是小圖才能確定控制元件的尺寸,所以這裡無法提前知道控制元件尺寸,也就無法做到按需載入UnionPagesFetcher使用很頻繁,但是目前只能做到載入寬最大是720,高最大是1440的圖。
最佳化點10:把CustomTarget改成PreloadTarget
PreloadTarget優於CustomTarget的地方是,前者載入完成之後就會自動清理掉
該方案被驗證不可行。
高低版本相容處理
問題1:低版本上書城頻道是按照圖片的原始尺寸載入的
載入請求的日誌:
2020-09-25 19:57:35.093 11157-11157/com.chaozh.iReaderFree V/GenericRequest: Got onSizeReady width=-2147483648, height=-2147483648, in 0.051771 this: 117751467寬、高給的是一個特殊的初始值,該值會導致直接載入原始尺寸的bitmap,直接略過獲取控制元件尺寸的步驟。
解碼bitmap的日誌:
2020-09-25 19:57:35.953 11157-11657/com.chaozh.iReaderFree V/Downsampler: Decoded [300x400] RGB_565 (240000) from [300x400] image/webp with inBitmap null for [-2147483648x-2147483648], sample size: 1, density: 0, target density: 0, thread: fifo-pool-thread-7, duration: 245.66122299999998從日誌可以看出,載入和解碼的都是原始尺寸的圖片。
適配上已經支援低版本根據控制元件的尺寸來載入bitmap,所有外掛都已經修改,而且外掛都可以配到可支援的最低版本。
適配方法:
要了解根據控制元件尺寸載入bitmap的原理才能進行適配。原理參見第四部分的最佳化點1。 適配方法就是直接把真實的view傳過去,而不是把封裝的ZyImageTargetView傳過去。 傳過去真實的view之後,如果view在業務層呼叫了setTag方法,由於Glide內部也使用了setTag方法,導致兩者衝突,我認為這是Glide V3.x版本設計上的缺陷 4.x版本的Glide解決了setTag和應用層衝突的問題,所以我適配的時候參考了Glide V4.x.0版本的方案。 方案很簡單,就是使用setTag(int id,Object obj)方法,給Glide定義一個專屬的id,這樣應用層不論是否傳了id都不會衝突了 其實3.x版本的Glide也提供了這種方法ViewTarget#setTagId(int tagId),但是這個方法需要App啟動的時候呼叫(呼叫時機晚了就失效了),而且只能調一次(否則崩潰),所以從相容低版本角度考慮只能反射了
高版本和適配後的低版本都已經支援根據控制元件的尺寸按需載入bitmap。
問題2:外掛中打iReader_plugin的jar
為了方便打jar包,保留了主工程中的GlideV3.5.2版本的庫,這個庫從iReaderV7.38.0開始已經廢棄不用。 GlideV3.5.2版本的庫本應該刪除掉,但是刪除之後打的jar包會有編譯錯誤,無奈只能保留這個庫。 程式碼中已經新增了廢棄的註釋。不推薦使用。推薦使用com.bumptech.glide.Glide。
問題3:外掛中新增一個glide-4.9.x的jar包(provided-libs資料夾下)
該jar僅供編譯,不應該打包到外掛中。外掛執行的時候使用的是主工程的庫。
問題4:把定製和擴充套件的便利性留給外掛(當然主工程本身就支援這些便利性)
不論是iReader的高低版本,外掛中都能拿到Glide的類,都支援定製和擴充套件 只不過外掛中在使用時要注意區分iReader的版本,注意以下關鍵版本 V7.9.0 引入Glide V3.5.3 V7.38.0升級Glide到V4.9.0 現在外掛中都有一個provided方式的glide的jar包,該jar僅參與外掛的編譯,該jar包是從iReader V7.38.0加入的,支援外掛中使用Glide擴充套件 外掛中自定義view可以直接交給Glide去載入,只需要簡單處理一下即可,處理如下 new ViewTarget<CustomView, Bitmap>(customView的例項) 或者new CustomViewTarget<CustomView, Bitmap>(customView的例項) 然後複寫Target的onResourceReady方法即可拿到bitmap,支援判斷是否來自快取 把bitmap傳遞給自定義view顯示即可
遇到的一些反常現象
現象1:使用Glide V3.7.5 + 根據控制元件尺寸大小載入bitmap時的反常現象
支援按照控制元件的尺寸載入bitmap之後,載入出來的bitmap還是比控制元件尺寸大,日誌:
2020-09-25 20:35:16.269 13135-13771/? V/Downsampler: Decoded [300x400] RGB_565 (240000) from [300x400] image/webp with inBitmap [300x400] RGB_565 (240000) for [262x341], sample size: 1, density: 0, target density: 0, thread: fifo-pool-thread-4, duration: 21.315106可以看出來是給尺寸為 [262x341]的控制元件載入的,結果解碼後的尺寸是[300x400],原因是什麼呢?
解碼之前會先測量出bitmap原始的尺寸,此時原始尺寸是[300x400] 根據原始尺寸和目標尺寸(控制元件尺寸)計算取樣的比例 此處取樣的比例為1,所以最後解碼出來的bitmap尺寸就是原始尺寸[300x400] 類似的有(左邊是解碼後的bitmap尺寸,右邊是控制元件的尺寸)
[300x400] → [132x176](這個原始尺寸是[300x400]壓縮比是1,垃圾,沒有最佳化)
[300x400] → [165x2088](注意這個控制元件的高度是2088,而不是280)
[150x200] → [132x176] (這個原始尺寸是[300x400]壓縮比是2,是一個不錯的最佳化效果)最佳化點:使用Glide V4.9.0 + 根據控制元件尺寸大小載入bitmap時的效能提升
對比上文低版本的日誌,發現高版本確實有顯著的提升,基本上都非常接近控制元件的尺寸。
高版本上的表現如下:(左邊是解碼後的bitmap尺寸,右邊是控制元件的尺寸)
[132x176] → [132x176] (這個原始尺寸是[300x400]壓縮比是2,是一個不錯的最佳化效果,而且比以前尺寸更小) duration: 10.454532
[121x161] → [121x161] (這個原始尺寸是[300x400]壓縮比是2,是一個不錯的最佳化效果,而且比以前尺寸更小) duration:10.167657
[358x477] → [358x470](這個原始尺寸是[300x400]壓縮比是1,圖片被拉伸了,這是控制元件比圖片尺寸大的情況) duration: 21.670575
[321x428] → [318x428](這個原始尺寸是[300x400]壓縮比是1,圖片被拉伸了,這是控制元件比圖片尺寸大的情況) duration: 29.638752999999998高版本上的這種“反常"的高效能是怎麼做到的呢?值得研究一下。
具體原理參見:
https://blog.csdn.net/weixin_34416649/article/details/91372458
現象2:書城Tab切換頻道 + ViewPager#offsetPageLimie(1) + Fragment#onDestroy + Glide回收記憶體
這種場景下,如果頻繁切換頻道會導致明顯的卡頓。
卡頓原因是Fragment會被重建。
而且Glide回收記憶體效果不明顯。
分類Ttab也有同樣的問題。
這個現象和預期的不一樣,預期的是:記憶體會有明顯下降。
遺留的問題
針對漫畫的定製,目前漫畫使用的還是Volley的memoryCache,這部分有很大最佳化空間 專案中一些老程式碼仍然在使用Volley的memoryCache,這部分程式碼需要修改成使用Glide管理
iReader使用Glide更新說明
Glide是從TAG V4.10.0開始遷移到了Android X的。
iReader是從7.9.0版本開始引入的Glide,引入的版本是V3.5.2
iReader從7.10.0版本開始在AndroidManifest.xml中加入了全域性配置類
a. 設定了快取路徑和大小
b. 預設圖片格式565
c. 網路請求客戶端改成了OkHttp
iReader從7.38.0版本開始升級Glide版本到V4.9.0,後續iReader遷移到Android X之後,再升級到更新的版本上去
iReader本次升級新增特性有
根據控制元件尺寸按需載入圖片,避免直接載入原始尺寸 載入時context繫結的是當前頁面的Fragment,方便生命週期管理 頁面關閉後及時回收頁面佔用的記憶體 釋放該頁面佔用的網路請求物件,回收bitmap資源並放到快取池 移除快取池中不再使用的資源,減少記憶體 修復了一批記憶體洩露問題 系統可分配的記憶體不足時,及時回收記憶體,避免出現OOM App置於後臺後大幅度回收記憶體,待定 修改預設的快取池的大小,減少一半,待定 滑動列表時停止圖片載入,停止滑動後恢復圖片載入,待定
Glide從3.5.2升級到4.9.0有以下更新
v4.1.0預設情況下從O +的低記憶體裝置中刪除了BitmapPool,並減小了O +的預設BitmapPool大小(bb5c391)在Android O +中新增了對Bitmap.Config.HARDWARE的支援。 v4.0.0-RCO各種效能改進,包括大幅減少對影象進行下采樣時的垃圾處理,更智慧的預設磁碟快取策略以及載入GIF時的效能提高。改進了對檢視大小和佈局的處理,尤其是在RecyclerView中 v3.8.0更好地捕獲OOM異常導致的崩潰 bug修復 Glide4.11.0版本值得期待的新特性(目前iReader使用的版本是4.9.0)
在應用程式中不經常使用Glide時,為Glide的執行緒新增執行緒超時以減少記憶體 在資源ID快取鍵(1b391c4)中包括晝/夜模式。
新增可以在Glide上手動呼叫的API以清除記憶體,尤其是在後臺執行應用程式的情況下
在M +(525e7ba)上在後臺釋放更多記憶體
Glide原理介紹
debug模式下檢視日誌,開啟如下命令即可。
adb shell setprop log.tag.Glide VERBOSE
adb shell setprop log.tag.Engine VERBOSE
adb shell setprop log.tag.DecodeJob VERBOSE
adb shell setprop log.tag.MemorySizeCalculator VERBOSE
adb shell setprop log.tag.GlideProfile VERBOSE
adb shell setprop log.tag.GlideRequest VERBOSE
adb shell setprop log.tag.CustomViewTarget VERBOSE
adb shell setprop log.tag.LruBitmapPool VERBOSE
adb shell setprop log.tag.Downsampler VERBOSE
adb shell setprop log.tag.RequestTracker VERBOSE
adb shell setprop log.tag.TransformationUtils VERBOSE一、載入網路圖片、解碼bitmap
以bitmap格式載入網路圖片成功後的回撥順序從上到下依次是:
DecodeJob#run#runWrapped
DecodeJob#runGenerators...INITIALIZE...
...
DecodeJob#runGenerators(SWITCH_TO_SOURCE_SERVICE)
DataCacheGenerator#startNext
OkHttpGlideUrlFetcher#loadData
OkHttpGlideUrlFetcher#onResponse
ByteBufferFileLoader#ByteBufferFetcher#loadData
DataCacheGenerator#onDataReady
SourceGenerator#onDataFetcherReady
bitmap圖片decode的順序從上到下依次是:
DecodeJob#decodeFromRetrievedData
DecodeJob#decodeFromData
DecodeJob#decodeFromFetcher
DecodeJob#runLoadPath
LoadPath#load
LoadPath#loadWithExceptionList
DecodePath#decode
DecodePath#decodeResources
DecodePath#decodeResourcesWithList
ByteBufferBitmapDecoder#decode
Downsampler#decode詳細的decode過程參見Downsampler#decode方法。
二、解碼生成bitmap檔案,參與bitmap解碼的主要類和方法有:
DecodeJob#decodeFromFetcher
DecodePath#decodeResourceWithList
ByteBufferBitmapDecoder#decodeGlide使用model類下載圖片成功後會拿到一個InputStream,在對InputStream解碼之前會做一些最佳化。
從IO中解碼一個bitmap檔案,透過BitmapFactory.decodeStream(is, null, options); 具體實現參見Downsampler#decodeFromWrappedStreams
在解碼之前Glide額外做了如下最佳化:
1. 嘗試向下取樣,options.inSampleSize = powerOfTwoSampleSize;
2. 調整bitmap縮放比例
3. 調整bitmap的畫素密度options.inDensity
4. 給options.inBitmap設定了一個可複用的bitmap。Glide做的額外最佳化就是給options.inBitmap設定了一個可複用的bitmap。
因為一旦在Options中設定inBitmap,解碼方法將嘗試在載入內容時重用此bitmap。具體使用方式是:
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
而這個bitmap是透過一個bitmap pool來管理並獲取的。bitmap pool的具體實現參見LruBitmapPool#getDirty。
5. 自動校正顯示角度三、Glide V3.5.2和V4.9.0解碼圖片時裁剪功能對比
V3.5.2版本解碼bitmap的原理
解碼出bitmap的原始寬、高
計算bitmap的取樣率
用請求的寬比bitmap原始的寬sourceWidth,都是int型別
用請求的高比bitmap原始的高sourceHeight,都是int型別
取兩者中的最小值,即為inSampleSize
inSampleSize引數取值應該為2的冪,即1、2、4、8等,若給的值不為2的冪,則會取一個比給的值小的最大的2的冪來代替。
例如inSampleSize引數取值為10,則會用8來代替。但這個結論並非在所有系統上成立,因此此處應該嚴格控制,否則會得到意想不到的結果。
選取合適的顏色通道,如果圖片有alpha通道,即便明確使用RGB_565也無效,Glide內部自動轉成ARGB_8888
inBitmap複用+BitmapFactory.decodeStream即可獲取到目標bitmap
V4.9.0版本解碼bitmap的原理
- 解碼出bitmap的原始寬、高
- 計算bitmap的取樣率
- 用請求的寬requestWidth比bitmap原始的寬sourceWidth,都是float型別
- 用請求的高requestHeight比bitmap原始的高sourceHeight,都是float型別
- 取兩者中的最大值,得到一個縮放因子exactScaleFactor,float型別
- 使用scaleFactor和原始寬sourceWidth、高sourceHeight計算出目標寬outWidth、高outHeight,結算結果四捨五入,使得計算出的目標寬outWidth、高outHeight是int型別
- 使用原始寬/目標寬sourceWidth/outWidth;原始高/目標高sourceHeight/outHeight,都是int型別。得到寬、高的縮放比widthScaleFactor、heightScaleFactor
- 取widthScaleFactor、heightScaleFactor的最小值,得到一個int型別的縮放比scaleFactor
- 根據scaleFactor獲取inSampleSize的值。inSampleSize引數取值應該為2的冪,即1、2、4、8等,若給的值不為2的冪,則會取一個比給的值小的最大的2的冪來代替。演算法同V3.5.2。
- 根據不同的圖片型別處理略有不同,以PNG為例。用bitmap原始的寬sourceWidth/(float)inSampleSize結果向下取整,得到int型別的寬powerOfTwoWidth。同樣的處理得到powerOfTwoHeight。
- 再計算一次縮放比,步驟同a、b、c。只不過這次使用powerOfTwoWidth代替requestWidth,powerOfTwoHeight代替requestHeight。得到一個縮放因子adJustedScaleFactor,float型別。
- 使用adJustedScaleFactor的值來計算options#inTargetDensity和options#inDensity。如果這兩個值可以使用則會進一步壓縮圖片的尺寸。注意:這一步就是效能上高版本優於低版本的地方。
- 選取合適的顏色通道,如果圖片有alpha通道,即便明確使用RGB_565也無效,Glide內部自動轉成ARGB_8888
- inBitmap複用+BitmapFactory.decodeStream即可獲取到目標bitmap
