系列文章:
前言
ListView
是用來展示大量數(shù)據(jù)的控件沪铭,且不會因為展示大量數(shù)據(jù)而出現(xiàn)內(nèi)存溢出的現(xiàn)象喘蟆,其原因是相關(guān)緩存機制保證了內(nèi)存的合理使用。
ListView的使用也相對比較簡單蚁吝,大家也都會慨飘,現(xiàn)在官方基本都推薦使用RecyclerView去替代ListView某残,二者之間有相似之處,也有不同之處蹈丸,本文先分析ListView的源碼成黄,重點是緩存的實現(xiàn)原理,后續(xù)再補充RecyclerView的原理分析逻杖,并將二者進行對比討論奋岁。
ListView的使用可以參考ListView簡單實用
ListView繼承自AbsListView,AbsListView又繼承自AdapterView荸百,AdapterView繼承自ViewGroup闻伶。
RecycleBin機制
在ListView的緩存機制中,有一個類我們必須提前了解:RecycleBin
够话,它是ListView緩存的核心機制蓝翰。RecycleBin是AbsListView
的一個內(nèi)部類光绕,而ListView繼承AbsListView,所以ListView可以使用這個機制畜份。下面是RecycleBin的部分關(guān)鍵源碼:
class RecycleBin {
private RecyclerListener mRecyclerListener;
private int mFirstActivePosition;
// 存儲View
private View[] mActiveViews = new View[0];
// 存儲廢棄View
private ArrayList<View>[] mScrapViews;
private int mViewTypeCount;
// 存儲廢棄View
private ArrayList<View> mCurrentScrap;
// Adapter當中可以重寫一個getViewTypeCount()來表示ListView中有幾種類型的數(shù)據(jù)項
// 而setViewTypeCount()方法的作用就是為每種類型的數(shù)據(jù)項都單獨啟用一個RecycleBin緩存機制
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
//noinspection unchecked
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;
}
// 第一個參數(shù)表示要存儲的view的數(shù)量诞帐,第二個參數(shù)表示ListView中第一個可見元素的position值
// 根據(jù)傳入的參數(shù)來將ListView中的指定元素存儲到mActiveViews數(shù)組當中。
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();
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
activeViews[i] = child;
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
// 從mActiveViews數(shù)組當中獲取數(shù)據(jù)
// 該方法接收一個position參數(shù)爆雹,表示元素在ListView當中的位置
// 方法內(nèi)部會自動將position值轉(zhuǎn)換成mActiveViews數(shù)組對應的下標值
// mActiveViews當中所存儲的View停蕉,一旦被獲取了之后就會從mActiveViews當中移除
// 下次獲取同樣位置的View將會返回null,也就是說mActiveViews不能被重復利用
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是沒有順序可言的
// 因此getScrapView()方法中的算法也非常簡單
// 就是直接從mCurrentScrap當中獲取尾部的一個scrap view進行返回
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
// 將一個廢棄的View進行緩存
// 該方法接收一個View參數(shù)谷徙,當有某個View確定要廢棄掉的時候(比如滾動出了屏幕)
// 應該調(diào)用這個方法來對View進行緩存
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
lp.scrappedFromPosition = position;
final int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
getSkippedScrap().add(scrap);
}
return;
}
scrap.dispatchStartTemporaryDetach();
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
if (mAdapter != null && mAdapterHasStableIds) {
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<>();
}
mTransientStateViewsById.put(lp.itemId, scrap);
} else if (!mDataChanged) {
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<>();
}
mTransientStateViews.put(position, scrap);
} else {
getSkippedScrap().add(scrap);
}
} else {
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
}
下面就上述幾個關(guān)鍵變量和方法做一些說明:
- mActiveViews:用來存放正在展示在屏幕上的view,從顯示在屏幕山上的第一個view到最后一個view
- mScrapViews:存放可以由適配器用作convert view的view驯绎,是一個數(shù)組完慧,數(shù)組的每個元素類型為ArrayList<View>
- mCurrentScrap:是mScrapViews的第0個元素,當view種類數(shù)量為1時存放廢棄view
- fillActiveViews():這個方法接收兩個參數(shù)剩失,第一個參數(shù)表示mActiveViews數(shù)組最小要保存的View數(shù)量屈尼,第二個參數(shù)表示ListView中第一個可見元素的position值。根據(jù)傳入的參數(shù)來將ListView中的指定元素存儲到mActiveViews數(shù)組當中拴孤。
- getActiveView():從mActiveViews數(shù)組當中取出特定元素脾歧,position參數(shù)表示元素在ListView當中的位置,方法內(nèi)部會自動將position值轉(zhuǎn)換成mActiveViews數(shù)組對應的下標值演熟,mActiveViews當中所存儲的View鞭执,一旦被獲取了之后就會從mActiveViews當中移除,下次獲取同樣位置的View將會返回null芒粹,也就是說mActiveViews不能被重復利用兄纺。如果在mActiveViews數(shù)組中沒有找到,則返回null化漆。
- addScrapView():將一個廢棄的View進行緩存估脆,該方法接收一個View參數(shù),當有某個View確定要廢棄掉的時候(比如滾動出了屏幕)座云,應該調(diào)用這個方法來對View進行緩存疙赠,當view類型為1時則用mCurrentScrap存儲廢棄view,否則使用mScrapViews添加廢棄view朦拖。
- getScrapView(): 從廢棄緩存中取出一個View圃阳,這些廢棄緩存中的View是沒有順序可言,就是直接從mCurrentScrap當中獲取尾部的一個scrap view進行返回
- setViewTypeCount():Adapter當中可以重寫一個getViewTypeCount()來表示ListView中有幾種類型的數(shù)據(jù)項璧帝,而setViewTypeCount()方法的作用就是為每種類型的數(shù)據(jù)項都單獨啟用一個RecycleBin緩存機制
RecycleBin類的核心方法和變量的解釋暫且放在此處捍岳,后續(xù)講解時會用到此處信息。
ListView的繪制流程
ListView本質(zhì)上還是一個View,因此繪制的過程還是分為三步:onMeasure祟同、onLayout作喘、onDraw,onMeasure測出其占用屏幕空間晕城,最大為整個屏幕泞坦,而onDraw用于將ListView內(nèi)容繪制到屏幕上,在ListView中無實際意義砖顷,因為ListView本身只是提供了一種布局方式贰锁,真正的繪制是ListView中的子View完成的,因此onLayout方法是最為關(guān)鍵的滤蝠。
onLayout方法
ListView的OnLayout實現(xiàn)在AbsListView中豌熄,具體源碼如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
...
}
從上面代碼可以看出,首先調(diào)用了父類的onLayout方法物咳,再判斷ListView是否發(fā)生了變化(大小锣险,位置),如果ListView發(fā)生了變化览闰,則changed變量為true芯肤,changed為true則強制每個子布局都進行重新繪制,還需要注意一點的是同時還進行了mRecycler.markChildrenDirty()
這個操作压鉴,其中mRecycler就是一個RecycleBin的對象崖咨,而markChildrenDirty()方法會為每一個scrap view調(diào)用forceLayout();判斷完changed變量后又調(diào)用了layoutChildren()
方法油吭,點開此方法可以發(fā)現(xiàn)他是一個空方法击蹲,因為每個子元素的布局實現(xiàn)應該由自己來實現(xiàn),所以它的具體實現(xiàn)在ListView中婉宰。
layoutChildren方法
@Override
protected void layoutChildren() {
...
final int childCount = getChildCount();
...
boolean dataChanged = mDataChanged;
...
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
...
switch (mLayoutMode) {
...
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());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
...
}
它的方法過長歌豺,只貼出來一部分,此方法中首先會獲取子元素的數(shù)量芍阎,但此時ListView中還沒有任何子View世曾,因為數(shù)據(jù)都是由Adapter管理的,還沒有展示到界面上谴咸。接著又會判斷dataChanged
這個值,如果數(shù)據(jù)源發(fā)生變化則該值變?yōu)閠rue骗露,緊接著調(diào)用了RecycleBin的fillActiveViews()
方法岭佳;可是這時ListView中還沒有子View,因此fillActiveViews的緩存功能無法起作用萧锉。
那么我們再接著往下分析珊随,接下來又會判斷mLayoutMode
的值,默認情況下該值都是LAYOUT_NORMAL
,此模式下會直接進入default
語句中叶洞,其中有多次if條件判斷鲫凶,因為當前ListView中還沒有任何子View所以當前childCount
數(shù)量為0,mStackFromBottom
變量代表的是布局的順序衩辟,默認的布局順序是從上至下螟炫,因此會進入fillFromTop
方法中
fillFromTop方法
此方法具體代碼如下:
private View fillFromTop(int nextTop) {
mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
if (mFirstPosition < 0) {
mFirstPosition = 0;
}
return fillDown(mFirstPosition, nextTop);
}
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
fillFromTop首先計算出了mFirstPosition的值,并從mFirstPosition開始自頂至下調(diào)用fillDown
填充艺晴。
fillDown
中采用了while循環(huán)來填充昼钻,一開始時nextTop
的值是第一個子元素頂部距離整個ListView頂部的像素值,pos
是傳入的mFirstPosition
的值封寞,end是ListView底部減去頂部所得的像素值然评,mItemCount
是Adapter中的元素數(shù)量,因此nextTop是小于end的狈究,pos也小于mItemCount碗淌,每次執(zhí)行while循環(huán)時,pos加1抖锥,nextTop也會累加當nextTop大于end時贯莺,也就是子元素超出屏幕了,或者pos大于mItemCount時宁改,即Adapter中所有元素都被遍歷了缕探,出現(xiàn)以上兩種情況中一種便會跳出while循環(huán)。
在此while循環(huán)中还蹲,我們注意到調(diào)用了makeAddView
這個方法爹耗,下面具體分析下。
makeAddView方法
首先貼下代碼:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// Try to use an existing view for this position.
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
// Found it. We're reusing an existing child, so it just needs
// to be positioned like a scrap view.
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// Make a new view for this position, or convert an unused view if
// possible.
final View child = obtainView(position, mIsScrap);
// This needs to be positioned and measured.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
當Adapter的數(shù)據(jù)源未發(fā)生變化時谜喊,會從RecycleBin中獲取一個activeView潭兽,但是目前RecycleBin中還沒有緩存任何的View,因此這里得到的child為null斗遏,接著又調(diào)用了obtainView
方法來獲取一個View山卦,再來看看這個方法吧,源碼如下:
obtainView方法
View obtainView(int position, boolean[] outMetadata) {
outMetadata[0] = false;
...
final View scrapView = mRecycler.getScrapView(position);
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.
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
...
return child;
}
首先調(diào)用了RecycleBin
的getScrapView
方法來嘗試獲取一個廢棄緩存中的View诵次,但是這里是獲取不到的账蓉;接著又調(diào)用了getView
方法,即自定的Adapter中的getView方法逾一,getView方法接收三個參數(shù)铸本,第一個是當前子元素位置,第二個參數(shù)是convertView
遵堵,上面?zhèn)魅氲氖莕ull箱玷,說明沒有covertView可以利用怨规,因此在Adapter中判斷convertView為null時可以調(diào)用LayoutInflater的inflate
方法去加載一個布局,并將此view返回锡足。
同時我們可以看到波丰,這個view最終也會作為obtainView方法的返回結(jié)果,并傳入makeAddView方法中后續(xù)調(diào)用的setupChild()
方法中舶得,上面過程可以說明第一次layout過程中掰烟,所有子View都是調(diào)用LayoutInflater的inflate方法動態(tài)加載對應布局而產(chǎn)生的,解析布局的過程肯定是耗時的扩灯,但是在后續(xù)過程中媚赖,這種情況不會出現(xiàn)了。接下來珠插,繼續(xù)看下setupChild方法源碼:
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
...
addViewInLayout(child, flowDown ? -1 : 0, p, true);
....
}
在setupChild
方法中會調(diào)用addViewInLayout
方法將它添加到ListView中惧磺,那么回到fillDown
方法,其中的while循環(huán)就會讓子元素View將整個ListView控件填滿然后跳出捻撑,也就是說即使Adapter中有很多條數(shù)據(jù)磨隘,ListView也只會加載第一屏數(shù)據(jù)。下圖是第一次onLayout的過程:
第二次onLayout
即使是一個再簡單的View顾患,在展示到界面上之前都會經(jīng)歷至少兩次onMeasure()
和兩次onLayout()
的過程番捂,自然ListView的繪制過程也不例外,同樣我們關(guān)注的重點還是onLayout過程江解。
首先還是從layoutChildren()
方法看起:
layoutChildren方法
再來看一遍該方法源碼:
@Override
protected void layoutChildren() {
...
final int childCount = getChildCount();
...
boolean dataChanged = mDataChanged;
...
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
...
// Clear out old views
detachAllViewsFromParent();
switch (mLayoutMode) {
...
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());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
...
}
首先還是會獲取子元素的數(shù)量设预,不同于第一次onLayout,此時獲取到的子View數(shù)量不再為0犁河,而是ListView中顯示的子元素數(shù)量鳖枕;下面又調(diào)用了RecycleBin
的fillActiveViews()
方法,目前ListView已經(jīng)有子View了桨螺,這樣所有的子View都會被緩存到RecycleBin中的mActiveViews
數(shù)組中宾符,后面會使用到他們。
接下來有一個重要的方法:detachAllViewsFromParent()
灭翔,這個方法會將所有ListView當中的子View全部清除掉魏烫,從而保證第二次Layout過程不會產(chǎn)生一份重復的數(shù)據(jù),因為layoutChildren方法會向ListView中添加View肝箱,在第一次layout中已經(jīng)添加了一次哄褒,如果第二次layout繼續(xù)添加,那么必然會出現(xiàn)數(shù)據(jù)重復的問題狭园,因此這里先調(diào)用detachAllViewsFromParent方法將第一次添加的View清除掉读处。
這樣把已經(jīng)加載好的View又清除掉,待會還要再重新加載一遍唱矛,這不是嚴重影響效率嗎?不用擔心,我們剛剛調(diào)用了RecycleBin
的fillActiveViews()
方法來緩存子View绎谦,等會將直接使用這些緩存好的View來進行添加子View管闷,而并不會重新執(zhí)行一遍inflate過程,因此效率方面并不會有什么明顯的影響窃肠。
再進入判斷childCount
是否為0的邏輯中,此時會走和第一次layout相反的else邏輯分支冤留,這其中又有三條邏輯分支碧囊,第一條一般不成立,因為開始時我們還沒選中任何子View纤怒,第二條一般成立糯而,mFirstPosition開始時為0,只要Adapter中數(shù)據(jù)量大于0即可泊窘,所以進入了fillSpecific
方法:
fillSpecific方法
此方法源碼如下:
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
// Possibly changed again in fillUp if we add rows above this one.
mFirstPosition = position;
View above;
View below;
final int dividerHeight = mDividerHeight;
if (!mStackFromBottom) {
above = fillUp(position - 1, temp.getTop() - dividerHeight);
// This will correct for the top of the first view not touching the top of the list
adjustViewsUpOrDown();
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooHigh(childCount);
}
} else {
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
if (tempIsSelected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}
fillSpecific()
方法的功能和fillUp熄驼,fillDown差不多,但是在fillSpecific()
方法會優(yōu)先加載指定位置的View烘豹,再加載該View上下的其它子View瓜贾,由于這里我們傳入的position就是第一個子元素的位置,因此此時其效果和上述的fillDown()基本一致携悯。
此外可以看到祭芦,fillSpecific()
方法中也調(diào)用了makeAndAddView()
方法,因為我們之前調(diào)用detachAllViewsFromParent()
方法把所有ListView當中的子View全部清除掉了憔鬼,這里肯定要重新再加上龟劲,在makeAndAddView()
方法中:
makeAndAddView方法
再來看一遍此方法源碼:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// Try to use an existing view for this position.
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
// Found it. We're reusing an existing child, so it just needs
// to be positioned like a scrap view.
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// Make a new view for this position, or convert an unused view if
// possible.
final View child = obtainView(position, mIsScrap);
// This needs to be positioned and measured.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
首先還是會從RecycleBin中獲取ActiveView,不同于第一次layout逊彭,這次能獲取到了咸灿,那肯定就不會進入obtainView中了,而是直接調(diào)用setupChild()
方法侮叮,此時setupChild()
方法的最后一個參數(shù)是true避矢,表明當前的view是被回收過的,再來看看setupChild()
方法源碼:
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
...
if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
...
} else {
...
}
...
}
可以看到囊榜,setupChild()
方法的最后一個參數(shù)是isAttachedToWindow
审胸,方法執(zhí)行過程中會對這個變量進行判斷,由于isAttachedToWindow
現(xiàn)在是true卸勺,所以會執(zhí)行attachViewToParent()
方法砂沛,而第一次Layout過程則是執(zhí)行的else語句中的addViewInLayout()
方法。
這兩個方法最大的區(qū)別在于曙求,如果我們需要向ViewGroup中添加一個新的子View碍庵,應該調(diào)用addViewInLayout()
方法映企,而如果是想要將一個之前detach的View重新attach到ViewGroup上,就應該調(diào)用attachViewToParent()
方法静浴。那么由于前面在layoutChildren()方法當中調(diào)用了detachAllViewsFromParent()
方法堰氓,這樣ListView中所有的子View都是處于detach狀態(tài)的,所以這里attachViewToParent()
方法是正確的選擇苹享。
經(jīng)歷了這樣一個detach又attach的過程双絮,ListView中所有的子View又都可以正常顯示出來了,那么第二次Layout過程結(jié)束得问。
下圖展示了第二次onLayout的過程:
ListView如何做到滑動加載更多子View?
onTouchEvent方法
通過以上兩次layout宫纬,我們已經(jīng)能看到ListView的第一屏內(nèi)容了焚挠,但是如果Adapter中有大量數(shù)據(jù),剩下的數(shù)據(jù)怎么加載呢哪怔?我們知道實際使用過程中宣蔚,ListView滑動的時候剩余的數(shù)據(jù)便顯示出來了,那滑動首先肯定要監(jiān)聽觸摸事件认境,相關(guān)代碼在AbsListView
中的onTouchEvent
中:
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
switch (actionMasked) {
...
case MotionEvent.ACTION_MOVE: {
onTouchMove(ev, vtev);
break;
}
...
}
...
return true;
}
我們主要關(guān)注ACTION_MOVE
滑動事件胚委,因為ListView是隨著滑動而加載更多子View的,其中調(diào)用了onTouchMove
方法:
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
...
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
...
case TOUCH_MODE_SCROLL:
case TOUCH_MODE_OVERSCROLL:
scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
break;
}
}
此方法中判斷了mTouchMode
叉信,當手指在屏幕上滑動時亩冬,TouchMode
是等于TOUCH_MODE_SCROLL
,接下來調(diào)用了scrollIfNeeded()
方法:
scrollIfNeeded方法
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
...
final int deltaY = rawDeltaY;
int incrementalDeltaY =
mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
int lastYCorrection = 0;
if (mTouchMode == TOUCH_MODE_SCROLL) {
...
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
...
}
}
...
}
注意到其中會調(diào)用trackMotionScroll
方法硼身,只要我們手指滑動了一點距離硅急,此方法就會被調(diào)用,自然如果手指在屏幕上滑動了很多佳遂,此方法就會被調(diào)用很多次营袜。
trackMotionScroll方法
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
...
final boolean down = incrementalDeltaY < 0;
...
if (down) {
int top = -incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
top += listPadding.top;
}
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
child.clearAccessibilityFocus();
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) {
// The view will be rebound to new data, clear any
// system-managed transient state.
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
}
...
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
...
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
...
return false;
}
這個方法接收兩個參數(shù),一個是deltaY丑罪,表示手指從按下的位置到當前手指位置的Y方向的距離荚板,incrementalDeltaY
則表示距離上次觸發(fā)event事件手指在Y方向上位置的改變量,可以通過incrementalDeltaY
的正負知道用戶是往上還是往下滑動吩屹。
如果incrementalDeltaY<0
跪另,說明是向下滑動,進入if (down)
分支中煤搜,其中有個for循環(huán)免绿,從上往下獲取子View,如果子View的bottom小于ListView的Top說明這個子View已經(jīng)移出屏幕了擦盾,則調(diào)用RecycleBin的addScrapView
方法將其加入到廢棄緩存中嘲驾,并將計數(shù)器count+1淌哟,計數(shù)器用于記錄有多少個子View被移出了屏幕。
那么如果是ListView向上滑動的話距淫,其實過程是基本相同的绞绒,只不過變成了從下往上依次獲取子View婶希,然后判斷該子View的top值是不是大于bottom值了榕暇,如果大于的話說明子View已經(jīng)移出了屏幕,同樣把它加入到廢棄緩存中喻杈,并將計數(shù)器加1彤枢。
接下來,如果count大于0筒饰,說明有子View被加入廢棄緩存了缴啡,則會調(diào)用detachViewsFromParent()
方法將所有移出屏幕的子View全部detach掉。有View被移出瓷们,那么自然就需要添加新的View业栅,所以如果ListView中最后一個View的底部已經(jīng)移入了屏幕,或者ListView中第一個View的頂部移入了屏幕谬晕,就會調(diào)用fillGap()
方法碘裕,fillGap()
方法是用來加載屏幕外數(shù)據(jù)的,如下所示:
fillGap方法
此方法的實現(xiàn)在ListView中攒钳,
void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
int paddingTop = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingTop = getListPaddingTop();
}
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
paddingTop;
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
int paddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingBottom = getListPaddingBottom();
}
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - paddingBottom;
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}
fillGap
接受一個down參數(shù)帮孔,此參數(shù)代表之前ListView是向下還是向上滑動,如果向下則調(diào)用fillDown()
方法不撑,如果向上滑動則調(diào)用fillUp()
方法文兢,這兩個方法之前已經(jīng)說過了,內(nèi)部有一個while循環(huán)來對ListView進行填充焕檬,填充的過程是通過makeAndAddView
來實現(xiàn)的姆坚,好吧,再去makeAndAddView
方法中看看实愚。
makeAndAddView方法
這是第三次來看此方法源碼了:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// Try to use an existing view for this position.
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
// Found it. We're reusing an existing child, so it just needs
// to be positioned like a scrap view.
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// Make a new view for this position, or convert an unused view if
// possible.
final View child = obtainView(position, mIsScrap);
// This needs to be positioned and measured.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
首先還是從RecycleBin
中獲取activeView
兼呵,不過此時已經(jīng)獲取不到了,因為第二次layout過程中被獲取過了爆侣,所以只好調(diào)用obtainView
方法了萍程。
obtainView方法
View obtainView(int position, boolean[] outMetadata) {
...
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
...
return child;
}
這里會調(diào)用mRecycler.getScrapView
方法來獲取廢棄的緩存View,而剛好我們前面在trackMotionScroll
方法中處理已移出屏幕的View時將其加入廢棄緩存view中了兔仰,也就是說一旦有任何子View被移出了屏幕茫负,就會將它加入到廢棄緩存中,而從obtainView()
方法中的邏輯來看乎赴,一旦有新的數(shù)據(jù)需要顯示到屏幕上忍法,就會嘗試從廢棄緩存中獲取View潮尝。
所以它們之間就形成了一個生產(chǎn)者和消費者的模式,那么ListView神奇的地方也就在這里體現(xiàn)出來了饿序,不管你有任意多條數(shù)據(jù)需要顯示勉失,ListView中的子View其實來來回回就那么幾個,移出屏幕的子View會很快被移入屏幕的數(shù)據(jù)重新利用起來原探,因而不管我們加載多少數(shù)據(jù)都不會出現(xiàn)OOM的情況乱凿,甚至內(nèi)存都不會有所增加。
還有一點需要注意徒蟆,我們將獲取到的scrapView
傳入了mAdapter.getView()
方法中,那么這個參數(shù)具體是什么用呢型型,我們來看一個Adapter的getView例子:
public View getView(int position, View convertView, ViewGroup parent) {
String url = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
} else {
view = convertView;
}
...
return view;
}
第二個參數(shù)就是convertView
段审,所以當這個參數(shù)不為null的時候,我們會直接復用此View闹蒜,然后更新下對應的數(shù)據(jù)即可寺枉,而為null時才去加載布局文件。
再之后的代碼就是調(diào)用setupChild()
方法绷落,將獲取到的view重新attach到ListView當中姥闪,因為廢棄緩存中的View也是之前從ListView中detach掉的。
至此已經(jīng)基本將ListView的工作原理說清楚了嘱函,下圖是滑動時ListView工作原理:
總結(jié)一下ListView的緩存原理如下: