ListView和GridView的缓存机制及measure过程
目錄
前言
1、View的Transient狀態(tài)
2、RecycleBin
3、obtainView
4、getView的調用
5、GridView的onMeasure
6、ListView的onMeasure
前言
在Android開發(fā)中我們經常使用ListView和GridView,它們都有一套緩存機制,通過復用防止view的不停創(chuàng)建。
ListView和GridView都是AbsListView的子類,使用其內部類RecycleBin來進行view的緩存。
?
1、View的Transient狀態(tài)
要想搞懂RecycleBin的緩存機制,我們首先要了解Transient和Scrap都是什么。
Transient是View的一種狀態(tài),可以通過View的hasTransientState函數來判斷,官方解釋如下:
A view with transient state cannot be trivially rebound from an external data source, such as an adapter binding item views in a list. This may be because the view is performing an animation, tracking user selection of content, or similar.
從解釋上看,Transient是指View的一種不穩(wěn)定狀態(tài),是瞬時狀態(tài),比如說正在執(zhí)行一個動畫,有可能下一秒就改變了。
而Scrap則是ListView和GridView的緩存狀態(tài),當一個Item不可見被回收后存入緩存。
?
2、RecycleBin
在RecycleBin中與緩存相關的有三個List:
private ArrayList<View>[] mScrapViews; private SparseArray<View> mTransientStateViews; private LongSparseArray<View> mTransientStateViewsById;其中mTransientStateViews和mTransientStateViewsById都是緩存Transient狀態(tài)的view的,而mScrapViews則是緩存Scrap狀態(tài)的view。
我們從添加緩存開始來看,來看RecycleBin的addScrapView函數,部分代碼如下:
在這里我們先判斷view是否處于Transient狀態(tài),如果是Transient,則將其保存至mTransientStateViews或mTransientStateViewsById中。
至于到底保存到哪個list中,則通過mAdapterHasStableIds變量來判斷,mAdapterHasStableIds則是通過Adapter的hasStableIds函數獲得的,這個函數是需要子類去實現,它的含義是Adapter擁有穩(wěn)定的ItemId,即Adapter中同一個Object的ItemId是固定不變的,這就需要我們一定要重寫Adapter的getItemId方法,否則這里就會出現問題。
關于ItemId這部分,在AbsListView的setItemViewLayoutParams可以查看到:
回到addScrapView函數,如果不是Transient狀態(tài),則會將child保存到mScrapViews中。
3、obtainView
前面我們看到了添加緩存的過程,那么在哪里使用呢?
obtainView函數是AbsListView的一個函數,用于獲取每個item的view,其中就包括使用緩存機制。
這個函數的代碼如下:
這里面包含兩個部分
第一部分:
通過RecycleBin的getTransientStateView獲取transient狀態(tài)的view。
如果存在對應position的transient狀態(tài)的view,再判斷transientView的viewType與這個position的ViewType是否一致。
如果ViewType一致,則調用Adapter的getView方法獲取child,而transientView作為convertView參數。
如果得到的child的view與transientView不是同一個對象,比如getView中未使用convertView,則將child添加進ScrapView緩存中。
第一部分結束直接return了,不會繼續(xù)執(zhí)行下一部分。
第二部分:
如果不存在transient狀態(tài)的view,即getTransientStateView獲取的是null,那么通過RecycleBin的getScrapView函數從緩存列表中獲取一個scrapView。
注意這里沒有判斷ViewType,是因為getScrapView函數內部進行判斷處理了。
然后調用Adapter的getView方法獲取child,而將scrapView作為convertView參數。
最后同樣判斷得到的child的view與scrapView是不是同一個對象,不是則添加進ScrapView緩存。
4、getView的調用
以上就是ListView和GridView的緩存機制。
那么我們來思考另外一個問題:
經常使用Adapter的同學可能會發(fā)現,當初始化頁面的時候,getView的調用并不是從0到count走一遍即可。那么為什么會這樣?這樣的意義在哪?
這就要從ListView和GridView的measuer說起。
5、GridView的onMeasure
先來看看GridView的onMeasure方法,關鍵代碼如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);...mItemCount = mAdapter == null ? 0 : mAdapter.getCount();final int count = mItemCount;if (count > 0) {final View child = obtainView(0, mIsScrap);AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();...child.measure(childWidthSpec, childHeightSpec);childHeight = child.getMeasuredHeight();childState = combineMeasuredStates(childState, child.getMeasuredState());if (mRecycler.shouldRecycleViewType(p.viewType)) {mRecycler.addScrapView(child, -1);}}if (heightMode == MeasureSpec.UNSPECIFIED) {heightSize = mListPadding.top + mListPadding.bottom + childHeight +getVerticalFadingEdgeLength() * 2;}if (heightMode == MeasureSpec.AT_MOST) {int ourSize = mListPadding.top + mListPadding.bottom;final int numColumns = mNumColumns;for (int i = 0; i < count; i += numColumns) {ourSize += childHeight;if (i + numColumns < count) {ourSize += mVerticalSpacing;}if (ourSize >= heightSize) {ourSize = heightSize;break;}}heightSize = ourSize;}...setMeasuredDimension(widthSize, heightSize);mWidthMeasureSpec = widthMeasureSpec; }
當GridView有Adapter且其count>0時,通過obtainView這個函數獲取到了position為0的child。
在這里就解釋來getView的調用問題,因為通過前面內存我們知道obtainView函數中調用了getView,所以對于GridView來說position為0的getView會提前被調用一次。
那么這里為什么要得到這個child?
我們繼續(xù)向下看,拿到child之后收到調用了它的measure函數進行自身測量,然后拿到child的高度MeasuredHeight。
繼續(xù)向下看,當height的SpecMode為UNSPECIFIED或AT_MOST時,則需要用這個child的MeasuredHeight去計算GridView的高度。
當SpecMode為UNSPECIFIED時,GridView的高度只是一個child的高度,這就是為什么在ListView或ScrollView中嵌套GridView只顯示一行的原因。
當SpecMode為AT_MOST,需要考慮GridView的ColumnNum,GridView的高度實際上是第一個child的高度和rowNum的乘積,并且加上垂直方向的間隔mVerticalSpacing。
上面的嵌套情況,我們一般的做法是將GridView完全撐開,即自定義一個GridView并重寫onMeasuer方法,代碼如下:
這種情況下正是SpecMode為AT_MOST的情況,注意這時的GridView的撐開的高度只與第一個child的高度有關!
6、ListView的onMeasure
上面我們研究了GridView的onMeasure,下面來看看ListView的onMeasure,代碼如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// Sets up mListPaddingsuper.onMeasure(widthMeasureSpec, heightMeasureSpec);...mItemCount = mAdapter == null ? 0 : mAdapter.getCount();if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED|| heightMode == MeasureSpec.UNSPECIFIED)) {final View child = obtainView(0, mIsScrap);// Lay out child directly against the parent measure spec so that// we can obtain exected minimum width and height.measureScrapChild(child, 0, widthMeasureSpec, heightSize);childWidth = child.getMeasuredWidth();childHeight = child.getMeasuredHeight();childState = combineMeasuredStates(childState, child.getMeasuredState());if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(((LayoutParams) child.getLayoutParams()).viewType)) {mRecycler.addScrapView(child, 0);}}...if (heightMode == MeasureSpec.UNSPECIFIED) {heightSize = mListPadding.top + mListPadding.bottom + childHeight +getVerticalFadingEdgeLength() * 2;}if (heightMode == MeasureSpec.AT_MOST) {// TODO: after first layout we should maybe start at the first visible position, not 0heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);}setMeasuredDimension(widthSize, heightSize);mWidthMeasureSpec = widthMeasureSpec; }
同樣,當有Adapter且其count>0時,通過obtainView這個函數獲取到了第一個child。
然后我們沒有看到child的measure函數,但是執(zhí)行了一個measureScrapChild函數,這個函數中對child進行了一次measure,這里就不貼出代碼了。
在高度計算方面,SpecMode為UNSPECIFIED時與GridView一樣,這也解釋了ScrollView或ListView嵌套ListView為啥只顯示一行。
與GridView一樣,解決嵌套問題也是自定義ListView并重寫onMeasure方法。
但是這里SpecMode為AT_MOST的情況與GridView有所不同,我們看到執(zhí)行了一個measureHeightOfChildren函數,這個函數代碼如下:
當Adapter不為空,這時startPosition是0,而endPosition是count-1。
再往下看,發(fā)現會遍歷拿到所有的child,并通過measureScrapChild函數執(zhí)行它們的measure函數。
并且將這些child的高度累加起來,同時還會加上divider的高度。
這里就與GridView有所不同了。GridView只用了第一個child去做乘積,而ListView則用到了所有child。所以當SpecMode不是AT_MOST時,ListView之后提前調用一次getView,position 是0。但是如果SpecMode是AT_MOST時,ListView先調用一次position為0的getView,然后再遍歷調用一遍所有的getView,如果算上添加布局時的調用,第一個child的getView就會被調用三次!
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的ListView和GridView的缓存机制及measure过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android中如何使控件保持固定宽高比
- 下一篇: resource.arsc二进制内容解析