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線程進行的,如果在該方法進行耗時操作,將會影響滑動的流暢性。

局部刷新

可以用一下一些方法,替代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)
用SortedList實現添加刪除ItemView自動更新

我們在給RecyclerView的ArrayList<Item> data添加一個Data數據時,一般需要自己通知RecyclerView更新。Android Support Lirbrary中提供了一個SortedList工具類,它是一個有序列表,數據變動時會回調SortedList.Callback中方法。

class AppAdapter(val context: Context): RecyclerView.Adapter<AppAdapter.AppViewHolder>() {
    private var datas: SortedList<AppInfo>?= null

    init {
        datas = SortedList(AppInfo::class.java, object: SortedListAdapterCallback<AppInfo>(this) {
            override fun compare(o1: AppInfo, o2: AppInfo): Int {
                // 實現這個方法來定義Item的顯示順序
                if (o1.name.length>o2.name.length) {
                    return 1
                } else if (o1.name.length<o2.name.length) {
                    return 1
                } else {
                    return 0
                }
            }

            override fun areContentsTheSame(oldItem: AppInfo, newItem: AppInfo): Boolean {
                // 比較兩個Item的內容是否一致,如不一致則會調用adapter的notifyItemChanged()
                return oldItem.pkg.equals(newItem.pkg)
            }

            override fun areItemsTheSame(item1: AppInfo, item2: AppInfo): Boolean {
                return item1.intent == item2.intent
            }
        })
    }

    fun setDatas(datas: ArrayList<AppInfo>) {
        this.datas?.addAll(datas)
        // 會通過SortedListAdapterCallback自動通知更新
    }
}

當數據發生改變時,例如刪除,增加等,只需直接對mDataList進行相應操作,無需關心mAdapter內數據顯示更新問題,不用再調用notifyDataChanged等函數,因爲SortedListAdapterCallback內的回調函數自動完成了。

使用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,我們需要且僅需要額外編寫一個類。

/**
 * 核心類 用來判斷 新舊Item是否相等
 */
public class DiffCallBack extends DiffUtil.Callback {
    private List<AppInfo> mOldDatas, mNewDatas;

    @Override
    public int getOldListSize() {
        return mOldDatas.size();
    }

    @Override
    public int getNewListSize() {
        return mNewDatas.size();
    }

    /**
     * 被DiffUtil調用,用來判斷 兩個對象是否是相同的Item。
     * @param oldItemPosition
     * @param newItemPosition
     * @return
     */
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName());
    }

    /**
     * 被DiffUtil調用,用來檢查 兩個item是否含有相同的數據
     * @param oldItemPosition
     * @param newItemPosition
     * @return
     */
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return mOldDatas.get(oldItemPosition).getPkg().equals(mNewDatas.get(newItemPosition).getPkg());
    }
}

使用方式:

        val diffResult = DiffUtil.calculateDiff(DiffCallBack(mProducts, newProducts))
        diffResult.dispatchUpdatesTo(adapter)
優化滑動操作

設置 RecyclerView.addOnScrollListener();來在滑動過程中停止加載的操作。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章