Android-RecyclerView原理

甄胡非
2023-12-01

用ListView实现一个列表

ListView是最简单的一种列表实现,通过Adapter可将数据转换为视图。以下代码是ListView的一种典型使用方法

data class DemoItem(val text:String, val target: Class<*>)

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val list = ListView(this)
        setContentView(list)
        list.adapter = DemoListAdapter(this, getDemo())
 }
 
 class DemoListAdapter(val context: Context, val data: List<DemoItem>): BaseAdapter() {
            override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
                val text = TextView(context)
                text.height = 150
                text.gravity = gravity.center is For Sale
                text.textSize = 25.0F
                text.setPadding(20, 30, 20, 30)
                text.text = data[position].text
                text.setOnClickListener {
                    context.startActivity(Intent(context, data[position].target))
                }
                return text
            }

            override fun getItem(position: Int): Any {
                return data[position]
            }

            override fun getItemId(position: Int): Long {
                return position.toLong()
            }

            override fun getCount(): Int {
                return data.size
            }
        }

ListView的缓存机制

要用ListView实现一个列表,最重要的是实现一个BaseAdapter的子类,实现getItem、getItemId、getCount以及getView这几个抽象方法。getView最重要,是根据当前位置的返回一个特定的View以添加到ListView中。上面代码每次调用getView都会新创建一个视图,对性能影响较大,可以用以下方式优化

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
                if (convertView == null || convertView !is TextView) {
                    val text = TextView(context)
                text.height = 150
                text.gravity = gravity.center is For Sale
                text.textSize = 25.0F
                text.setPadding(20, 30, 20, 30)
                text.text = data[position].text
                text.setOnClickListener {
                    context.startActivity(Intent(context, data[position].target))
                }
                return text
                } else {
                    convertView.text = data[position].text
                    convertView.setOnClickListener {
                        context.startActivity(Intent(context, data[position].target))
                    }
                    return convertView
                }
            }


这里充分利用了ListView的利用机制,当滑动到特定位置,ListView会将当前postion位置的ItemView作为参数传给getView,我们就可以判断如果传入的ItemView不为null,就能复用这个ItemView,只需要更新数据,不需要再重新创建一个View了。

其实ListView也是有缓存机制的,ListView的父类AbsListView中有一个RecycleBin内部类,这个RecycleBin中有两个变量,分别是mCurrentScrap的List<View>和mScrapViews的List<View>[]。mCurrentScrap是一个缓存池,而mScrapViews是所有缓存池的集合。这是mScrapViews是个List的数组,这是因为需要为每种viewType创建一个缓存池,这个数组的长度就是getViewTypeCount的返回值,Adapter默认的viewTypeCount为1。当调用getView之前,会先尝试从缓存池中查找。

View getScrapView(int position) {
            ArrayList<View> scrapViews;
            if (mViewTypeCount == 1) {
                scrapViews = mCurrentScrap;
                int size = scrapViews.size();
                if (size > 0) {
                    return scrapViews.remove(size - 1);
                } else {
                    return null;
                }
            } else {
                int whichScrap = mAdapter.getItemViewType(position);
                if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
                    scrapViews = mScrapViews[whichScrap];
                    int size = scrapViews.size();
                    if (size > 0) {
                        return scrapViews.remove(size - 1);
                    }
                }
            }
            return null;
        }

代码逻辑很简单,如果只有一种viewType,则直接从缓存池的列表尾部取出,如果有多种viewType,则先判断当前postion的viewType是什么,然后获取这种viewType的缓存池,再从这个缓存池的尾部获取。

void addScrapView(View scrap) {
            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
            if (lp == null) {
                return;
            }
            // Don't put header or footer views or views that should be ignored
            // into the scrap heap
            int viewType = lp.viewType;
            if (!shouldRecycleViewType(viewType)) {
                return;
            }
            if (mViewTypeCount == 1) {
                mCurrentScrap.add(scrap);
            } else {
                mScrapViews[viewType].add(scrap);
            }
            if (mRecyclerListener != null) {
                mRecyclerListener.onMovedToScrapHeap(scrap);
            }
     }

