博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
ListView原理
阅读量:5158 次
发布时间:2019-06-13

本文共 16283 字,大约阅读时间需要 54 分钟。

表明转载自http://blog.csdn.net/iispring/article/details/50967445

在自己定义Adapter时,我们经常会重写Adapter的getView方法,该方法的签名例如以下所看到的:

public abstract View getView (int position, View convertView, ViewGroup parent) 
  • 1

此处会传入一个convertView变量。它的值有可能是null。也有可能不是null,假设不为null,我们就能够复用该convertView。对convertView里面的一些控件赋值后能够将convertView作为getView的返回值返回,这么做的目的是降低LayoutInflater.inflate()的调用次数。从而提升了性能(LayoutInflater.inflate()比較消耗性能)。

本文将介绍ListView中的RecycleBin机制,让大家对ListView中的优化机制有个概括的了解。同一时候也说明convertView的来龙去脉。

首先,我们知道,Adapter是数据源,AdapterView是展示数据源的UI控件。Adapter是给AdapterView使用的。通过调用AdapterView的setAdapter方法就能够让一个AdapterView绑定Adapter对象,从而AdapterView会将Adapter中的数据展示出来。

AdapterView的子类有AbsListView和AbsSpinner等,当中AbsListView的子类又有ListView、GridView等。所以ListView继承自AdapterView。

假设Adapter中有10000条数据。将这个Adapter对象赋给ListView。假设ListView创建10000个子View,那么App肯定崩溃了,由于Android没有能力同一时候绘制这么多的子View。并且,即便能同一时候绘制这10000个子View也没什么意义,由于手机的屏幕大小是有限的,有可能ListView的高度仅仅能最多显示10个子View。

基于此,Android在设计ListView这个类的时候。引入了RecycleBin机制—–对子View进行回收利用,RecycleBin直译过来就是回收站的意思。


RecycleBin基本原理

以下先简要说一下RecycleBin中的工作原理。后面会结合源代码具体说明。

在某一时刻,我们看到ListView中有很多View呈如今UI上,这些View对我们来说是可见的,这些可见的View能够称作OnScreen的View,即在屏幕中能看到的View,也能够叫做ActiveView。由于它们是在UI上可操作的。

当触摸ListView并向上滑动时,ListView上部的一些OnScreen的View位置上移。并移除了ListView的屏幕范围。此时这些OnScreen的View就变得不可见了,不可见的View叫做OffScreen的View。即这些View已经不在屏幕可见范围内了,也能够叫做ScrapView。Scrap表示废弃的意思。ScrapView的意思是这些OffScreen的View不再处于能够交互的Active状态了。ListView会把那些ScrapView(即OffScreen的View)删除,这样就不用绘制这些本来就不可见的View了,同一时候,ListView会把这些删除的ScrapView放入到RecycleBin中存起来,就像把临时没用的资源放到回收站一样。

当ListView的底部须要显示新的View的时候,会从RecycleBin中取出一个ScrapView,将其作为convertView參数传递给Adapter的getView方法,从而达到View复用的目的,这样就不必在Adapter的getView方法中运行LayoutInflater.inflate()方法了。

RecycleBin中有两个重要的View数组,各自是mActiveViews和mScrapViews。这两个数组中所存储的View都是用来复用的,仅仅只是mActiveViews中存储的是OnScreen的View。这些View非常有可能被直接复用;而mScrapViews中存储的是OffScreen的View,这些View主要是用来间接复用的。

上面对mActiveViews和mScrapViews的说明比較笼统,事实上在细节上还牵扯到Adapter的数据源发生变化的情况,详细细节后面会解说。


源代码解析

AdapterView是继承自ViewGroup的,ViewGroup中有addView方法能够向ViewGroup中加入子View。可是AdapterView重写了addView方法,例如以下所看到的:

