Listview源码分析

本文主要内容

Listview是一种常用的控件,它的主要特点是能够复用,上下滑动时不至于卡顿,内存波动等。要实现这种功能,肯定存在着缓存机制,今天我们着重分析下Listview的缓存机制以及它的设计模式。

  • 1、Listview简介
  • 2、缓存分析
  • 3、Listview原理
  • 4、滑动原理
  • 5、总结

1、Listview简介

Listview的继承关系如下,它的父类是AbsListView,缓存类是在AbsListView中实现的。

如果是我们来设计Listview,我们会如何设计?

Listview可以上下滑动,子view被滑动出屏幕以后要如何处理?如果直接将子view删除回收,那么每次滑动都要重新inflate子view的布局,将会导致性能问题,而且内存回收也会更频繁,导致内存波动较大,因为内存并不一定会这么及时地释放。

所以需要缓存机制,将滑动出屏幕以外的子view缓存起来,为后续重用做准备,这样性能问题不存在了,内存和性能相互平衡。

另外,Listview理论上可以显示任意形式的子view,为了方便开发者,也为了解耦数据来源,封装了adapter,这是绝佳的适配器模式示例了。

2、缓存分析

Listview缓存依赖于RecycleBin类(AbsListView的内部类)。一起来看看RecycleBin类的逻辑:

class RecycleBin {
//mActiveViews中第一个view在Listview中的位置
private int mFirstActivePosition;
//缓存当前正在显示的view
private View[] mActiveViews = new View[0];
//缓存没有被显示的view,废弃view,注意它是一个List数组
private ArrayList<View>[] mScrapViews;
//类型值,有多少种类型,mScrapViews数组的长度就是多少
private int mViewTypeCount;
//当前的废弃view缓存列表
private ArrayList<View> mCurrentScrap;
//设置数据类型,有几种类型就有多少种废弃view缓存列表,一种类型对应一个列表
public void setViewTypeCount(int viewTypeCount) {
    ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
    for (int i = 0; i < viewTypeCount; i++) {
        scrapViews[i] = new ArrayList<View>();
    }
    mViewTypeCount = viewTypeCount;
    mCurrentScrap = scrapViews[0];
    mScrapViews = scrapViews;
}
//将正在显示的子view都添加到mActiveViews数组中来
void fillActiveViews(int childCount, int firstActivePosition) {
    mFirstActivePosition = firstActivePosition;
    final View[] activeViews = mActiveViews;
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
        if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            activeViews[i] = child;
        }
    }
}
//获取某个位置上的正在显示的view
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;
}
//添加废弃view,注意如果子view是暂存状态,即hasTransientState为true,则不缓存
void addScrapView(View scrap, int position) {
    final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
    lp.scrappedFromPosition = position;
    final int viewType = lp.viewType;
    if (mViewTypeCount == 1) {
        mCurrentScrap.add(scrap);
    } else {
        mScrapViews[viewType].add(scrap);
    }
}
//将所有正在显示的view都添加到废弃缓存列表中
void scrapActiveViews() {
    final View[] activeViews = mActiveViews;
    final boolean hasListener = mRecyclerListener != null;
    final boolean multipleScraps = mViewTypeCount > 1;
    ArrayList<View> scrapViews = mCurrentScrap;
    final int count = activeViews.length;
    for (int i = count - 1; i >= 0; i--) {
        final View victim = activeViews[i];
        if (victim != null) {
            final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) victim.getLayoutParams();
            final int whichScrap = lp.viewType;
            activeViews[i] = null;
            lp.scrappedFromPosition = mFirstActivePosition + i;
            scrapViews.add(victim);
        }
    }
    //删除过多的废弃缓存
    pruneScrapViews();
}