添加到缓存也比较简单,基本上就是获取缓存的逆操作。这个被叫作二级缓存,ListView还有个被称作一级缓存的机制,其实原理比较简单,就是只缓存当前屏幕内的View,在滑动时,如果当前Adapter的数据未发生变化,就根据position从一级缓存中取,能取到就直接用,取不到于调用getView获取。

由上面分析可知ListView拥有两级缓存,首先会从一级缓存中取,如果取到了,则直接使用;取不到再从二级缓存中取,由于二级缓存的View上的数据可能发生了变化,因此将二级缓存取出来的View交由getView方法再处理一下,最上面使用的判断if (convertView == null)的复用机制其实就是为了配合二级缓存机制。

RecyclerView的优势

ListView已经有两层缓存机制了,为什么还要开发出一个RecyclerView再替代ListView呢?

首先,ListView在一次数据更新时会执行两次layout,具体原因是执行了两次performTraversals。其实有时候还不只调用了两次。这将会导致getView被调用多次,性能会大大受影响。为什么会调多次呢?这是因为ListView所在的ViewTree的路径上,有一层把高度(如果是纵向的ListView,横向的就是宽)设置成了wrap_content,这样在测量的时候,由于ItemView还未被加载,因此还不知道ListView的真正高度,但由于getView是layout触发的,因此需要用一次layout来触发ItemView的加载,当ItemView加载完成后,再来一遍测试最终确认ListView的高度。

再者ListView必须判断convertView是否为空,再结合ViewHolder方式来配合二级缓存机制才能真正使用二级缓存,其实这个过程写了非常多的模板代码,可以将ItemVie的创建过程与数据渲染过程分离,使用二级缓存其实只需要使用数据渲染过程即可。这也是RecyclerView的实现方式,用onCreateViewHolder来创建视图,用onBindViewHolder将数据绑定到视图上,使用缓存时可以只调用onBindViewHolder来更新数据。

ListView要实现没有动画效果,要实现ItemView的动画效果比较复杂,而RecyclerView已经为ItemView提供了动画效果的api。再者RecyclerView已经为ItemView的分隔线提供了统一的设置api,而ListView还需要在每个ItemView中设置。

ListView只支持全局刷新,如果在Feed流中每次都使用全局刷新,对性能的影响将会非常大,因为Feed流可以item数据会有几百甚至几千个。而RecyclerView提供了局部的增删改操作。

ListView拥有二级缓存,而RecyclerView拥有四级缓存,使用者对缓存的可定制性更强,可以在适应的环境下达到更好的缓存效果。

RecyclerView简单的使用方式

首先是在布局中添加一个RecyclerView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:layout_width="match_parent"
    android:layout_height="50dp">
    <TextView
        android:id="@+id/title"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="1"
        android:textSize="20sp"
        />
    <TextView
        android:id="@+id/summary"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_weight="3"
        android:paddingLeft="5dp"
        android:textSize="14sp"
        />
</LinearLayout>

定义每个Item的数据类以及Adapter

companion object {

        data class Item(val id: Int, val title: String, val summary: String)

        class DemoAdapter: RecyclerView.Adapter<DemoViewHolder>() {

            val list = LinkedList<Item>()

            fun add(newData: List<Item>) {
                val start = list.size
                list.addAll(newData)
                notifyItemRangeInserted(start, newData.size)
            }

            fun remove(start: Int, itemCount: Int) {
                val count = if (list.size > start + itemCount) {itemCount} else {list.size - start}
                if (list.size > start) {
                    val iterator = list.iterator()
                    var index = 0
                    while(iterator.hasNext()) {
                        if (index++ == start) {
                            iterator.next()
                            iterator.remove()
                        }
                        if (index == start + count) {
                            break
                        }
                    }
                }
                notifyItemRangeRemoved(start, count)
            }

            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DemoViewHolder {
                return DemoViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_demo, parent, false))
            }

            override fun getItemCount(): Int {
                return list.size
            }

            override fun onBindViewHolder(holder: DemoViewHolder, position: Int) {
                holder.title.text = list[position].title
                holder.summary.text = list[position].summary
            }
        }

        class DemoViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
            val title = itemView.findViewById<TextView>(R.id.title)
            val summary = itemView.findViewById<TextView>(R.id.summary)
        }
    }

设置RecyclerView的布局管理器以前Adapter

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_demo_recyclerview)
        recyclerview.setHasFixedSize(true)
        recyclerview.layoutManager = LinearLayoutManager(this)
        val demoAdapter = DemoAdapter()
        recyclerview.adapter = demoAdapter
        add.setOnClickListener {
            val newData = MutableList<Item>(10) { index ->
                Item(index + 1 + (demoAdapter.list.lastOrNull()?.id?:0),"title-${index + 1 + (demoAdapter.list.lastOrNull()?.id?:0)}", "summary-${index + 1 + (demoAdapter.list.lastOrNull()?.id?:0)}")
            }
            demoAdapter.add(newData)
        }

        remove.setOnClickListener {
            demoAdapter.remove(0, 5)
        }
    }

这里使用了RecyclerView的局部刷新api,使用局部刷新时,其他位置的ViewHolder不会执行到onCreateViewHolder和onBindViewHolder

RecyclerView源码分析

既然RecyclerView的刷新都是通过Adapter来触发的,那就先看看RecyclerView.Adapter的notify系列方法。查看其源码,发现notify系列方法都是直接调用了相应的mObservable的方法,这个mObservable是AdapterDataObservable类型的对象,每个Adapter在创建的时候就把这个mObservable对象给new出来了,Adapter还提供了register和unregister方法,看来这个AdapterDataObservable就是个可观察对象,在特定时机会触发监听回调,将所有的监听对象都执行一遍。再看AdapterDataObservable的notify系列方法的源码,几乎都是for循环注册的所有监听者对象,调用相应的on方法,也就是相当于提供了数据变更的监听。

当调用notify系列方法时,RecyclerView是能自动刷新的,RecyclerView肯定也是注册了数据变更监听的。搜索一下Adapter的registerAdapterDataObserver方法的使用地方,可以找到RecyclerView#setAdapterInternal中有使用到。

/**
     * Replaces the current adapter with the new one and triggers listeners.
     * @param adapter The new adapter
     * @param compatibleWithPrevious If true, the new adapter is using the same View Holders and
     *                               item types with the current adapter (helps us avoid cache
     *                               invalidation).
     * @param removeAndRecycleViews  If true, we'll remove and recycle all existing views. If
     *                               compatibleWithPrevious is false, this parameter is ignored.
     */
    private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
            boolean removeAndRecycleViews) {
        if (mAdapter != null) {
            mAdapter.unregisterAdapterDataObserver(mObserver);
            mAdapter.onDetachedFromRecyclerView(this);
        }
        if (!compatibleWithPrevious || removeAndRecycleViews) {
            removeAndRecycleViews();
        }
        mAdapterHelper.reset();
        final Adapter oldAdapter = mAdapter;
        mAdapter = adapter;
        if (adapter != null) {
            adapter.registerAdapterDataObserver(mObserver);
            adapter.onAttachedToRecyclerView(this);
        }
        if (mLayout != null) {
            mLayout.onAdapterChanged(oldAdapter, mAdapter);
        }
        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
        mState.mStructureChanged = true;
    }

RecyclerView中持有了一个mObserver监听者对象,在setAdapter的时候,调用了这个setAdapterInternal方法,将set进来的Adapter对象保存起来,并将mObserver注册给Adapter,在setAdapter方法的最后,调用了requestLayout,这里应该就是主动触发刷新的地方了。

RecyclerView的mObserver对象是RecyclerViewDataObserver类型的,继承自AdapterDataObserver。在RecyclerViewDataObserver的所有onXXXChanged系列方法中,都直接或间接地调用了requestLayout方法,而且都使用了mAdapterHelper来判断数据变化是否满足刷新条件,这也证明了前面所说的requestLayout是直接触发布局刷新的地方。

requestLayout会调用View的onMeasure和onLayout方法,首先看下onMeasure方法。

RecyclerView的onMeasure方法中调用了mLayout.onMeasure,是将onMeasure委托给LayoutManager来做。以LinearLayoutManager为例,未重写onMeasure方法,那就调用父类的,间接调用了RecyclerView的defaultOnMeasure。

再看看RecyclerView的onLayout的实现。直接调用了dispatchLayout方法,

