RecyclerView性能優化實戰

在Android中RecyclerView的使用隨處可見,它的性能優化程度跟用戶體驗息息相關。

性能優化實戰的例子如下,是獲取手機所有已安裝app列表:


RecyclerView的一些優化方案和使用技巧:

  • recyclerView.setHasFixedSize(true)

當Item的高度如是固定的,設置這個屬性為true可以提高性能,尤其是當RecyclerView有條目插入、刪除時性能提升更明顯。RecyclerView在條目數量改變,會重新測量、布局各個item,如果設置了setHasFixedSize(true),由于item的寬高都是固定的,adapter的內容改變時,RecyclerView不會整個布局都重繪。

 void onItemsInsertedOrRemoved() {
   if (hasFixedSize) layoutChildren();
   else requestLayout();
}
  • 使用getExtraLayoutSpace為LayoutManager設置更多的預留空間

在RecyclerView的元素比較高,一屏只能顯示一個元素的時候,第一次滑動到第二個元素會卡頓。

RecyclerView (以及其他基于adapter的view,比如ListView、GridView等)使用了緩存機制重用子 view(即系統只將屏幕可見范圍之內的元素保存在內存中,在滾動的時候不斷的重用這些內存中已經存在的view,而不是新建view)。

這個機制會導致一個問題,啟動應用之后,在屏幕可見范圍內,如果只有一張卡片可見,當滾動的時 候,RecyclerView找不到可以重用的view了,它將創建一個新的,因此在滑動到第二個feed的時候就會有一定的延時,但是第二個feed之 后的滾動是流暢的,因為這個時候RecyclerView已經有能重用的view了。

        val linearLayoutManager: LinearLayoutManager = object : LinearLayoutManager(applicationContext, LinearLayoutManager.VERTICAL, false) {
            override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
                return 300
            }
        }
        recyclerView.layoutManager = linearLayoutManager
  • 避免創建過多監聽器

onCreateViewHolder 和 onBindViewHolder 對時間都比較敏感,盡量避免繁瑣的操作和循環創建對象。例如創建 OnClickListener,可以全局創建一個。同時onBindViewHolder調用次數會多于onCreateViewHolder的次數,如從RecyclerViewPool緩存池中取到的View都需要重新bindView,所以我們可以把監聽放到CreateView中進行。

優化前:
注意,反復滑動列表,會一直調用onBindViewHolder方法,所以這里會一直創建OnClickListener對象。

    override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
        holder.ivIcon?.background = context.packageManager.getActivityIcon(datas[position].intent)
        holder.tvName?.text = datas[position].name
        holder.tvPkg?.text = "包名:" + datas[position].pkg

        holder.itemView.setOnClickListener(object: OnClickListener {
            override fun onClick(v: View?) {
                context.startActivity(datas[position].intent)
            }
        })
    }

優化后:

    class AppViewHolder(itemView: View): ViewHolder(itemView) {
        var ivIcon: ImageView?= null
        var tvName: TextView?= null
        var tvPkg: TextView?= null

        init {
            ivIcon = itemView.findViewById(R.id.iv_icon)
            tvName = itemView.findViewById(R.id.tv_name)
            tvPkg = itemView.findViewById(R.id.tv_pkg)
            itemView.setOnClickListener(onClickListener)
        }

        var onClickListener: OnClickListener = object: OnClickListener {
            override fun onClick(v: View?) {
                
            }
        }
    }
數據處理與視圖綁定分離

RecyclerView的 bindViewHolder方法是在UI線程進行的,如果在該方法進行耗時操作,將會影響滑動的流暢性。
比如:

mTextView.setText(Html.fromHtml(data).toString());

這里的 Html.fromHtml(data) 方法可能就是比較耗時的,存在多個
TextView 的話耗時會更為嚴重,這樣便會引發掉幀、卡頓,而如果把這
一步與網絡異步線程放在一起,站在用戶角度,最多就是網絡刷新時間稍
長一點。

局部刷新

可以用一下一些方法,替代notifyDataSetChanged,達到局部刷新的目的。notifyDataSetChanged會觸發所有item的detached回調再觸發onAttached回調。

notifyItemChanged(int position)
notifyItemInserted(int position)
notifyItemRemoved(int position)
notifyItemMoved(int fromPosition, int toPosition) 
notifyItemRangeChanged(int positionStart, int itemCount)
notifyItemRangeInserted(int positionStart, int itemCount) 
notifyItemRangeRemoved(int positionStart, int itemCount) 
復用RecycledViewPool

