RecyclerView優化總結

本文主要總結下RecyclerView使用中的一些優化措施,先了解一些相關的知識點。

一、鋪墊

1.RecycleBin

RecycleBin是一個簡單的對象複用池,它通過SparseArray<Queue>類型的變量實現,非常像HashMap,注意它也不是線程安全的,只能在單獨線程中進行增刪操作使用。對象複用池在Android優化中有很多可用的地方。

public class RecycleBin<T> {
    private final SparseArray<Queue<T>> map;

    public RecycleBin() {
        this.map = new SparseArray<Queue<T>>();
    }

    public void put(int recycleKey, T obj) {
        Queue<T> queue = map.get(recycleKey);
        if (queue == null) {
            queue = new LinkedList<T>();
            map.put(recycleKey, queue);
        }
        queue.add(obj);
    }

    public T get(int recycleKey) {
        Queue<T> queue = map.get(recycleKey);
        if (queue != null) {
            T object = queue.poll();
            if (queue.isEmpty()) {
                map.remove(recycleKey);
            }
            return object;
        }
        return null;
    }
}

2.LayoutInflater.inflate()

這個方法用來加載xml佈局並進行解析生成View,這個流程比較耗時,通常在數十毫秒的量級。

3.RecyclerView.Adapter的回調方法

除了onCreateViewHolder()和onBindViewHolder()外,RecyclerView.Adapter還有幾個需要注意的回調方法。

3.1 onViewDetachedFromWindow() 和onViewAttachedToWindow()
public void onViewDetachedFromWindow(@NonNull VH holder) {
}

Called when a view created by this adapter has been detached from its window.
Becoming detached from the window is not necessarily a permanent condition; the consumer of an Adapter’s views may choose to cache views offscreen while they are not visible, attaching and detaching them as appropriate.

onViewDetachedFromWindow()是ViewHolder的itemView從Window中移除時的回調,此時可以暫停itemView內部動畫、釋放不必要的資源、暫停圖片加載等。與它對應的是

public void onViewAttachedToWindow(@NonNull VH holder) {
}

Called when a view created by this adapter has been attached to a window.
This can be used as a reasonable signal that the view is about to be seen by the user. If the adapter previously freed any resources in onViewDetachedFromWindow those resources should be restored here.

它會在ViewHolder的itemView被添加到Window時被回調。

3.2 onViewRecycled()
public void onViewRecycled(@NonNull VH holder) {
}

Called when a view created by this adapter has been recycled.
A view is recycled when a RecyclerView.LayoutManager decides that it no longer needs to be attached to its parent RecyclerView. This can be because it has fallen out of visibility or a set of cached views represented by views still attached to the parent RecyclerView. If an item view has large or expensive data bound to it such as large bitmaps, this may be a good place to release those resources.
RecyclerView calls this method right before clearing ViewHolder’s internal data and sending it to RecycledViewPool. This way, if ViewHolder was holding valid information before being recycled, you can call RecyclerView.ViewHolder.getAdapterPosition() to get its adapter position.

這個方法很重要,從方法描述中加粗的部分可以看到,如果ViewHolder的itemView綁定了大量佔用內存的數據,可以在這裏釋放這些資源。onViewRecycled(VH holder)回調後,holder會被添加到RecyclerView的RecycledViewPool中。

4.RecycledViewPool

RecycledViewPool是RecyclerView多級緩存中的最後一級緩存,會針對每種viewType存儲至多5個ViewHolder。假設列表中先展示viewType爲1的item 5個,向上滾動再展示viewType爲2的item 5個,如果viewType爲2的item完全將屏幕佔滿還多,會導致viewType爲1的5個item都被放置到RecycledViewPool中,如果這5個item仍持有圖片引用,那會造成內存的巨大浪費。特別是在Android 8.0以下的手機上,圖片內存都存儲在虛擬機堆上,很容易內存喫緊。另外由於RecycledViewPool針對每種ViewType進行ViewHolder緩存,所以適當減少ViewType的數目,有時也會產生正面的作用。
RecycledViewPool的實現與RecycleBin實現很類似。

    public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;

        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();

        public void clear() {
            for (int i = 0; i < mScrap.size(); i++) {
                ScrapData data = mScrap.valueAt(i);
                data.mScrapHeap.clear();
            }
        }

        @Nullable
        public ViewHolder getRecycledView(int viewType) {
            final ScrapData scrapData = mScrap.get(viewType);
            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                return scrapHeap.remove(scrapHeap.size() - 1);
            }
            return null;
        }

        public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }
            
            scrap.resetInternal();
            scrapHeap.add(scrap);
        }

    }