@Override    public void addView(View child) {        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");    }@Override    public void addView(View child, int index) {        throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在AdapterView的addView方法中会抛出异常,也就是说AdapterView禁用了addView方法。

在详细解说之前。我们还是先花一点时间简要说一下View的每一帧的显示流程,当然,ListView也肯定遵循此流程。一个View要想在界面上呈现出来。须要经过三个阶段:measure->layout->draw。

View是一帧一帧绘制的,每一帧绘制都经历了measure->layout->draw这三个阶段,绘制完一帧之后,假设UI须要更新,比方用户滚动了ListView,那么又会绘制下一帧,再次经历measure->layout->draw方法,假设对此不了解。能够參见还有一篇博文。

我们上面说了,AdapterView把addView方法给禁用了。那么ListView怎么向当中加入child呢?奥秘就在layout中,在布局的时候,ListView会运行layoutChildren方法。该方法是ListView对View进行加入以及回收的关键方法,RecycleBin的非常多方法都在layoutChildren方法中被调用。在layoutChildren方法中实现对子View的增删。经过layoutChildren方法之后,ListView中全部的子View都是在屏幕中可见的,也就是说layoutChildren方法为接下来的帧绘制把子View准备完好了。这就保证了在后面的draw方法的运行过程中可以正确绘制ListView。

ListView的layoutChildren方法代码比較多。我们仅仅研究和View增删相关的关键代码,主要分下面三个阶段:

  1. ListView的children->RecycleBin
  2. ListView清空children
  3. RecycleBin->ListView的children

在layout这种方法刚刚開始运行的时候,ListView中的children事实上还是上一帧中须要绘制的子View的集合,在layout这种方法运行完毕的时候,ListView中的children就变成了当前帧立即要进行绘制的子View的集合。

以下对以上这三个阶段分别说明。

  1. ListView的children->RecycleBin 

    该阶段的关键代码例如以下所看到的:

    //mFirstPosition是ListView的成员变量,存储着第一个显示的child所相应的adapter的position        final int firstPosition = mFirstPosition;        final RecycleBin recycleBin = mRecycler;        if (dataChanged) {            //假设数据发生了变化,那么就把ListView的全部子View都放入到RecycleBin的mScrapViews数组中            for (int i = 0; i < childCount; i++) {                //addScrapView方法会传入一个View。以及这个View所相应的position                recycleBin.addScrapView(getChildAt(i), firstPosition+i);            }        } else {            //假设数据没发生变化,那么把ListView的全部子View都放入到RecycleBin的mActiveViews数组中            recycleBin.fillActiveViews(childCount, firstPosition);        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    再次强调一下,在上面的代码刚開始的时候,ListView的中的children还是上一帧须要绘制的子View。

    • 假设Adapter调用了notifyDataSetChanged方法。那么AdapterView就会知道Adapter的数据源发生了变化,此时dataChanged变量就为true,这样的情况下,ListView会觉得children中的View都是不合格的了。这时候会用getChildAt方法遍历children中全部的child。并把这些child通过RecycleBin的addScrapView方法将其放入RecycleBin的mScrapViews数组中。

    • 假设adapter的数据没有发生变化,那么会调用RecycleBin的fillActiveViews方法将全部的children都放入到RecycleBin的mActiveViews数组中。

    经过上面的操作之后,ListView全部的子View都放入到了RecycleBin中。这就实现了ListView的children->RecycleBin的迁移过程。放到RecycleBin的目的是为了分类缓存ListView中的children,以便在兴许过程中对这些View进行复用。

  2. ListView清空children 

    然后调用ViewGroup的detachAllViewsFromParent方法,该方法将全部的子View从ListView中分离。也就是清空了children。该方法源代码例如以下所看到的:

    protected void detachAllViewsFromParent() {    final int count = mChildrenCount;    if (count <= 0) {        return;    }    final View[] children = mChildren;    mChildrenCount = 0;    for (int i = count - 1; i >= 0; i--) {        children[i].mParent = null;        children[i] = null;    }}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  3. RecycleBin->ListView的children

    然后ListView会依据mLayoutMode进行推断,源代码例如以下所看到的:

    switch (mLayoutMode) {        case LAYOUT_SET_SELECTION:            if (newSel != null) {                sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);            } else {                sel = fillFromMiddle(childrenTop, childrenBottom);            }            break;        case LAYOUT_SYNC:            sel = fillSpecific(mSyncPosition, mSpecificTop);            break;        case LAYOUT_FORCE_BOTTOM:            sel = fillUp(mItemCount - 1, childrenBottom);            adjustViewsUpOrDown();            break;        case LAYOUT_FORCE_TOP:            mFirstPosition = 0;            sel = fillFromTop(childrenTop);            adjustViewsUpOrDown();            break;        case LAYOUT_SPECIFIC:            sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);            break;        case LAYOUT_MOVE_SELECTION:            sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);            break;        default:            if (childCount == 0) {                if (!mStackFromBottom) {                    final int position = lookForSelectablePosition(0, true);                    setSelectedPositionInt(position);                    sel = fillFromTop(childrenTop);                } else {                    final int position = lookForSelectablePosition(mItemCount - 1, false);                    setSelectedPositionInt(position);                    sel = fillUp(mItemCount - 1, childrenBottom);                }            } else {                if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {                    sel = fillSpecific(mSelectedPosition,                            oldSel == null ?

    childrenTop : oldSel.getTop()); } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">else</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (mFirstPosition < mItemCount) { sel = fillSpecific(mFirstPosition, oldFirst == <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span> ? childrenTop : oldFirst.getTop()); } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">else</span> { sel = fillSpecific(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, childrenTop); } } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">break</span>; }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li><li style="box-sizing: border-box; padding: 0px 5px;">43</li><li style="box-sizing: border-box; padding: 0px 5px;">44</li><li style="box-sizing: border-box; padding: 0px 5px;">45</li><li style="box-sizing: border-box; padding: 0px 5px;">46</li><li style="box-sizing: border-box; padding: 0px 5px;">47</li><li style="box-sizing: border-box; padding: 0px 5px;">48</li><li style="box-sizing: border-box; padding: 0px 5px;">49</li><li style="box-sizing: border-box; padding: 0px 5px;">50</li></ul>

    在该switch代码段中,会依据不同情况增删子View,这些方法的代码逻辑大部分终于调用了fillDown、fillUp等方法。 

    fillDown用子View从指定的position自上而下填充ListView,fillUp则是自下而上填充,我们以fillDown方法为例具体说明。

     

    fillDown方法的源代码例如以下所看到的:

    private View fillDown(int pos, int nextTop) {    View selectedView = null;    //end表示ListView的高度    int end = (mBottom - mTop);    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {        end -= mListPadding.bottom;    }    //nextTop < end确保了我们仅仅要将新增的子View可以覆盖ListView的界面就行了    //pos < mItemCount确保了我们新增的子View在Adapter中都有相应的数据源item    while (nextTop < end && pos < mItemCount) {        // is this the selected item?        boolean selected = pos == mSelectedPosition;        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);        //将最新child的bottom值作为下一个child的top值,存储在nextTop中        nextTop = child.getBottom() + mDividerHeight;        if (selected) {            selectedView = child;        }        //position自增        pos++;    }    setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);    return selectedView;}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • fillDown接收两个參数,pos表示列表中第一个要绘制的item的position,其相应着Adapter中的索引,nextTop表示第一个要绘制的item在ListView中实际的位置, 即该item所相应的子View的顶部到ListView的顶部的像素数。

    • 首先将mBottom - mTop的值作为end,end表示ListView的高度。

    • 然后在while循环中加入子View,我们先不看while循环的详细条件。先看一下循环体。在循环体中,将pos和nextTop传递给makeAndAddView方法,该方法返回一个View作为child,该方法会创建View,并把该View作为child加入到ListView的children数组中。

    • 然后运行nextTop = child.getBottom() + mDividerHeight,child的bottom值表示的是该child的底部到ListView顶部的距离,将该child的bottom作为下一个child的top。也就是说nextTop一直保存着下一个child的top值。

    • 最后调用pos++实现position指针下移。

      如今我们回过头来看一下while循环的条件while (nextTop < end && pos < mItemCount)。

    • nextTop < end确保了我们仅仅要将新增的子View可以覆盖ListView的界面就行了,比方ListView的高度最多显示10个子View。我们不是必需向ListView中增加11个子View。

    • pos < mItemCount确保了我们新增的子View在Adapter中都有相应的数据源item,比方ListView的高度最多显示10个子View,可是我们Adapter中一共才有5条数据,这样的情况下仅仅能向ListView中增加5个子View,从而不能填充满ListView的所有高度。

经过了上面的while循环之后,ListView对子View的增删就完毕了。即children中存放的就是要在后面画图过程中即将渲染的子View的集合。

上面while循环的方法体中调用了makeAndAddView方法。通过该方法会获得一个子View。并把该子View加入到ListView的children中。该方法的方法签名例如以下所看到的:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,            boolean selected)
  • 1
  • 2