在TabLayout+ViewPager+RecyclerView的場景中,當多個RecyclerView有相同的item布局結構時,多個RecyclerView共用一個RecycledViewPool可以避免創建ViewHolder的開銷,避免GC。RecycledViewPool對象可通過RecyclerView對象獲取,也可以自己實現。
如果LayoutManager是LinearLayoutManager或其子類,需要手動開啟這個特性: layout.setRecycleChildrenOnDetach(true)

        val recycledViewPool = recyclerView.recycledViewPool
        recyclerView1.setRecycledViewPool(recycledViewPool)
        recyclerView2.setRecycledViewPool(recycledViewPool)
使用DiffUtil局部刷新

DiffUtil是androidx.recyclerview.widget包下的一個工具類,當你的RecyclerView需要更新數據時,將新舊數據集傳給它,它就能快速告知adapter有哪些數據需要更新。就相當于如果改變了就對某個item刷新,沒改變就沒刷新,可以簡稱為局部刷新。

mAdapter.notifyDataSetChanged()有兩個缺點:
1.不會觸發RecyclerView的動畫(刪除、新增、位移、change動畫)
2.性能較低,畢竟是無腦的刷新了一遍整個RecyclerView , 極端情況下:新老數據集一模一樣,效率是最低的。

它會自動計算新老數據集的差異,并根據差異情況,自動調用以下四個方法

adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);

簡單使用DiffUtil,我們需要且僅需要額外編寫一個類。

class AticalDiff: DiffUtil.ItemCallback<ArticleItem>() {
    override fun areItemsTheSame(oldItem: ArticleItem, newItem: ArticleItem): Boolean {
        return oldItem.userId == newItem.userId
    }

    override fun areContentsTheSame(oldItem: ArticleItem, newItem: ArticleItem): Boolean {
        return (oldItem == newItem && oldItem.userId == newItem.userId && oldItem.link == newItem.link &&
                oldItem.title == newItem.title)
    }

}

使用方式:
adapter繼承androidx.recyclerview.widget.ListAdapter包下的ListAdapter,并在構造方法中傳入自定義的DiffUtil.ItemCallback,代碼如下:

class Adapter(val context: Context): ListAdapter<ArticleItem, Adapter.MyViewHolder>(AticalDiff()) {

}
優化滑動操作

如果RecyclerView加載很多大圖,快速滑動卡頓解決方案:
考慮滾動的時候不做復雜布局及圖片的加載,盡量減少滾動過程中的耗時操作,這樣滾動停止的時候再加載可見區域的布局。

onScrollStateChanged的幾種狀態:

SCROLL_STATE_IDLE 屏幕停止滾動

SCROLL_STATE_DRAGGING 屏幕滾動且用戶使用的觸碰或手指還在屏幕上

SCROLL_STATE_SETTLING 由于用戶的操作,屏幕產生慣性滑動

Gilde同時也為我們提供了兩個方法:

resumeRequests() 開始加載圖片
pauseRequests() 停止加載圖片
public class AutoLoadRecyclerView extends RecyclerView {

    private void init() {
        addOnScrollListener(new ImageAutoLoadScrollListener());
    }

    //監聽滾動來對圖片加載進行判斷處理
    public class ImageAutoLoadScrollListener extends OnScrollListener {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
        }

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            switch (newState) {
                case SCROLL_STATE_IDLE: // The RecyclerView is not currently scrolling.
                    //當屏幕停止滾動,加載圖片
                    try {
                        if (getContext() != null) Glide.with(getContext()).resumeRequests();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                case SCROLL_STATE_DRAGGING: // The RecyclerView is currently being dragged by outside input such as user touch input.
                    //當屏幕滾動且用戶使用的觸碰或手指還在屏幕上,停止加載圖片
                    try {
                        if (getContext() != null) Glide.with(getContext()).pauseRequests();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                case SCROLL_STATE_SETTLING: // The RecyclerView is currently animating to a final position while not under outside control.
                    //由于用戶的操作,屏幕產生慣性滑動,停止加載圖片
                    try {
                        if (getContext() != null) Glide.with(getContext()).pauseRequests();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
            }
        }
    }
}

方案二:在正在滾動和停止滾動時給adapter設置是否滾動的屬性值,在adapter判斷值的狀態去過略加載圖片邏輯。


參考:
https://blog.csdn.net/GracefulGuigui/article/details/103646864
https://blog.csdn.net/yaojie5519/article/details/117174114

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容