滑动列表展示数据在应用开发中已经基本是最为常见的一个场景,RecyclerView作为Android新钦定的列表展示控件在5.0版本的support包中出现,又经过了后续版本的改进,目前已经有一万多行代码。其对于性能的优化和灵活的设计可以说又是Android源码中的一个经典,非常值得我们学习观摩。
RecyclerView的使用不再赘述,我们从其整体设计开始看。
RecyclerView整体的模块化设计
随便看一眼RecyclerView的源码,会发现其中有一大半是内部类的实现。这是RecyclerView设计的一大特点,RecyclerView采用了模块化的设计,将各个部分职责分别抽出来进行了模块化的封装。其中最主要的有:最复杂的LayoutManager负责整个界面的布局工作,ItemAnimator负责界面变化时的动画,ItemDecoration负责界面上的装饰效果,Recycler负责缓存回收复用工作。
其中,开发者更是可以自己编写LayoutManager,ItemAnimator,ItemDecoration实现各种各样的列表展示效果。这里要提及的是,这几者都封装得非常浅,这使得开发者拥有极大的自由度,但也导致自行编写的工作量非常大,还好附带的几个默认实现已经基本能满足绝大部分的需求。
模块化的设计使得一万多行复杂的代码分离开来,既利于维护,又方便今后的扩展,在阅读时也能最快地在宏观上把握作者的思路(虽然阅读源码还是非常头疼)。
ViewHolder的引入与适配器模式的使用
在实际开发中的数据展示场景,我们一般拿到的是一个数据的列表,而需要将其转化成视图列表展示出来。在这个场景下RecyclerView对外采用了适配器模式,将数据和视图两部分完全抽象出来。
数据部分就是我们拿到的数据列表,可能是来自网络,也可能是来自SQLite,它的类型有无限种可能性。
而视图部分,在这里RecyclerView引入了一个ViewHolder的概念,一个ViewHolder个体就是一个数据项对应的视图的抽象。ViewHolder并不继承于View,它只是被封装出来用于标准化整个RecyclerView范围内的视图个体,RecyclerView内部所有的展示、更新、回收、复用都是以ViewHolder为操作个体的,而实际被添加进ViewTree的是其内部的itemView。所以在代码层面来讲,可以说ViewHolder是封装了itemView与其一些状态的一个包裹类。
在编写Adapter的时候我们必须要重写getItemCount()
、onCreateViewHolder()
与onBindViewHolder()
,其意义就是,前者就是对于每个数据项创建一个对应的ViewHolder并且描述它长什么样,交给RecyclerView;后者则是建立数据与ViewHolder的绑定关系。
如图,在这里RecyclerView使用了适配器模式以后,Adapter将RecyclerView和Data完全隔离开来,RecyclerView根本不需要知道任何与数据相关的信息,只由Adapter负责将一个个数据项适配成一个个ViewHolder后交给RecyclerView,RecyclerView再来对它熟知的ViewHolder进行安排。这样做的好处是,既保证了多变的数据部分的极大灵活性,又能让RecyclerView以不变应万变,处理各种各样的数据。这样一来,RecyclerView就能专注于它作为一个ViewGroup掌管视图绘制的使命。至于针对不同数据而需要做的相应处理,由开发者来根据实际情况在Adapter中编写就好了。就像我们通常会在Adapter中保留一份数据的引用,然后封装一些方法对它进行诸如添加,修改之类的操作,不管你怎么操作数据,只要最后将它转化成ViewHolder就可以。
Adapter数据更新与观察者模式的使用
在数据更新的时候,RecyclerView与它的Adapter通过观察者模式的配合来实现界面的同步更新。
具体的实现方法是,Adapter中持有一个AdapterDataObservable对象,AdapterDataObservable直接继承了Java源码中的Observable<T>,这是一个由Java提供的观察者模式模板。
public static abstract class Adapter<VH extends ViewHolder> {
private final AdapterDataObservable mObservable = new AdapterDataObservable();
...
}
与此同时,RecyclerView中持有一个RecyclerViewDataObserver。
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
...
}
于是我们完全可以认为,在这里,就是由RecyclerView作为一个观察者,时刻关注着被观察者Adapter中的数据变动,从而在数据发生变动的时候作出界面上的响应。
那Observer是何时注册到Observable的呢?在RecyclerView调用setAdapter()
设置自己的Adapter时,内部的setAdapterInternal()
会调用adapter的registerAdapterDataObserver()
将自己的Observer注册到Adapter的Observable上。
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
...
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);//注册
...
}
...
}
从这个时候开始,数据和界面就算绑定到了一起了。我们都知道,和ListView一样,在数据有更新的时候我们应该调用Adapter的notifyDataSetChanged()
方法,这样RecyclerView就会知道要对界面进行重绘了。而这个通知过程在这里的实现,就是mObservable通过notifyDataSetChanged()
中notifyChanged()
通知在自己这注册了的所有观察者mObserver数据产生了变动,要调用自己的onChanged()
回调方法响应变动了。而这个需要由观察者重写的onChanged()
方法:
private class RecyclerViewDataObserver extends AdapterDataObserver {
...
@Override
public void onChanged() {
assertNotInLayoutOrScroll(null);
mState.mStructureChanged = true;
setDataSetChangedAfterLayout();
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();//请求界面重绘
}
}
...
}
可以看到最终会调用requestLayout()向上级请求界面重绘,于是界面就会刷新了。
我们上面说会通知所有注册了的观察者,事实上在这个套路中,一般注册在Adapter里的观察者只有RecyclerView中的mObserver一个人,但是事实上完全可以有多个观察者。也就是说,你可以给多个RecyclerView设置同一个Adapter,甚至自己创造一个观察者去关注Adapter数据的变动。
在这里使用观察者模式后,RecyclerView获知数据变动不需要依赖于Adapter,而只依赖于抽象的被观察者,这样的设计使得他们两者之间的耦合度更加低,面向未来其它形式的扩展更加灵活。
在RecyclerView诞生时,由于已经有先前大量的ListView的设计使用经验,我们可以发现RecyclerView设计的目的非常明确。设计者将开发者在列表展示这个业务场景中接触最频繁、自定义需求最多的几个,如布局、动画模块,单独抽了出来,留下了接口供开发者自行设计。而在对于其它开发者不需要太关注的实现细节上,均已经进行了非常细致的实现,而且花了非常多的功夫进行性能上的优化。在布局上,默认提供的3种LayoutManager非常实用,基本上能覆盖大部分的开发需求,而灵活的RecyclerView还能实现很多别的旧控件一模一样甚至更佳的效果。另外在回收复用方面,RecyclerView已经实现了Scrap机制、CacheView机制与RecycleViewPool机制几种缓存方式,但仍然留下了一个ViewCacheExtension可供开发者根据自己的实际情况自行设计一层额外的缓存。可以说RecyclerView如今的热门不是没有理由的,它本身就是一个非常经典的设计。