有了這些鋪墊,可以開始正文了。

二、增加複用

1.如果公有佈局,比如個人信息展示、交互操作、發表評論等,可以抽象出單獨View,在多個ViewHolder中複用;在這裏插入圖片描述
對於公共的操作可以抽象相應的接口和Utils類,減少代碼冗餘,比如聲明一個進行交互的接口,讓需要進行交互操作的ViewHolder實現該接口,並通過Utils方法進行對控件進行刷新。

interface IFeedInteract {
    fun updateLike(isFavorite: Boolean, favoriteCount: Int) // 喜歡

    fun updateFollow(isFollowing: Boolean, isFollower: Boolean) // 關注

    fun updateComment(comments: Int) // 評論

    fun updateShare(shareCount: Int) // 分享

    fun updateCollect(isCollected: Boolean, collectCount: Int)

    fun updateCircleWorkTop(isCircleWorkTop: Boolean)

    fun updateHotComments(post: PostCard)
}

2.動態添加的佈局的複用,如上圖中的評論列表,不能事先(寫佈局文件時)確定評論的數目,只能在數據與視圖綁定時才能確定(onBindViewHolder())。因此,簡單的做法是聲明一個評論的Container控件,按需動態創建多個單條評論View,添加到Container中。但如果每次綁定操作都進行創建,一則耗時長,二則生成很多臨時對象,增加了GC的頻率。這時可以使用第一部分介紹的RecycleBin類。在ViewHolder的onViewRecycled()和onBindViewHolder()中,將Container的子View都移除,並放入RecycleBin中,等添加評論時,先從RecycleBin中查詢是否有評論的子View,如果有,則直接取出使用,如果沒有,才使用inflate()進行創建。
具體實現
2.1 先聲明一個OnViewRecycledListener的接口

public interface OnViewRecycledListener {
    void onViewRecycled();
}

2.2 在Adapter.onViewRecycled()回調中,如果是OnViewRecycledListener,則執行其
onViewRecycled()方法

    override fun onViewRecycled(holder: BaseViewHolder<FeedCard>) {
        super.onViewRecycled(holder)
        if (holder is OnViewRecycledListener) {
            holder.onViewRecycled()
        }
    }

2.3 需要進行回收操作的ViewHolder,實現OnViewRecycledListener接口

    override fun onViewRecycled() {
        recycleContainer()
    }
    override fun onBindViewHolder(holder: BaseViewHolder<FeedCard>, position: Int) {
        recycleContainer()
        mContainerView.addComments(commentList)
    }
    private fun recycleContainer() {
        if (mContainerView.childCount > 0) {
            FeedAdapterHelper.recycleViewGroup(pageLifecycle, mContainerView, recycleBin)
            mContainerView.removeAllViews()
        }
    }

其中

    @JvmStatic
    fun recycleViewGroup(pageLifecycle: PageLifecycle?, containerView: ViewGroup, recycleBin: RecycleBin<View>?) {
        val childCount = containerView.childCount
        for (childIndex in 0 until childCount) {
            val child = containerView.getChildAt(childIndex)
            if (child is ViewGroup) {
                val resId = child.getTag(R.id.cache_layout_res_id)
                if (resId is Int) {
                    val grandChildCount = child.childCount
                    for (grandChildIndex in 0 until grandChildCount) {
                        val grandChild = child.getChildAt(grandChildIndex)
                        if (grandChild is ImageView) {
                            ImageLoaderUtils.clearView(pageLifecycle, grandChild.context, grandChild)
                        }
                    }

                    recycleBin?.put(resId, child)
                }
            } else if (child is ImageView) {
                val resId = child.getTag(R.id.cache_layout_res_id)
                if (resId is Int) {
                    ImageLoaderUtils.clearView(pageLifecycle, child.context, child)
                    recycleBin?.put(resId, child)
                }
            }
        }
    }

