安卓高手之路之图形系统(6)ListView继续 - 修补C++ - ITeye技术网站
综述:
本篇首先介绍了ListView的实现细节。然后介绍了Gallery,ListView,ViewPager的效率对比分析。以及效率低下的原因。最后给出了一些解决方案。
1.在上一篇讨论了requestLayout的效率问题。对如何避免这个问题也进行了深入探讨。本篇就内存问题进行讨论。一般情况下,安卓的ListView实现方式上,就存在要移动childView位置的需求。
如果对childView进行了回收并且回收的childView必须仍然在原来的位置上,那么childView的位置可能要出现在两个位置上。这非常有挑战性,因为安卓的很多东西都是基于一个View一个位置的这样的思想。现在突然出现两个位置。那么也就是说,回收的View不能再呆在原来的位置了。必须被remove掉。remove掉之后呢,其他的View必须挤过去。总之所有的children都得改变位置。
也就是说必须改变布局。但是改变布局是否就一定要reqestLayout,也是个问题。刚才说了不需要。只要调用一下onLayout就行了。onLayout调用的时候,必须知道childView的位置吧。、那还得改变child的位置,那又调用什么呢?
那么,仅仅改变childView位置的函数是什么呢?这个函数就是offset系列函数。在Gallery那边是offsetChildrenLeftAndRight另外还有个setX和setY。这些都是改变位置的函数。
ListView的实现时非常高效的。既保证了回收childiew,有能不进行layout。非常高效。
第一。View的回收机制。
第二。View不进行requestLayout
2.Gallery的实现方式
Gallery在实现的时候没有采用回收机制。经过测试的Adapter.getView方法参数,View都是null。也就是说不对View进行复用。其实上Gallery中的View是越来越多。而且每一个View都不会进行回收。这跟一个
ScrollView+ViewPager的实现方式是一样的。唯一的区别是Gallery采用了Adapter机制,并且使用了ListView的滚动原理。但是Gallery没有对View进行回收,全部保存了起来。在内存不够用的时候,尽量不要使用这个。下面拿Gallery和ListView的回收机制进行了对比,
先看trackMotionScroll方法:
Gallery:
- /**
- * Tracks a motion scroll. In reality, this is used to do just about any
- * movement to items (touch scroll, arrow-key scroll, set an item as selected).
- *
- * @param deltaX Change in X from the previous event.
- */
- void trackMotionScroll(int deltaX) {
- if (getChildCount() == 0) {
- return;
- }
- boolean toLeft = deltaX < 0;
- int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX);
- if (limitedDeltaX != deltaX) {
- // The above call returned a limited amount, so stop any scrolls/flings
- mFlingRunnable.endFling(false);
- onFinishedMovement();
- }
- offsetChildrenLeftAndRight(limitedDeltaX);
- detachOffScreenChildren(toLeft);
- if (toLeft) {
- // If moved left, there will be empty space on the right
- fillToGalleryRight();
- } else {
- // Similarly, empty space on the left
- fillToGalleryLeft();
- }
- // Clear unused views
- mRecycler.clear();
- setSelectionToCenterChild();
- onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these.
- invalidate();
- }
/** * Tracks a motion scroll. In reality, this is used to do just about any * movement to items (touch scroll, arrow-key scroll, set an item as selected). * * @param deltaX Change in X from the previous event. */ void trackMotionScroll(int deltaX) { if (getChildCount() == 0) { return; } boolean toLeft = deltaX < 0; int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX); if (limitedDeltaX != deltaX) { // The above call returned a limited amount, so stop any scrolls/flings mFlingRunnable.endFling(false); onFinishedMovement(); } offsetChildrenLeftAndRight(limitedDeltaX); detachOffScreenChildren(toLeft); if (toLeft) { // If moved left, there will be empty space on the right fillToGalleryRight(); } else { // Similarly, empty space on the left fillToGalleryLeft(); } // Clear unused views mRecycler.clear(); setSelectionToCenterChild(); onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these. invalidate(); }ListView
- /**
- * Track a motion scroll
- *
- * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion
- * began. Positive numbers mean the user's finger is moving down the screen.
- * @param incrementalDeltaY Change in deltaY from the previous event.
- * @return true if we're already at the beginning/end of the list and have nothing to do.
- */
- boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
- final int childCount = getChildCount();
- if (childCount == 0) {
- return true;
- }
- final int firstTop = getChildAt(0).getTop();
- final int lastBottom = getChildAt(childCount - 1).getBottom();
- final Rect listPadding = mListPadding;
- // "effective padding" In this case is the amount of padding that affects
- // how much space should not be filled by items. If we don't clip to padding
- // there is no effective padding.
- int effectivePaddingTop = 0;
- int effectivePaddingBottom = 0;
- if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
- effectivePaddingTop = listPadding.top;
- effectivePaddingBottom = listPadding.bottom;
- }
- // FIXME account for grid vertical spacing too?
- final int spaceAbove = effectivePaddingTop - firstTop;
- final int end = getHeight() - effectivePaddingBottom;
- final int spaceBelow = lastBottom - end;
- final int height = getHeight() - mPaddingBottom - mPaddingTop;
- if (deltaY < 0) {
- deltaY = Math.max(-(height - 1), deltaY);
- } else {
- deltaY = Math.min(height - 1, deltaY);
- }
- if (incrementalDeltaY < 0) {
- incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
- } else {
- = Math.min(height - 1, incrementalDeltaY);
- }
- final int firstPosition = mFirstPosition;
- // Update our guesses for where the first and last views are
- if (firstPosition == 0) {
- mFirstPositionDistanceGuess = firstTop - listPadding.top;
- } else {
- mFirstPositionDistanceGuess += incrementalDeltaY;
- }
- if (firstPosition + childCount == mItemCount) {
- mLastPositionDistanceGuess = lastBottom + listPadding.bottom;
- } else {
- mLastPositionDistanceGuess += incrementalDeltaY;
- }
- final boolean cannotScrollDown = (firstPosition == 0 &&
- firstTop >= listPadding.top && incrementalDeltaY >= 0);
- final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
- lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);
- if (cannotScrollDown || cannotScrollUp) {
- return incrementalDeltaY != 0;
- }
- final boolean down = incrementalDeltaY < 0;
- final boolean inTouchMode = isInTouchMode();
- if (inTouchMode) {
- hideSelector();
- }
- final int headerViewsCount = getHeaderViewsCount();
- final int footerViewsStart = mItemCount - getFooterViewsCount();
- int start = 0;
- int count = 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) {
- mRecycler.addScrapView(child, position);
- if (ViewDebug.TRACE_RECYCLER) {
- ViewDebug.trace(child,
- ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
- firstPosition + i, -1);
- }
- }
- }
- }
- } 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) {
- mRecycler.addScrapView(child, position);
- if (ViewDebug.TRACE_RECYCLER) {
- ViewDebug.trace(child,
- ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
- firstPosition + i, -1);
- }
- }
- }
- }
- }
- mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
- mBlockLayoutRequests = true;
- if (count > 0) {
- detachViewsFromParent(start, count);
- }
- offsetChildrenTopAndBottom(incrementalDeltaY);
- if (down) {
- mFirstPosition += count;
- }
- invalidate();
- final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
- if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
- fillGap(down);
- }
- if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
- final int childIndex = mSelectedPosition - mFirstPosition;
- if (childIndex >= 0 && childIndex < getChildCount()) {
- positionSelector(mSelectedPosition, getChildAt(childIndex));
- }
- } else if (mSelectorPosition != INVALID_POSITION) {
- final int childIndex = mSelectorPosition - mFirstPosition;
- if (childIndex >= 0 && childIndex < getChildCount()) {
- positionSelector(INVALID_POSITION, getChildAt(childIndex));
- }
- } else {
- mSelectorRect.setEmpty();
- }
- mBlockLayoutRequests = false;
- invokeOnItemScrollListener();
- awakenScrollBars();
- return false;
- }
/** * Track a motion scroll * * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion * began. Positive numbers mean the user's finger is moving down the screen. * @param incrementalDeltaY Change in deltaY from the previous event. * @return true if we're already at the beginning/end of the list and have nothing to do. */ boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { final int childCount = getChildCount(); if (childCount == 0) { return true; } final int firstTop = getChildAt(0).getTop(); final int lastBottom = getChildAt(childCount - 1).getBottom(); final Rect listPadding = mListPadding; // "effective padding" In this case is the amount of padding that affects // how much space should not be filled by items. If we don't clip to padding // there is no effective padding. int effectivePaddingTop = 0; int effectivePaddingBottom = 0; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { effectivePaddingTop = listPadding.top; effectivePaddingBottom = listPadding.bottom; } // FIXME account for grid vertical spacing too? final int spaceAbove = effectivePaddingTop - firstTop; final int end = getHeight() - effectivePaddingBottom; final int spaceBelow = lastBottom - end; final int height = getHeight() - mPaddingBottom - mPaddingTop; if (deltaY < 0) { deltaY = Math.max(-(height - 1), deltaY); } else { deltaY = Math.min(height - 1, deltaY); } if (incrementalDeltaY < 0) { incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY); } else { = Math.min(height - 1, incrementalDeltaY); } final int firstPosition = mFirstPosition; // Update our guesses for where the first and last views are if (firstPosition == 0) { mFirstPositionDistanceGuess = firstTop - listPadding.top; } else { mFirstPositionDistanceGuess += incrementalDeltaY; } if (firstPosition + childCount == mItemCount) { mLastPositionDistanceGuess = lastBottom + listPadding.bottom; } else { mLastPositionDistanceGuess += incrementalDeltaY; } final boolean cannotScrollDown = (firstPosition == 0 && firstTop >= listPadding.top && incrementalDeltaY >= 0); final boolean cannotScrollUp = (firstPosition + childCount == mItemCount && lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0); if (cannotScrollDown || cannotScrollUp) { return incrementalDeltaY != 0; } final boolean down = incrementalDeltaY < 0; final boolean inTouchMode = isInTouchMode(); if (inTouchMode) { hideSelector(); } final int headerViewsCount = getHeaderViewsCount(); final int footerViewsStart = mItemCount - getFooterViewsCount(); int start = 0; int count = 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) { mRecycler.addScrapView(child, position); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, firstPosition + i, -1); } } } } } 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) { mRecycler.addScrapView(child, position); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, firstPosition + i, -1); } } } } } mMotionViewNewTop = mMotionViewOriginalTop + deltaY; mBlockLayoutRequests = true; if (count > 0) { detachViewsFromParent(start, count); } offsetChildrenTopAndBottom(incrementalDeltaY); if (down) { mFirstPosition += count; } invalidate(); final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { fillGap(down); } if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { final int childIndex = mSelectedPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { positionSelector(mSelectedPosition, getChildAt(childIndex)); } } else if (mSelectorPosition != INVALID_POSITION) { final int childIndex = mSelectorPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { positionSelector(INVALID_POSITION, getChildAt(childIndex)); } } else { mSelectorRect.setEmpty(); } mBlockLayoutRequests = false; invokeOnItemScrollListener(); awakenScrollBars(); return false; }可以看到Gallery处理View的关键代码:
- offsetChildrenLeftAndRight(limitedDeltaX);
- detachOffScreenChildren(toLeft);
- 。。。。
- mRecycler.clear();
offsetChildrenLeftAndRight(limitedDeltaX); detachOffScreenChildren(toLeft); 。。。。 mRecycler.clear();而ListView中为:
- if (count > 0) {
- detachViewsFromParent(start, count);
- }
- offsetChildrenTopAndBottom(incrementalDeltaY);
if (count > 0) { detachViewsFromParent(start, count); } offsetChildrenTopAndBottom(incrementalDeltaY);detachOffScreenChildren和detachViewsFromParent跟ListView功能一样。但是Gallery多出一个 mRecycler.clear()。等待重复利用的mRecycler被回收了,这就造成了再Gallery中无法复用之前的View的情况,由此可见Gallery在内存的使用上存在很大的设计缺陷。
再看Gallery与ListView的makeAndAddView方法
Gallery
- private View makeAndAddView(int position, int offset, int x, boolean fromLeft) {
- View child;
- if (!mDataChanged) {
- child = mRecycler.get(position);
- if (child != null) {
- // Can reuse an existing view
- int childLeft = child.getLeft();
- // Remember left and right edges of where views have been placed
- mRightMost = Math.max(mRightMost, childLeft
- + child.getMeasuredWidth());
- mLeftMost = Math.min(mLeftMost, childLeft);
- // Position the view
- setUpChild(child, offset, x, fromLeft);
- return child;
- }
- }
- // Nothing found in the recycler -- ask the adapter for a view
- child = mAdapter.getView(position, null, this);
- // Position the view
- setUpChild(child, offset, x, fromLeft);
- return child;
- }
private View makeAndAddView(int position, int offset, int x, boolean fromLeft) { View child; if (!mDataChanged) { child = mRecycler.get(position); if (child != null) { // Can reuse an existing view int childLeft = child.getLeft(); // Remember left and right edges of where views have been placed mRightMost = Math.max(mRightMost, childLeft + child.getMeasuredWidth()); mLeftMost = Math.min(mLeftMost, childLeft); // Position the view setUpChild(child, offset, x, fromLeft); return child; } } // Nothing found in the recycler -- ask the adapter for a view child = mAdapter.getView(position, null, this); // Position the view setUpChild(child, offset, x, fromLeft); return child; }ListView:
- private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
- boolean selected) {
- View child;
- if (!mDataChanged) {
- // Try to use an existing view for this position
- child = mRecycler.getActiveView(position);
- if (child != null) {
- if (ViewDebug.TRACE_RECYCLER) {
- ViewDebug.trace(child, ViewDebug.RecyclerTraceType.RECYCLE_FROM_ACTIVE_HEAP,
- position, getChildCount());
- }
- // Found it -- we're using an existing child
- // This just needs to be positioned
- setupChild(child, position, y, flow, childrenLeft, selected, true);
- return child;
- }
- }
- // Make a new view for this position, or convert an unused view if possible
- child = obtainView(position, mIsScrap);
- // This needs to be positioned and measured
- setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
- return child;
- }
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.RECYCLE_FROM_ACTIVE_HEAP, position, getChildCount()); } // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // Make a new view for this position, or convert an unused view if possible child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }由此可见Gallery每次都将不可见的View进行了清理。对比ListView来说,在滚动过程中多了new一个View的开销。那么就存在一个避免此类问题的方法,自己对View建一个recyler从而弥补这个缺点。
另外安卓的ViewPager+ScrollView 采用了传统的整个容器进行滚动算法,而不是调整childView的位置,但是也存在卡顿问题。
ViewPager的用法见下文:
http://blog.csdn.net/wangjinyu501/article/details/816
ViewPager的卡顿见下文:
http://marspring.mobi/viewpager-majorization/
里面google提供了一个接口解决卡顿问题。至少目前看来,只是五十步笑百步。不能根本解决问题。默认当前正中位置前后缓存一个View。改后可以缓存多个View。当缓存的多个View被用完的时候,仍然是卡顿。
另外,
有人说用异步加载,异步加载解决的是数据加载问题,跟这个new View造成的卡顿问题是两码事儿。
如何解决?
自己做类似ListView的回收机制,对View进行复用,从而根本上解决这个问题。
另外,安卓没有采用享元模式。。。。很遗憾。。。也许google大牛们会想到,希望如此。希望google能体谅我们,再提供一个类似View的轻量级显示控件来直接支持享元模式。这在Brew平台是有的。。。。。。。。。。。。。。。。。。。。。。郁闷。