您的位置:首页 > 其它

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

2015-03-26 18:09 274 查看
安卓高手之路之图形系统(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:

Java代码





/**

* 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

Java代码





/**

* 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的关键代码:

Java代码





offsetChildrenLeftAndRight(limitedDeltaX);

detachOffScreenChildren(toLeft);

。。。。

mRecycler.clear();

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


而ListView中为:

Java代码





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

Java代码





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:

Java代码





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

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: