表明转载自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增删相关的关键代码,主要分下面三个阶段:
- ListView的children->RecycleBin
- ListView清空children
- RecycleBin->ListView的children
在layout这种方法刚刚開始运行的时候,ListView中的children事实上还是上一帧中须要绘制的子View的集合,在layout这种方法运行完毕的时候,ListView中的children就变成了当前帧立即要进行绘制的子View的集合。
以下对以上这三个阶段分别说明。
-
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进行复用。
-
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
-
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方法为例具体说明。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机制有所帮助!