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实现一个列表,最重要的是实现一个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)的复用机制其实就是为了配合二级缓存机制。
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
<?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的刷新都是通过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
网上无数文章分析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。