至於addComments(),對commentList進行遍歷處理

    private fun getFromCacheOrInit(layoutResId: Int): View {
        var view = mRecycleBin?.get(layoutResId)
        if (view == null) {
            view = LayoutInflater.from(context).inflate(layoutResId, this, false)
        }
        view?.setTag(R.id.cache_layout_res_id, layoutResId)
        return view!!
    }

其實RecycleBin這套操作廣了使用的話,除了對動態佈局外,對公有的佈局可以進行復用,進一步減少對象創建和銷燬次數,和降低內存使用。

三、優化加載速度

RecyclerView首次添加數據時,需要創建多個ViewHolder,此時伴隨着這些ViewHolder的itemView的批量創建,會佔用UI線程大量的時間,導致卡頓。一個措施是使用AsyncLayoutInflater提前異步創建itemView,並緩存起來,當onCreateViewHolder回調時,先從緩存中獲取是否有對應viewType的itemView,如果有,則直接使用,如果沒有,才走正常的流程。具體參見使用AsyncLayoutInflater預加載,加快列表渲染
對於第二部分所說的動態添加的佈局,也可以提前加載創建。

四、優化內存

Android內存佔用主要是在圖片上,同樣使用第二部分onViewRecycled()回調,“清除”被回收itemView持有的圖片內存引用。
需要注意的是,這裏的“清除”並不是真的清除,通常我們是通過圖片加載框架加載圖片,而圖片加載框架都有多級緩存,我們清理了itemView持有的圖片內存引用,並不會直接將這部分內存給釋放,而是交給了圖片加載框架的內存緩存來管理,如果應用內存緊張,圖片加載框架會回收這部分內存;如果用戶往回滾動,去瀏覽剛剛看過的圖片,有可能還在內存緩存中,此時並不會影響到圖片的加載速度。
另外,對於Glide而言,清除View佔用的圖片引用,是通過RequestManager的clear(View)方法,並不是通過ImageView.setImageBitmap(null)等方法。
還有Glide加載圖片時,在Fragment的View,with()參數用fragment,在Activity中的View,with()參數使用Activity,避免使用with(applicationContext)或with(View)方法。具體參見說一說Glide.with()
Glide的ViewTarget和CustomViewTarget中還有一個clearOnDetach()方法,當View從Window中detach時會暫停請求,當重新attach時會恢復請求,但只是個實驗方法,太過於激進。

五、刷新優化

1.使用局部刷新
使用notifyItemInserted()、notifyItemRemoved()、notifyItemChanged()等方法替代notifyDatasetChanged()
ViewHolder局部刷新使用payload

   @JvmStatic
   override fun onBindViewHolder(holder: BaseViewHolder<FeedCard>, position: Int, payloads: MutableList<Any>) {
        var updateByPayload = false
        for (payload in payloads) {
            if (payload is String && holder is BaseFeedViewHolder) {
                updateByPayload = true
                when (payload) {
                   
                }
            }
        }
        if (!updateByPayload) {
            onBindViewHolder(holder, position)
        }
    }

需注意,有可能連續操作,進而發起多個payload,所以需要對payloads進行循環,進行相應處理。
具體參見RecyclerView局部刷新和原理介紹

2.對於RecyclerView佈局屬性爲match_parent或固定尺寸,可以設置setFixedSize(true)。

public void setHasFixedSize(boolean hasFixedSize) {
    mHasFixedSize = hasFixedSize;
}

RecyclerView can perform several optimizations if it can know in advance that RecyclerView’s size is not affected by the adapter contents. RecyclerView can still change its size based on other factors (e.g. its parent’s size) but this size calculation cannot depend on the size of its children or contents of its adapter (except the number of items in the adapter).
If your use of RecyclerView falls into this category, set this to true. It will allow RecyclerView to avoid invalidating the whole layout when its adapter contents change.

如果RecyclerView提前知道它對尺寸不受Adapter的內容發生改變,可以提前做一些優化。

3.減少佈局層次

4.ViewHolder的itemView佈局避免使用ConstraintLayout

5.處於SCROLL_STATE_SETTLING狀態時,暫停圖片加載。

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