那些年踩过的坑
RecyclerView的IndexOutOfBoundsException异常
大半年没有敲代码了,顺手写个上拉加载更多居然就出现了异常,还是系统异常,让人费解.
异常信息:
崩溃了 : java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{3b3cebd position=5 id=-1, oldPos=-1, pLpos:-1 no parent}
at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:5297)
at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5479)
at android.support.v7.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:282)
at android.support.v7.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:336)
at android.support.v7.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:349)
at android.support.v7.widget.GapWorker.prefetch(GapWorker.java:356)
at android.support.v7.widget.GapWorker.run(GapWorker.java:387)
at android.os.Handler.handleCallback(Handler.java:761)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6523)
10-19 10:08:38.892 17210-17210/? E/My custom tag: │ at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:942)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:832)
在网上查阅资料,处理方式主要分为两种.
- 继承LinearLayoutManager,重写onLayoutChildren
可以说这种办法单纯为了catch住异常,问题只是被掩盖,并没有解决问题,当鸵鸟并不能解决问题.
- 更新完数据及时调用notifyxxx相关方法,通知数据更新
这种是正常做法,我也是用的这种,但是在Adapter的集合对象修改了之后调用还是容易出现异常.
问题所在
google的官方回复也是说在数据变化之后及时调用notify更新,我前面出现了一个认知上的错误.
Adapter的数据对象通常用List进行封装,我只在List对象有变化的时候调用notify更新数据.
对于RecyclerView来说数据变化并不在于我封装的List,而是getItemCount()的返回值,我的问题就出现在List集合对象没有改变的时候却改变了getItemCount()的值.
RecyclerView中数据长度并不是通过实时调用getItemCount()来确定.而是通过RecyclerView.State对象中的mItemCount对象来确定数据长度,在调用notify方法时会调用requestLayout()方法来进行重新测量,绘制,并对mItemCount进行重新赋值.
抛出异常的代码:
if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
+ "adapter position" + holder);
}
这里也可以清楚地看出抛出异常的条件,holder的数量由mItemCount来进行控制,当更新完数据之后再更改了mAdapter.getItemCount()的返回值,一旦将返回值变小就会触发异常,并抛出.
我的异常
我简单的随便写了个上拉加载更多,根据是否需要加载更多来判断来决定是否增加加载更多的holder界面
@Override
public int getItemCount() {
if (mList == null || mList.size() == 0) {
return 0;
} else {
return mLoadMore ? mList.size() + 1 : mList.size();
// TODO: 2017.10.16 google 新版本优化之后内部维护了长度mItemCount,并不是每次调用 getItemCount. 所以需要保证返回结果不变,或者结果改变之后调用更新数据
}
}
我的解决
在getItemCount()值需要调用notify方法同步的情况下,通过mLoadMore来变更返回值就需要多调用几次notify方法了,为了避免不必要的刷新,最终解决方式通过判断mLoadMore的值直接显示或隐藏加载更多的holder.
一个多调用一次刷新,一个多调用一次Holder的创建绑定的那个流程,也不好判断那种方式更优.
但是隐藏方式需要注意若addItemDecoration()需要注意隐藏了一个,也要对itemDecoration进行处理,不然会出现两条重合,影响界面.
成员对象初始化问题
一直以来就认为成员变量直接初始化随时都能用,然后就被坑了一把.
问题情况
B类有个成员变量,直接实例化的成员变量
private List<Object> mList = new ArrayList<>();
该对象在构造方法调用的过程中传递给对象C.结果对象B中mList初始化成功,C对象中为Null.
问题原因
B类继承了A类,B类的初始化是直接使用的A类构造函数中调用的抽象的init()
方法.
在初始化时先调用父类构造函数,父类构造函数中调用子类init
实例化方法,然而此时父类构造方法还没走完,子类成员变量还未进行初始化过程,从而导致类传递给C类的对象为null.
解决方式
- 父类不调用初始化方法,父类构造函数中就不该调用子类方法,因为父类调用时子类方法时,子类成员变量都还未进行初始化,很容易出现各种问题.
- 子类不在声明时初始化,在
init()
方法中进行成员变量的初始化. - 父类不调用初始化方法,子类在构造函数中自行调用初始化方法.