//删除过多的废弃缓存,因为缓存也不是无上限的,确定废弃缓存不超过mActiveViews数组的长度
private void pruneScrapViews() {
    final int maxViews = mActiveViews.length;
    final int viewTypeCount = mViewTypeCount;
    final ArrayList<View>[] scrapViews = mScrapViews;
    for (int i = 0; i < viewTypeCount; ++i) {
        final ArrayList<View> scrapPile = scrapViews[i];
        int size = scrapPile.size();
        final int extras = size - maxViews;
        size--;
        for (int j = 0; j < extras; j++) {
            removeDetachedView(scrapPile.remove(size--), false);
        }
    }
}
//从废弃缓存列表中取一个子view出来,注意有不同的取法,某至有匹配不到直接拿废弃列表中的最后一个
//所以adapter的getView方法中,需要对converView做检查,匹配,否则有可能出现数据错乱
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
    final int size = scrapViews.size();
    if (size > 0) {
        // See if we still have a view for this position or ID.
        for (int i = 0; i < size; i++) {
            final View view = scrapViews.get(i);
            final AbsListView.LayoutParams params = (AbsListView.LayoutParams) view.getLayoutParams();
            if (mAdapterHasStableIds) {
                final long id = mAdapter.getItemId(position);
                if (id == params.itemId) {
                    return scrapViews.remove(i);
                }
            } else if (params.scrappedFromPosition == position) {
                final View scrap = scrapViews.remove(i);
                clearAccessibilityFromScrap(scrap);
                return scrap;
            }
        }
        final View scrap = scrapViews.remove(size - 1);
        clearAccessibilityFromScrap(scrap);
        return scrap;
    } else {
        return null;
    }
}
}

需要注意几点:

  • 如果子view处理TransientState状态,即hasTransientState方法返回为true,子view不会被添加到废弃列表中

  • 二级缓存,第一重缓存为mActiveViews,代表着当前正在显示的view,第二重缓存为废弃view队列数组。它们之间的数据流转,需要查看Listview源码才能解释清楚

  • 注意到废弃view的缓存是一个列表数组,而mActiveViews则只是一个数组。理论上废弃view缓存是一个数组就行了,为什么现在有多个废弃view队列了呢?这是因为Listview可能有多种数据类型,一种数据类型对应着一个废弃view队列。在Listview或者RecycleView中显示多种数据类型,相信很多人都用过吧。

  • 只要是缓存,肯定有对应的缓存算法,当要缓存数量的数量超过了最大值了怎么办?要删除哪些缓存才能装得下新缓存。熟悉的有LRU算法(最近最少使用算法),而Listview比较简单,查看 pruneScrapViews 方法,只要废弃队列的长度超过了 mActiveViews 的长度,就从废弃队列最末尾开始删除,直到不超过为止。每次添加新项到废弃队列中,都会检查。

3、Listview原理

任何一个控件的原理都脱不开基本的View原理,即measure、layout、draw这一套,所以就根据这个原理,盘它!!

measure用于确定自身以及子view的大小,Listview的measure过程比较简单,自身大小受限于Listview父控件,所以measure过程不改变它。而子view的大小也根据用户的设定即可。

layout是整个过程中最复杂的一项,代码非常的长。onLayout方法中定义在父类AbsListView中,AbsListView还定义了一个抽象方法,layoutChildren,它的子类可以根据自身需求更改布局。别忘了,AbsListView还有个子类,GridView。这种设计思想值得学习,模板方法,钩子函数。

