Cache mechanism and measurement process of ListView and GridView

Android Advanced Road Series: http://blog.csdn.net/column/details/16488.html


In Android development, we often use ListView and GridView, both of which have a set of caching mechanisms to prevent the continuous creation of views through reuse.

Both ListView and GridView are subclasses of AbsListView and use their internal class RecycleBin to cache views.


1. Transient state of View

To understand the caching mechanism of RecycleBin, we must first understand what Transient and Scrap are.
Transient is a state of View, which can be judged by the hasTransientState function of View. The official explanation is as follows:

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.


From the interpretation point of view, Transient refers to an unstable state of the View, which is a transient state. For example, when an animation is being performed, it may be in the next second. changed.

Scrap is the cache state of ListView and GridView. When an item is not visible and is recycled, it is stored in the cache.


2、RecycleBin

There are three Lists related to caching in RecycleBin:
private ArrayList<View>[] mScrapViews;
private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;
Among them, mTransientStateViews and mTransientStateViewsById are the views that cache the Transient state, and mScrapViews are the views that cache the Scrap state.
Let's start with adding a cache and look at the addScrapView function of RecycleBin. Part of the code is as follows:
void addScrapView(View scrap, int position) {
    ...


    // Don't scrap views that have transient state.
    final boolean scrapHasTransientState = scrap.hasTransientState();
    if (scrapHasTransientState) {
        if (mAdapter != null && mAdapterHasStableIds) {
            // If the adapter has stable IDs, we can reuse the view for
            // the same data.
            if (mTransientStateViewsById == null) {
                mTransientStateViewsById = new LongSparseArray<>();
            }
            mTransientStateViewsById.put(lp.itemId, scrap);
        } else if (!mDataChanged) {
            // If the data hasn't changed, we can reuse the views at
            // their old positions.
            if (mTransientStateViews == null) {
                mTransientStateViews = new SparseArray<>();
            }
            mTransientStateViews.put(position, scrap);
        } else {
            // Otherwise, we'll have to remove the view and start over.
            getSkippedScrap().add(scrap);
        }
    } else {
        if (mViewTypeCount == 1) {
            mCurrentScrap.add(scrap);
        } else {
            mScrapViews[viewType].add(scrap);
        }


        if (mRecyclerListener != null) {
            mRecyclerListener.onMovedToScrapHeap(scrap);
        }
    }
}

Here we first determine whether the view is in the Transient state, and if it is Transient, save it to mTransientStateViews or mTransientStateViewsById.

As for which list is saved, it is judged by the mAdapterHasStableIds variable. mAdapterHasStableIds is obtained through the hasStableIds function of the Adapter. This function needs to be implemented by subclasses. It means that the Adapter has a stable ItemId, that is, the same one in the Adapter. The ItemId of Object is fixed, which requires us to rewrite the getItemId method of the Adapter, otherwise there will be problems here.


Regarding the ItemId part, you can see it in the setItemViewLayoutParams of AbsListView:
private void setItemViewLayoutParams(View child, int position) {
    final ViewGroup.LayoutParams vlp = child.getLayoutParams();
    ...


    if (mAdapterHasStableIds) {
        lp.itemId = mAdapter.getItemId(position);
    }
    lp.viewType = mAdapter.getItemViewType(position);
    if (lp != vlp) {
      child.setLayoutParams(lp);
    }
}

Back to the addScrapView function, if it is not in the Transient state, the child will be saved to mScrapViews.


3、obtainView

We saw the process of adding a cache earlier, so where to use it?

The obtainView function is a function of AbsListView, which is used to obtain the view of each item, including the use of the cache mechanism.

The code for this function is as follows:
View obtainView(int position, boolean[] isScrap) {
    ...


    // Check whether we have a transient state view. Attempt to re-bind the
    // data and discard the view if we fail.
    final View transientView = mRecycler.getTransientStateView(position);
    if (transientView != null) {
        final LayoutParams params = (LayoutParams) transientView.getLayoutParams();


        // If the view type hasn't changed, attempt to re-bind the data.
        if (params.viewType == mAdapter.getItemViewType(position)) {
            final View updatedView = mAdapter.getView(position, transientView, this);


            // If we failed to re-bind the data, scrap the obtained view.
            if (updatedView != transientView) {
                setItemViewLayoutParams(updatedView, position);
                mRecycler.addScrapView(updatedView, position);
            }
        }


        isScrap[0] = true;


        // Finish the temporary detach started in addScrapView().
        transientView.dispatchFinishTemporaryDetach();
        return transientView;
    }


    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 {
            isScrap[0] = true;
            // Finish the temporary detach started in addScrapView().
            child.dispatchFinishTemporaryDetach();
        }
    }


    ...


    return child;
}

This contains two parts.

The first part:
Get the view of transient state through getTransientStateView of RecycleBin.
If there is a view of the transient state corresponding to the position, then judge whether the viewType of the transientView is consistent with the ViewType of this position.
If the ViewType is the same, call the getView method of the Adapter to get the child, and the transientView is used as the convertView parameter.
If the obtained child's view and transientView are not the same object, for example, convertView is not used in getView, add the child to the ScrapView cache.
The first part ends and returns directly, and will not continue to execute the next part.

The second part:
If there is no transient state view, that is, getTransientStateView gets null, then get a scrapView from the cache list through the getScrapView function of RecycleBin.
Note that the ViewType is not judged here, because the judgment processing is performed inside the getScrapView function.
Then call the getView method of the Adapter to get the child, and use the scrapView as the convertView parameter.
Finally, it is also judged whether the obtained child's view and scrapView are the same object, and if not, it will be added to the ScrapView cache.


4. The call of getView


The above is the caching mechanism of ListView and GridView.

Then let's think about another question:
students who often use Adapter may find that when initializing the page, the call to getView does not just go from 0 to count. So why is this happening? What is the meaning of this?

This starts with the measurer of ListView and GridView.



5、GridView的onMeasure

Let's take a look at the onMeasure method of GridView first. The key code is as follows:
@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;
}

When the GridView has an Adapter and its count>0, the child whose position is 0 is obtained through the obtainView function.
The problem of calling getView is explained here, because we know that getView is called in the obtainView function through the previous memory, so for GridView, getView with position 0 will be called once in advance.
So why get this child here?
We continue to look down, and after getting the child, we receive the measure function that calls it to measure itself, and then get the height of the child, MeasuredHeight.
Continue to look down, when the SpecMode of the height is UNSPECIFIED or AT_MOST, you need to use the MeasuredHeight of the child to calculate the height of the GridView.

When SpecMode is UNSPECIFIED, the height of GridView is only the height of a child, which is why nesting GridView in ListView or ScrollView only displays one row.

When the SpecMode is AT_MOST, the ColumnNum of the GridView needs to be considered. The height of the GridView is actually the product of the height of the first child and the rowNum, plus the vertical interval mVerticalSpacing.
In the above nesting situation, our general practice is to completely open the GridView, that is, customize a GridView and override the onMeasuer method. The code is as follows:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
         MeasureSpec.AT_MOST);
   super.onMeasure(widthMeasureSpec, expandSpec);
}

In this case, the SpecMode is AT_MOST. Note that the height of the GridView at this time is only related to the height of the first child!



6、ListView的onMeasure

Above we have studied onMeasure of GridView, let's take a look at onMeasure of ListView, the code is as follows:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // Sets up mListPadding
    super.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 0
        heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
    }


    setMeasuredDimension(widthSize, heightSize);


    mWidthMeasureSpec = widthMeasureSpec;
}

Similarly, when there is an Adapter and its count>0, the first child is obtained through the obtainView function.
Then we didn't see the child's measure function, but executed a measureScrapChild function, which performed a measure on the child, so I won't post the code here.
In terms of height calculation, when SpecMode is UNSPECIFIED, it is the same as GridView, which also explains why ScrollView or ListView nested ListView only displays one row.
As with GridView, the solution to the nesting problem is to customize the ListView and override the onMeasure method.
But the case where SpecMode is AT_MOST is different from GridView. We see that a measureHeightOfChildren function is executed. The code of this function is as follows:
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
        int maxHeight, int disallowPartialChildPosition) {
    final ListAdapter adapter = mAdapter;
    if (adapter == null) {
        return mListPadding.top + mListPadding.bottom;
    }


    ...


    endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
    ...


    for (i = startPosition; i <= endPosition; ++ i) {
        child = obtainView(i, isScrap);


        measureScrapChild(child, i, widthMeasureSpec, maxHeight);


        if (i > 0) {
            // Count the divider for all but one child
            returnedHeight += dividerHeight;
        }


        // Recycle the view before we possibly return from the method
        if (recyle && recycleBin.shouldRecycleViewType(
                ((LayoutParams) child.getLayoutParams()).viewType)) {
            recycleBin.addScrapView(child, -1);
        }


        returnedHeight += child.getMeasuredHeight();


        ...
    }


    // At this point, we went through the range of children, and they each
    // completely fit, so return the returnedHeight
    return returnedHeight;
}

When the Adapter is not empty, startPosition is 0, and endPosition is count-1.
Looking further down, it is found that all children will be traversed and their measure functions will be executed through the measureScrapChild function.
And the height of these children is accumulated, and the height of the divider is also added.

This is different from GridView. GridView only uses the first child to do the product, while ListView uses all children. So when SpecMode is not AT_MOST, ListView calls getView in advance and the position is 0. But if SpecMode is AT_MOST, ListView first calls getView with position 0, and then traverses and calls all getViews again. If you count the calls when adding layout, the first child's getView will be called three times!


Android Advanced Road Series: http://blog.csdn.net/column/details/16488.html

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324843083&siteId=291194637