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状态时,暂停图片加载。

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