其源代码例如以下所看到的:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,            boolean selected) {        View child;        if (!mDataChanged) {            // 假设数据源没发生变化,那么尝试用该position从RecycleBin的mActiveViews中获取可复用的View            child = mRecycler.getActiveView(position);            if (child != null) {                // 假设child 不为空,说明我们找到了一个已经存在的child,这样mActiveViews中存储的View就被直接复用了                // 调用setupChild,对child进行定位                setupChild(child, position, y, flow, childrenLeft, selected, true);                return child;            }        }        // 假设没可以从mActivieViews中直接复用View。那么就要调用obtainView方法获取View,该方法尝试间接复用RecycleBin中的mScrapViews中的View。假设不能间接复用,则创建新的View        child = obtainView(position, mIsScrap);        // 调用setupChild方法。进行定位和量算        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);        return child;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

我们重点说一下前两个參数position和y,position表示的是数据源item在Adapter中的索引。y表示要生成的View的top值或bottom值。假设第三个參数flow是true,那么y表示top值,否则表示bottom值。

  • 假设数据源没发生变化,那么尝试用该position从RecycleBin的mActiveViews中获取可复用的View。RecycleBin的getActiveView方法接收一个position參数,能够在RecycleBin的mActiveViews数组中查找有没有相应position的View。假设能找到就能够直接复用该View作为child了。

    举一个样例,假设在某一时刻ListView中显示了10个子View,position依次为从0到9。

    然后我们手指向上滑动,且向上滑动了一个子View的高度,ListView须要绘制下一帧。这时候ListView在layoutChildren方法中把这10个子View都放入到了RecycleBin的mActiveViews数组中了,然后清空了children数组。然后调用fillDown方法,向ListView中依次加入position1到10的子View。在加入position为1的子View的时候,因为在上一帧中position为1的子View已经被放到mActiveViews数组中了。这次直接能够将其从mActiveViews数组中取出来,这样就是直接复用子View,所以说RecycleBin的mActiveViews数组主要是用于直接复用的。

    在直接复用了子View后,我们须要调用setupChild方法。该方法会将child加入到ListView的children数组中,并对child进行定位。

  • 假设没可以从mActivieViews中直接复用View。那么就要调用obtainView方法获取View。该方法尝试间接复用RecycleBin中的mScrapViews中的View,假设不能间接复用,则创建新的View。

    在通过obtainView获取了View之后,调用setupChild方法。该方法会将child加入到ListView的children数组中,并对child进行定位和量算。

以下我们再来看一下obtainView方法,该方法的方法签名例如以下所看到的:

View obtainView(int position, boolean[] isScrap)
  • 1

该方法接收position參数,其关键的源代码有下面两行:

final View scrapView = mRecycler.getScrapView(position);final View child = mAdapter.getView(position, scrapView, this);
  • 1
  • 2

通过调用RecycleBin的getScrapView方法。从mScrapViews数组中获取一个View,该View是用来间接复用的,该View可能为null,也可能不为null。将其作为我们熟悉的convertView传递给Adapter的getView方法,这样我们就能够在AdapterView的getView方法中通过推断convertView是否为空进行间接复用了。

希望本文对大家理解ListView的RecycleBin机制有所帮助!

转载于:https://www.cnblogs.com/zsychanpin/p/7269041.html

你可能感兴趣的文章
hello world``````````
查看>>
利用android Matrix来处理简单图片
查看>>
第九周总结
查看>>
Microsoft Hololens开发上手(3)
查看>>
大数据时代之你不得不了解的大数据概念
查看>>
倒排索引
查看>>
【学习笔记】C# 构造和析构
查看>>
黑客新手入门
查看>>
PHPSTORM/IntelliJ IDEA 常用 设置配置优化
查看>>
python爬虫入门10.16
查看>>
MVC,MVP 和 MVVM 的图示
查看>>
Sql Server 的DataReader 与 DataSet
查看>>
关于NSA的EternalBlue(永恒之蓝) ms17-010漏洞利用
查看>>
数据结构之B进制(确定进制)
查看>>
python小白-day9 数据库操作与Paramiko模块
查看>>
git push 冲突
查看>>
自然连接(natural join)
查看>>
Python pyspider HTTP 599 错误
查看>>
Data visualization 课程 笔记3
查看>>
Lucene的搭建(3)
查看>>