從Glide.with(View)開始最佳化RecyclerView掉幀問題

山有木xi發表於2024-01-14

最近在專案中發現有個recyclerview滑動掉幀現象很嚴重,剛好最近要更新一個版本,就準備把這個問題最佳化了


一、Glide.with(View) 是低效的

Android 中對於圖片的處理 無非就是 Glide Picasso Fresco 這幾種 而在 imageView 直接顯示圖片 Glide 無疑是更好的選擇 Glide 的實現這裡就不贅述 Glide with 方法有很多種 例如

@NonNull
  public static RequestManager with(@NonNull Context context) {
    return getRetriever(context).get(context);
  }
 @NonNull
  @Deprecated
  public static RequestManager with(@NonNull Activity activity) {
    return with(activity.getApplicationContext());
  }
 @NonNull
  public static RequestManager with(@NonNull Fragment fragment) {
    return getRetriever(fragment.getContext()).get(fragment);
  }
....

recycler V iew item 如果要顯示圖片 我們一般拿到資料後 會將資料直接放入 adapter 然後在進行圖片處理 而在 adapter 中又不應該持有 activity fragment context 所以就會呼叫 Glide.with(View)

  @NonNull
  public static RequestManager with(@NonNull View view) {
    return getRetriever(view.getContext()).get(view);
  }

Glide 中關於這個方法的解釋是

其中有一句 This method may be inefficient aways and is definitely inefficient for large hierarchies. Consider memoizing the result after the View is attached or again, prefer the Activity and Fragment variants whenever possible. ”大致的意思就是 這個方法是低效的 除非萬不得已的情況下 ( 自定義 view ) 不然還是建議使用其他的 with () 方法

Glide 的求生慾望真的很強大 讓我們看看他的真實面目

1) 先是透過 findActivity 方法遞迴獲取 activity


2) 然後還要獲取 fragment 透過 findAllSupportFragmentsWithViews 方法遞迴呼叫獲取 Activity 中所有的 Fragment ,並以 fragment.getView() 作為鍵, fragment 作為值,以鍵值對的形式存入 map

總結起來就是 獲取 view activity 然後如果找到 view fragment 就使用 fragment 如果沒有找到 fragment 但是找到了 activity 就使用 activity 如果都沒找到就是用 application 同時生命週期也繫結在 application

 

其實如果一個 item 中的圖片資源少的話 影響倒是也不大 但是我們的那個 recycler V iew 中每個 item 都要載入好幾個 imageView 量變引發質變了屬於是

所以採用回撥的方式將 Adapter 中的 Glide 的操作放在 A ctivity / Fragment 中進行 對於效能來說就好了很多

二、簡化佈局的層級

其實首先排查的是佈局的層級 因為 ui 比較複雜 所以佈局這塊巢狀的層級比較多 這裡把該省的都省下來了 例如 LinearLayout 中巢狀了 Fr amlayout Fr amlayout 中又巢狀了 ConstraintLayout

三、 儘量使用LinearLayout FrameLayout

這當然是因為 LinearLayout FrameLayout 的效能足夠優秀 在效能對比如下

RelativeLayout 舉例 RelativeLayout 會讓子 View 呼叫 2 onMeasure LinearLayout 在有 weight 時, 會呼叫子View2 onMeasure RelativeLayout 的子 View 如果高度和 RelativeLayout 不同,會導致 RelativeLayout onMeasure() 方法中做橫向測量時,縱向的測量結果尚未完成,只好暫時使用自己的高度傳入子 View 系統。而父 View 給子 View 傳入的值也沒有變化就不會做無謂的測量的最佳化會失效


四、 滑動時不載入,停止時載入

其實到上面那一步之後 掉幀問題已經最佳化了很多 使用者正常上下滑動是沒有感覺到上面卡頓 但是 如果快速的較長時間的滑動依舊會出現卡頓 所以還需要繼續最佳化。 這個時候就準備從每個 item載入多個圖片這塊入手

主要思路是 當使用者滑動時停止載入圖片資源

Glide.with(this).pauseRequests()

在使用者停止滑動時載入圖片

Glide.with(this).resumeRequests()

體驗起來確實爽多了,不管怎麼快速滑動都是OK 但是又有新的問題 使用者的體驗就很差 因為滑動時看到的都是未載入的空白圖片 還得接著最佳化

前面說到 到第三步時 我們正常滑動其實已經不會卡頓了 無非就是要避免使用者快速滑動的問題 那我們只要判斷使用者滑動時速度 當速度過快時 停止載入 速度慢下來恢復載入 就可以完美解決問題 參考程式碼如下

mBinding.rvList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
   override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
       super.onScrollStateChanged(recyclerView, newState)
       if (newState != SCROLL_STATE_IDLE && mScrolled) {
           Glide.with(this).pauseRequests()
       } else {
           Glide.with(this).resumeRequests()
       }
   }
   override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
       super.onScrolled(recyclerView, dx, dy)
       mScrolled = dy > 80 || dy < -55
   }
})

五、 其他最佳化操作

參考了下別人的方案 如果後續還需要最佳化應該是還有最佳化的空間

1、 加大RecyclerView 的快取,用空間換時間,來提高滾動的流暢性。

mBinding.rvList.setItemViewCacheSize(20);
mBinding.rvList.setDrawingCacheEnabled(true);
mBinding.rvList.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);

2、 增加RecyclerView 預留的額外空間

object : LinearLayoutManager(this) {
   override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
       return size
   }
}

3、 設定固定高度

如果item 高度是固定的話,可以使用 RecyclerView.setHasFixedSize(true); 來避免 requestLayout 浪費資源。

 

4 極端情況下可以採用減少 xml 檔案 inflate 時間的方式

xml 檔案包括: layout drawable xml xml 檔案 inflate ItemView 是透過耗時的 IO 操作。可以使用程式碼去生成佈局,即 newView() 的方式。這種方式是比較麻煩,但是在佈局太過複雜,或對效能要求比較高的時候可以使用。


來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/69917874/viewspace-3003768/,如需轉載,請註明出處,否則將追究法律責任。

相關文章