ListView虽然已经几乎被RecycleView取代,但是其复用的核心思想还是很棒的,而且也经常在入门级面试中被提问。在看RecycleView的时候云里雾里的,就先理清ListView。这次的起因是因为RecycleView复用出现的严重bug。其实很久之前就捣鼓过一次ListView的复用问题,现在回过头看看真的是不可思议,我当时是怎么捣鼓出的多层嵌套的ListView的同时还解决了复用引发的问题?当年的我真厉害,连adapter都不知道是啥还弄了出来这些?。扯远了。
总而言之,这次干脆把两块的源码一起学习了。从简单点的ListView开始。
之前记录过ListView的控件使用,adapter的相关内容也比较简单,就不赘述了。
首先要知道ListView是个啥。android常常会出现OOM或者崩溃之类的情况。究其原因是因为内存溢出了。在一个列表项中,可能有成百上千个列表项,如果全部放进内存,为每一个列表项加载一个新的列表单元,内存吃不住,程序猿也吃不消啊。因此就有了ListView。它的主要目的就是不断复用减少相同单元占用的内存。ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。
ListView最大的优点就在于复用过程。也就是RecycleBin。这个类里面的几个主要方法:
fillActiveViews(int childCount, int firstActivePosition)
第一个参数是view的数量,第二个参数是开始的view的位置。这个方法就用来将指定位置的元素放到ListView 中相应位置上。
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
// Don't put header or footer views into the scrap heap
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
// active views.
// However, we will NOT place them into scrap views.
activeViews[i] = child;
}
}
}
getActiveView(int position)
这个方法和前一个是对应的,用于获取position位置的元素,然后移除掉获取过的元素。
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >= 0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;
return match;
}
return null;
}
addScrapView(View scrap)
当一个View确定要废弃掉的时候(比如滚动出了屏幕),这个方法就把View拿过来缓存。
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)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
removeDetachedView(scrap, false);
}
return;
}
if (mViewTypeCount == 1) {
dispatchFinishTemporaryDetach(scrap);
mCurrentScrap.add(scrap);
} else {
dispatchFinishTemporaryDetach(scrap);
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
getScrapView(int position)
就取出被废弃的view,废弃缓存中的View是没有顺序的,因此getScrapView()方法就直接获取尾部的一个scrap view进行返回。
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;
}
setViewTypeCount()
这个方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。重写后有机会解决复用问题。
这是复用部分类的代码。接下来是ListView的绘制过程,这部分主要是布局的时候有别于其他view,因此其onLayout()方法很关键。它的布局方法继承了父类AbsListView。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
}
逻辑上就是判断如果ListView发生了变化,changed就变为true,就强制重绘全部的子布局。在下面有一个layoutChildren()方法,然后在ListView中实现了这个方法。在layoutChildren()方法中主要工作就是子布局的绘制。然后就在setupChild()方法里面自顶部向下绘制,等到布局超出ListView的范围的时候就主动停止布局。因此不多解释代码了。最关键的一段代码在其中的obtainView()方法中。
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position
* The position to display
* @param isScrap
* Array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
可以看到一开始是从getScrapView()方法中获取一个View。在初始化整个ListView的时候肯定是没有废弃的view的,因此这里会拿到空值。于是就会去调用mAdapter里面的内容,也就是我们的adapter的getView()方法。
public View getView(int position, View convertView, ViewGroup parent){
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
}
我们的getView方法有三个参数:position,view和父控件。而在上面getView()方法的第二个参数是null,也就是没有布局,因此就调用LayoutInflater的inflate()方法加载一个布局,然后返回view。这样就说明第一次进入的时候每一个item都是初始化加载出来的。
接下来就是复用逻辑了
复用过程和之前区别不大,一个是加载的时候,会从当前位置开始加载,然后再加载它上面和下面的其他item,另一个就是不会再调用inflate方法再次布局了。这次就会返回一个true到setupChild()方法里面,告诉setupChild()方法这个view是已经被布局过的了,于是将一个之前detach的View重新attach到ViewGroup上。
这样复用过程就结束了。(算了,这次又没说清楚,怕是又连自己都看不懂了)
在图中会发现,下滑的时候元素0的item中把数据丢出去,装入了元素6的数据,就成了复用的过程。
这时候,复用的问题就出现了。假设元素中有个checkBox,我勾选了元素0,然后下滑,结果会发现元素6被勾上了?因为这里用的是元素0的checkBox,所以元素6同时也被勾选了。这是比较常见的问题,还记得几年前我就被这个搞懵了。解决方法有几个:
1.重写你的adapter,这是最麻烦的,但也是最实用最稳定的方法,把checkBox这种属性全部都和viewHolder绑定在一起,而不是和view绑定在一起,这样就能够实现在更改数据的时候checkBox属性随之更改。一般用setTag()方法绑定。
2.永不复用。别笑,这真的是一个方法,因为列表的item不多,干脆不再复用,这样基本上是靠损失性能换取bug的修复。当然,一般能不这么干还是别这么做了,确实太有风险了。
3.多重背景。这种checkBox就不适合这个方法了,也就是比如有三种类型的item的时候就干错做三种view,每次在adapter里判断情况选择不同的view来应对,另几个就隐藏起来。
顺便说一下Listview的优化方式:优化一:convertView的使用,主要优化加载布局问题,从上面分析就知道,多了convertView就实现了复用的关键之一。优化二;viewHolder的使用,缓存控件实例。(主要是减少findViewById()的开销)。优化三:滑动不载入图片(个人感觉不太好)。