最近读了很多关于ListView源码的文章,一是面试的是时候面试官喜欢问一下源码的问题,二来源码里面包含了开发者丰富的设计思想,特别是代码写多了,不应该再用API来堆代码了,应该去了解一下为什么要这样做。呃呃,一下子扯远了。ListView源码的文章,我比较推荐郭神的《AndroidListView工作原理完全解析,带你从源码的角度彻底理解》,讲的很清晰,这篇文章是自己的一些总结。
继承关系
ListView继承结构如下图:
从继承关系上来说,ListView直接继承自AbsListView,AbsListView有两个直接子类,一个是ListView,另一个是GridView。从用法上他们两就有点相似,源码上更是有很多相通之处。
缓存机制RecycleBin
ListView真是有了缓存机制,才使得ListView加载成千上万条数据不会出现OOM。ListView的缓存机制是靠RecycleBin实现的。RecycleBin中有两个最重要的变量:
*/ private View[] mActiveViews = new View[0]; /** * Unsorted views that can be used by the adapter as a convert view. */ private ArrayList<View>[] mScrapViews;
这两个变量是实现缓存机制的关键,其中mActiveViews保存屏幕中存活的Item,mScrapViews存放已经废弃的View;然后RecycleBin中定义了关于操作mActiveViews和mScrapViews的几个函数:
- fillActiveViews() 这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
- getActiveView() 这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。需要注意的是,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。
- addScrapView() 用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。
- getScrapView 用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。
- setViewTypeCount() 我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法通常情况下使用的并不是很多,所以我们只要知道RecycleBin当中有这样一个功能就行了。
绘图流程
Android View的绘制流程分三步:测量(measureàonMeasure),布局(layoutàonLayout),绘制(drawàonDraw)。Listview作为一个View,也要经历这些过程,onMeasure()并没有什么特殊的地方,因为它终归是一个View。onDraw()在ListView当中也没有什么意义,因为ListView本身也不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的了ListView的onLayout流程还是比较复杂,具体流程可以去看郭神的博客,我这里就简单总结一下,
ListView的onLayout会去调用layoutChildren,layoutChildrenàfillFromTopàfillDownàmakeAndAddViewàsetupChild
再谈缓存机制
在上一下节中,我们将ListView的绘制流程弄清楚了,ListView的缓存机制体现在makeAndAddView,makeAndAddView的作用是构造一个Item View,并将其添加到ListView,makeAndAddView内部流程是:数据集是否有变化,如果没有直接从mActiveVie获取View,如果有变化,会调用obtainView获取item view,obtainView内部通过getScrapView()方法来从mScrapView尝试获取一个废弃缓存中的View,如果有就复用这个View,没有就调用mAdapter的getView()方法来去获取一个View,那么mAdapter是什么呢?当然就是当前ListView关联的适配器了。而getView()方法又是什么呢?还用说吗,这个就是我们平时使用ListView时最最经常重写的一个方法了,这里getView()方法中传入了三个参数,分别是position,null和this。
那么我们平时写ListView的Adapter时,getView()方法通常会怎么写呢?这里我举个简单的例子:
@Override
public ViewgetView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
if (convertView == null) {
view =LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView)view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView)view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
getView()方法接受的三个参数,第一个参数position代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数convertView,刚才传入的是null,说明没有convertView可以利用,因此我们会调用LayoutInflater的inflate()方法来去加载一个布局。接下来会对这个view进行一些属性和值的设定,最后将view返回。那么这个View也会作为obtainView()的结果进行返回,并最终传入到setupChild()方法当中
总结起来,ListVie的View从三个方面获得,一是从mActiveViews中获取,二是mAdapter中获取,三是从mScrapViews中获取了,最后在调用setupChild方法把make的view添加到ListView上去。流程如下:
滑动加载
在 ListView滑动过程中 ListView中的视图是会随时变化的,ListView滑动加载过程是在他的父类AbsListView中的onTouchEvent中实现的,具体过程可以去看郭神的博客,这里还是总结一下。onTouchEvent中会判断滑动方向和滑动距离,ListView的子View根据bottom值和top值判断是否滑出屏幕,view如果滑出屏幕会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,接着会调用detachViewFromParent将需要移除的 view移出 ListView,还会将仍在布局中的View调用调用了offsetChildrenTopAndBottom()方法根据滑动距离做出移动,最后如果有View移出屏幕,则通过 fillGap方法充新的 View。在fillGap中又会调用fillDown和 fillUp方法,又回到了第二节中ListView绘制过程,最终用makeAndAddView方法来获取View,这次obtainView会从RecycleBin中的废弃View中取出需要的 View,由于滑出屏幕的 View已经添加到RecycleBin中,所以这里不为 null,在Adapter.getView时就可以重用被回收的View,实现了 ListView虽然有千百条数据,但是真正的 itemView确只有很少的几个。
文中部分内容来源于:
http://blog.csdn.net/guolin_blog/article/details/44996879
http://www.jianshu.com/p/ad621b49735e