void dispatchLayout() {
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

在dispatchLayoutStep2方法中,调用了mLayout.onLayoutChildren,这里是为子View进行布局。

onLayoutChildren方法中,首先计算了上下的剩余空间,然后调用fill方法进行布局。fill方法中有个while循环,这将会循环为每个item进行布局,每次循环时,都会判断下一个item是否还有剩余空间。真正为每个item进行布局的是layoutChunk方法,通过layoutState.next,获取到下一个item,将获取到了itemView添加到RecyclerView中,并测量itemView,调用itemView的measure方法。再对itemView进行布局,计算出itemView的left、top、right以及bottom,调用layoutDecoratedWithMargins,对itemView进行layout。

无疑,这个layoutState.next调用成为了一个非常重要的点,里面的实现是调用了RecyclerView.Recycler#getViewFromPosition,经常断点调试onCreateViewHolder和onBindViewHolder,会在调用栈中经常看到getViewFromPosition,因此这个getViewFromPositin就是itemView创建和数据绑定的起始点了。getViewFromPosition调到Recycler的Recycler的tryGetViewHolderForPositionByDeadline方法。

首先从mChangedScrap中取,这个mChangedScrap表示的是可以复用,但需要更新数据的ViewHolder缓存,前提条件是isPreLayout,表示的是有动画的预布局。

如果从mChangedScrap中获取不到,再调用getScrapOrHiddenOrCachedHolderForPosition从mAttachedScrap中获取,mAttachedScrap只有在scrapView中有add操作,条件是viewHolder被remove或者被设置为INVALID,或者holder没有update。从mAttachedScrap中获取到的viewHolder被设置FLAG_RETURNED_FROM_SCRAP的flag,表示不用更新数据,也就是不用执行onBindViewHolder。

getScrapOrHiddenOrCacheHolderFromPosition方法的第二轮操作是在dryRun为false(默认传进来是false)时将hidden状态的ViewHolder添加到scrap列表中,这时其实是为了复用做动画的ViewHolder。

第三轮操作是从mCachedViews这个缓存中取,mCachedViews也只有recycleViewHolderInternal这一个地方有add操作。可以看到这个mCachedViews的缓存容量默认只有2个,如果缓存超过了,就将第0个位置上的ViewHolder调用recycleCachedViewAt给remove并添加到缓存池中去。除了缓存容量要大于0,还需要ViewHolder的状态是REMOVE或UPDATE或ADAPTER_POSITION_UNKNOWN的,这个mCachedViews是为了缓存屏幕之外的ViewHolder的,比如滑动过程中,屏幕外的ViewHolder进入到屏幕内。

从getScrapOrHiddenOrCachedHolderForPosition方法也获取不到ViewHolder时,就调用getItemViewType方法,根据position获取当前位置的viewType,当Adapter的stableId被设置为true时,再调用getScrapOrCachedViewForId方法,这个stableId表示每种类型的每个Item拥有唯一id,因此就可以通过id来查询到ViewHolder,以此来复用这个ViewHolder,这也被当作从scrap中获取到复用的ViewHolder。

getScrapOrCachedViewForId也获取不到时,假如mViewCachedExtension不为null时,就调用getViewForPositionAndType来获取View,并根据这个View获取ViewHolder。这里的mViewCachedExtention是ViewCacheExtention类型的,这是RecyclerView对外提供的可自下定义缓存。

自定义缓存也无法获取到ViewHolder时,再调用getReccyledViewPool().getRecycledView,传入viewType,这就是大名鼎鼎的ViewHolder缓存池,也就是RecycledViewPool,可为每种ViewType缓存自下定义大小容量的缓存。因为在RecycleView中,每个ViewType表示一种布局结构,同一个ViewType表示布局结构完全相同,只是数据可能不同,因此ReccyledViewPool中取得的ViewHolder就不需要执行onCreateViewHolder,只需要调用onBindViewHolder重新绑定数据就可达到复用的目的。从ReccyledViewPool中取出来的ViewHolder调用了resetInternal方法,将很多状态字段重置,

从ViewHolder缓存池中也取不到时,最后就只能调用Adapter的onCreateViewHolder来新创建一个ViewHolder了。

这就是从缓存中复用或者创建ViewHolder的全过程,总结一下,这里一共用到了五个缓存,分别是mChagedScrap、mAttachedScrap、mCachedViews、mViewCacheExtention以及RecycledViewPool。其中mChagedScrap和mAttachedScrap表示还在屏幕内的缓存,但区别是mChagedScrap表示需要更新的ViewHolder,这两个由于在屏幕内,被称为一级缓存。mCachedViews表示屏幕外缓存的ViewHolder,容量默认为2,可通过setViewCacheSize来修改,通过FIFO来移除旧的ViewHolder,被称为二级缓存。然后是自定义缓存ViewCacheExtention被称为三级缓存,最后是ReccyledViewPool被称为四级缓存。

然后就是数组绑定过程了,判断ViewHolder的状态如果是未绑定状态(!holder.isBound),或者是需要更新(holder.needsUpdate)或者不可用状态(holder.isInvalid),就先获取最新位置offsetPosition,再调用tryBindViewHolderByDeadline,最后调到了Adapter的onBindViewHolder方法。数据绑定完毕之后,最后设置ViewHolder的根布局的LayoutParams,并将ViewHolder返回给LayoutManager


RecyclerView的滑动刷新

网上无数文章分析RecycleView的滑动原理,但很少有分析如何找到RecyclerView滑动触发刷新的过程的,其实所有的触发过程都可以使用同一个套路。之所以难找,是因为Android在处理UI时,可能会多次使用Handler来切换线程,有时候可能在UI线程还会使用Handler再来post一个Runnable以达到更丝滑的效果,如果一次性就把UI更新了,但可能这次更新比较耗时,会占用其他地方UI更新的CPU/GPU。

首先可以把断点打在Adapter的onBindViewHolder中,这样查看断点的调用栈,就可以找到调用onBindViewHolder的最终处。一般来说这样都可以找到线程或线程池的run中,实际是GapWorker的run方法调用了Adapter#onBindViewHolder。然后找一下有哪些地方可能会去执行GapWorker的run方法,再次通过上述方法,可以找到是GapWorker#postFromTraversal中,调用了recyclerView.post(this),来将GapWorker这个Runnable给post到Main线程去执行。继续找调用点栈底,发现是RecyclerView的onTouchEvent的MotionEvent.ACTION.MOVE分支的最后判断滑动在两个方向上只要不是0,就会调用mGapWorker.postFromTraversal,这样就反向查找到了RecyclerView的滑动刷新原理。即RecyclerView的onTouchEvent事件监听中,当监听到滑动时,会使用GapWorker去加载下一个需要显示出来的ViewHolder。

至于是怎么去加载下一个ViewHolder,这里是在GapWorker的run方法中调用了其prefetch方法,在prefetch方法中有两个调用,第一个是buildTaskList,这个buildTaskList方法中会调用collectPrefetchPositionsFromView,见名知意,其实就是根据当前RecyclerView的状态,获取当前是在哪个position位置,这样去加载下一个ViewHolder。走到LayoutManager中,将position和距离信息存入了一个GapWorker$LayoutPrefetchRegistryImpl的int数组mPrefetchArray中。这个数据其实是当两个数据来用的,每两个位置分别保存position和distance。用buildTaskList找到下一个ViewHolder的位置后,在prefetch方法中执行调用第二个方法flushTaskWithDeadline方法。执行到重载方法,代码如下

private void flushTaskWithDeadline(Task task, long deadlineNs) {
        long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
        RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
                task.position, taskDeadlineNs);
        if (holder != null
                && holder.mNestedRecyclerView != null
                && holder.isBound()
                && !holder.isInvalid()) {
            prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
        }
    }

获取使用prefetchPositionWithDeadline方法获取到一个ViewHolder,这个prefetchPositionWithDeadline方法中其实也是调用了Recycler的tryGetViewHolderForPositionByDeadline方法,这跟用Adapter去notify更新数据从而刷新RecyclerView是一样的,也会先去缓存中取,取不到就onCreate,然后再onBind。取出一个ViewHolder之后,判断这个ViewHolder的mNestedRecyclerView是否为null,viewHolder是否已经bound了,是否是可用状态,如果满足,再调用prefetchInnerRecyclerViewWithDeadline方法,这个mNestedRecyclerView是个WeakReference<RecyclerView>类型的,查看其赋值的地方,可以发现这个mNestedRecyclerView表示的是ViewHolder中嵌套着的内部RecyclerView。这样也就好理解prefetchInnerRecyclerViewWithDeadline的作用了,其实就是递归地去更新嵌套内部的View。

 类似资料: