安卓高手之路之图形系统(6)ListView继续

来源:互联网 发布:广州淘宝培训汇学好 编辑:程序博客网 时间:2024/06/15 01:45

综述:

本篇首先介绍了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();    }

 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;    }

 

 

 可以看到Gallery处理View的关键代码:

      offsetChildrenLeftAndRight(limitedDeltaX);        detachOffScreenChildren(toLeft);       。。。。        mRecycler.clear();

 

 

 

而ListView中为:

 

  

        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;    }

 

 

 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;    }

由此可见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平台是有的。。。。。。。。。。。。。。。。。。。。。。郁闷。