protected void layoutChildren() {
        final int firstPosition = mFirstPosition;
        final RecycleBin recycleBin = mRecycler;
        //如果数据改变,那么将所有子view都添加到废弃列表中
        if (dataChanged) {
            for (int i = 0; i < childCount; i++) {
                recycleBin.addScrapView(getChildAt(i), firstPosition+i);
            }
        } else {
            //如果数据不变,就将当前正在显示的所有view添加到mActiveViews中
            recycleBin.fillActiveViews(childCount, firstPosition);
        }
        // 删除所有的子view,要重新添加
        detachAllViewsFromParent();
        switch (mLayoutMode) {
            default:
                //根据Listview的模式,确定填充子view的方法,一般情况下,是Normal模式,会走下边这个方法 fillFromTop
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    sel = fillFromTop(childrenTop);
                }
        }
        // Flush any cached views that did not get reused above
        //在fillFromTop方法中,获取子view的时候其实就会从mActiveViews中拿缓存数据,如果fillFromTop方法执行完了,
        // mActiveViews 中还有数据,则将剩余的数据添加到废弃队列中
        //为什么 mActiveViews 中的数据没有被 fillFromTop 全被拿完呢,理论上 Listview不动,mActiveViews肯定会被拿完的
        //在用户滑动列表后,mActiveViews 有可能没有被拿完
        recycleBin.scrapActiveViews();
    }

接着我们来看 fillFromTop 方法

private View fillDown(int pos, int nextTop) {
    View selectedView = null;
    //end代表着整个listview的高度
    int end = (mBottom - mTop);
    //nexttop表示listview的top位置
    while (nextTop < end && pos < mItemCount) {
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
        //nextTop的值随着子view位置的往下,而越来越大,当nextTop值大于或等于end值时,跳出循环,不再添加子view
        //所有listview上只在可视区域添加子view。非可视区域并没有子view存在
        nextTop = child.getBottom() + mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        pos++;
    }
    return selectedView;
}

再看看makeAndAddView方法:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) {
    View child;
    if (!mDataChanged) {
        // Try to use an existing view for this position
        //如果没有数据改变,直接先从 Active 缓存中拿数据,拿到了就调用setupChild方法,将子view添加到listview中来
        child = mRecycler.getActiveView(position);
        if (child != null) {
            setupChild(child, position, y, flow, childrenLeft, selected, true);
            return child;
        }
    }
    // Make a new view for this position, or convert an unused view if possible
    //Active缓存中没有,从废弃缓存中拿数据,然后调用adapter的getView方法,生成一个子view,将子view添加到listview上
    child = obtainView(position, mIsScrap);
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    return child;
}

setupChild 方法就不再讲述了,它的重点在于调用子view的layout方法,并且将子view添加到listview中来。

而obtainView则是获取新的子view的,它会调用adapter的getView方法,获取子view:

//注意trace操作,在抓systrace时,如果有listview,就可以看到obtainView方法执行的细节
View obtainView(int position, boolean[] isScrap) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
    isScrap[0] = false;
    //从废弃队列中拿缓存数据
    final View scrapView = mRecycler.getScrapView(position);
    //调用adapter的getView方法,给了开发一个重用的机会,不用重新inflate view了,提高了效率
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            // Failed to re-bind the data, return scrap to the heap.
            //rebind过程,如果缓存和child不一样,重新放回废弃队列
            mRecycler.addScrapView(scrapView, position);
        }
    }
    //设置layout参数
    setItemViewLayoutParams(child, position);
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    return child;
}

到此为止,layout流程已经分析完了,缓存间的数据流转也明了了。在layout之前,将所有的显示的子view添加到active缓存当中。然后删除所有的子view,重新加载。在重新加载过程中,会先到active缓存中查找数据,如果有就直接添加子view,如果没有再去废弃缓存中查找。最后子view添加完后,将active缓存中剩下的view添加到废弃缓存中。每次添加废弃缓存时,都要去检查废弃缓存的容量,如果超过了active缓存的长度,则从队尾开始删除,直接少于为止。

4、滑动原理

Listview在layout过程中,只会在可见区域添加子view,可见区域外并不会添加,而滑动过程中用户能够看到新的子view,那么滑动时肯定动态添加子view了。另外缓存数据肯定也有一些操作。带着这些猜想一起来看吧。

滑动的流程稍显复杂,先来一张流程图:

