记一Bug:StaggeredGridLayoutManager :java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
崩溃
线上平台监控到一个bug,数量不多。一开始直找不到啥原因,先看报错日志:
咋一看,报错原因是数组越界了,但是你会发现具体报错位置是源码StaggeredGridLayoutManager$Span
的calculateCachedStart
报错了,具体如下:
void calculateCachedStart() {
final View startView = mViews.get(0);
final LayoutParams lp = getLayoutParams(startView);
mCachedStart = mPrimaryOrientation.getDecoratedStart(startView);
if (lp.mFullSpan) {
LazySpanLookup.FullSpanItem fsi = mLazySpanLookup
.getFullSpanItem(lp.getViewLayoutPosition());
if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_START) {
mCachedStart -= fsi.getGapForSpan(mIndex);
}
}
}
这里的mViews.get(0)
中mViews的size为0,所以数组越界了。
复现
复现场景也找到了:使用viewpager2导致当前的fragment了,导致布局缓存的销毁,会导致调用了StaggeredGridLayoutManager#onDetachedFromWindow
方法。由于我没有设置viewpager2的缓存数量(启动不多加载,启动快),在频繁切换的时候,会调用此方法。
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
super.onDetachedFromWindow(view, recycler);
removeCallbacks(mCheckForGapsRunnable);
for (int i = 0; i < mSpanCount; i++) {
//这里遍历清空了
mSpans[i].clear();
}
// SGLM will require fresh layout call to recover state after detach
view.requestLayout();
}
所以在滑动瀑布流布局的时候会回调calculateCachedStart
方法,导致崩溃了。
解决方案
设置viewpager2缓存数量
mViewPager.setOffscreenPageLimit(4);
这样就不会调用onDetachedFromWindow
方法了,虽然解决问题了,但是启动的时候会多初始化这么多fragment,那么就影响了启动速度,这是万万不行的。不在乎启动速度,改这个就行了。
重写onDetachedFromWindow
manager = new StaggeredGridLayoutManager(count,
StaggeredGridLayoutManager.HORIZONTAL){
@Override
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
//不处理了
}
};
处理SavedState
public static class SavedState implements Parcelable {
int mAnchorPosition;
int mVisibleAnchorPosition; // Replacement for span info when spans are invalidated
int mSpanOffsetsSize;
int[] mSpanOffsets;
int mSpanLookupSize;
int[] mSpanLookup;
List<LazySpanLookup.FullSpanItem> mFullSpanItems;
boolean mReverseLayout;
boolean mAnchorLayoutFromEnd;
boolean mLastLayoutRTL;
public SavedState() {
}
SavedState(Parcel in) {
mAnchorPosition = in.readInt();
mVisibleAnchorPosition = in.readInt();
mSpanOffsetsSize = in.readInt();
if (mSpanOffsetsSize > 0) {
mSpanOffsets = new int[mSpanOffsetsSize];
in.readIntArray(mSpanOffsets);
}
mSpanLookupSize = in.readInt();
if (mSpanLookupSize > 0) {
mSpanLookup = new int[mSpanLookupSize];
in.readIntArray(mSpanLookup);
}
mReverseLayout = in.readInt() == 1;
mAnchorLayoutFromEnd = in.readInt() == 1;
mLastLayoutRTL = in.readInt() == 1;
@SuppressWarnings("unchecked")
List<LazySpanLookup.FullSpanItem> fullSpanItems =
in.readArrayList(LazySpanLookup.FullSpanItem.class.getClassLoader());
mFullSpanItems = fullSpanItems;
}
public SavedState(SavedState other) {
mSpanOffsetsSize = other.mSpanOffsetsSize;
mAnchorPosition = other.mAnchorPosition;
mVisibleAnchorPosition = other.mVisibleAnchorPosition;
mSpanOffsets = other.mSpanOffsets;
mSpanLookupSize = other.mSpanLookupSize;
mSpanLookup = other.mSpanLookup;
mReverseLayout = other.mReverseLayout;
mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
mLastLayoutRTL = other.mLastLayoutRTL;
mFullSpanItems = other.mFullSpanItems;
}
void invalidateSpanInfo() {
mSpanOffsets = null;
mSpanOffsetsSize = 0;
mSpanLookupSize = 0;
mSpanLookup = null;
mFullSpanItems = null;
}
void invalidateAnchorPositionInfo() {
mSpanOffsets = null;
mSpanOffsetsSize = 0;
mAnchorPosition = RecyclerView.NO_POSITION;
mVisibleAnchorPosition = RecyclerView.NO_POSITION;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mAnchorPosition);
dest.writeInt(mVisibleAnchorPosition);
dest.writeInt(mSpanOffsetsSize);
if (mSpanOffsetsSize > 0) {
dest.writeIntArray(mSpanOffsets);
}
dest.writeInt(mSpanLookupSize);
if (mSpanLookupSize > 0) {
dest.writeIntArray(mSpanLookup);
}
dest.writeInt(mReverseLayout ? 1 : 0);
dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
dest.writeInt(mLastLayoutRTL ? 1 : 0);
dest.writeList(mFullSpanItems);
}
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
SavedState
是布局的一些状态保持,对比activtiy的state是一样的。所以我重写onDetachedFromWindow方法保存状态,重写
改为GridLayoutManager
如果布局支持,可以改,GridLayoutManager
滑动不会回调到calculateCachedStart
方法。不建议,很Low。
总结
代码不规范,测试两行泪啊