猜想都在 trackMotionScroll 这个方法中,删除不可见子view并缓存起来,然后添加新的子view,最后再将子view的位置也移动,尽在于此。

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
    //判定滑动方向,如果 incrementalDeltaY 小于0,则是向下滑动
    final boolean down = incrementalDeltaY < 0;
    int start = 0;
    int count = 0;
    if (down) {
        int top = -incrementalDeltaY;
        for (int i = 0; i < childCount; i++) {
            //向下滑动,子view的bottom值大于listview的top值,则表示子view仍居于可视位置,那么这类子view略过不处理
            final View child = getChildAt(i);
            if (child.getBottom() >= top) {
                break;
            } else {
                //将不可见view添加到废弃缓存中
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    } else {
        //另一种情况,往上滑动
        int bottom = getHeight() - incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            bottom -= listPadding.bottom;
        }
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= bottom) {
                break;
            } else {
                start = i;
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    }
    //count代表要删除的子view,如果count大于0,则从start开始,删除count个子view,这些子view已经不可见了
    if (count > 0) {
        detachViewsFromParent(start, count);
        mRecycler.removeSkippedScrap();
    }
    //滑动剩余子view,让子view也跟着手指动起来
    offsetChildrenTopAndBottom(incrementalDeltaY);
    //有些子view不可见了,必然有空位出来,空位要添加新的子view,fillGap,听名字就是填充子view的方法        
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }
}

读完代码后,上面判断down和up是应该相反的方法,如果是向上滑动,则是走down的流程,向下则是up的流程。

在向上滑动过程中,从第一个子view开始计算,如果子view的bottom位置小于listview的top位置,显然这个子view已经不可见了,要被添加到废弃缓存中去。最后记录起点,从起点开始删除count个子view。再执行 offsetChildrenTopAndBottom 方法,移动仍然可见的子view。

public void offsetChildrenTopAndBottom(int offset) {
    final int count = mChildrenCount;
    final View[] children = mChildren;
    boolean invalidate = false;

    for (int i = 0; i < count; i++) {
        final View v = children[i];
        v.mTop += offset;
        v.mBottom += offset;
        if (v.mRenderNode != null) {
            invalidate = true;
            v.mRenderNode.offsetTopAndBottom(offset);
        }
    }

    if (invalidate) {
        invalidateViewProperty(false, false);
    }
    notifySubtreeAccessibilityStateChangedIfNeeded();
}

从代码中可知,其实是遍历所有可见子view,直接将子view的top值变化,这样子view的位置当然就变了。于是listview可以跟手滑动了。

5、总结

Listview还有一处值得讨论,就是它的adapter设计。Listview在设计之初,肯定是想能显示一切类型的数据,它的子view可以是各种各样的格式。既然子view的格式千差万别,那么如何来兼容这些不同的数据,不同的格式。

adapter应运而生,将数据和view样式放到adapter中让用户自己定义,这实在是天才的设计,这样解耦了数据和listview的内在逻辑,listview得以专心处理自己的功能,而不用在意数据问题了,用户也能有更大的定制空间了。

listview整体代码还是非常复杂,本文之后,获益良多,多读源码,越读越会有新的收获。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 本文主要从源码的角度分析 ListView 的工作原理和使用方法,如有不正确的地方欢迎大家指正。 Adapter ...
    small_yg阅读 472评论 0 0
  • ListView源码分析 项目中使用ListView还是挺多的,之前看过几次,很是容易遗忘,今特做记录如下 And...
    Nvsleep阅读 3,143评论 2 11
  • 参考文章: android列表View,ListView源码分析 Android ListView工作原理完全解析...
    千涯秋瑟阅读 6,716评论 1 5
  • 今天好开心呐,我感觉生活丰富多彩,完成了很多任务。
    咿源泉物语阅读 223评论 0 0
  • 老公,宝宝,涛涛,我好想你。雨下的真大,刚送完一波孩子,尽管穿了雨衣,衣服头发还是湿透了。刚刚坐下,非常想念,坐在...
    涛之源阅读 186评论 0 0