经验总结-ListView出现IndexOutOfBoundsException异常

1,079 阅读2分钟

一、bug分析

Adapter数据集List变化后,没有及时调用Adapter.notifyDataSetChanged方法来刷新listview视图。

mAdapter = new JobSearchAdapter(this, mDatas, new OnClickListener(){});

@Override
public void refresh() {
    HashMap<String, String> map = createListRequestParams(true, false);
    NetManager.GET(createListUrl()).addParam(map)
            .request(new HttpCallback<BaseResult<JobResultBean>>() {
                         @Override
                         public void onSuccess(BaseResult<JobResultBean> data) {
                             if (!isFinishing()) {                     
                                 listData = data;
                                 mDatas.clear();
                                 if (refreshLayout == null) return;
                                 if (listData == null || listData.data == null) {
                                     //错误状态
                                     onRefreshError(ChrBaseListFragment.NoDataType.LOAD_FILED, "");
                                     return;
                                 }
                         //省略部分代码
                     showListData(listData);
}protected void showListData(BaseResult<JobResultBean> data) {
    mDatas.clear();
    if (data.getData().recruitList != null) {
        mDatas.addAll(data.getData().recruitList);
    }
//省略部分代码
    mAdapter.notifyDataSetChanged();
    mCurrentPage++;//请求一次后,pageNum+1
}

通过代码可以看出,如果listData == null || listData.data == null,那么就不会执行后面的mAdapter.notifyDataSetChanged()方法,而mDatas数据集是通过引用的方式传值,这就导致mDatas发生变化后,内存中数据集就发生了变化。但是RecyclerView没有及时刷新UI(同步数据集和listView的对应)。这样当发生页面重新渲染,重新绘制对应的item view时,就会出现数据集为空,但index对应不上的bug。

通过bugly反馈,发现只有部分手机会出现,并不是必现。

解决方式

1、及时通知刷新

if (listData == null || listData.data == null) {
                                     //错误状态
     mAdapter.notifyDataSetChanged();     onRefreshError(ChrBaseListFragment.NoDataType.LOAD_FILED, "");
       return;
  }

2、不使用数据集引用方式,在每次需要更新listview数据时,都重新赋值。setData和notifyDataSetChanged 成对出现

mAdapter = new JobSearchAdapter(this, new ArrayList<IJobBaseBean>() , new OnClickListener())
mAdapter.setData(mDatas);
mAdapter.notifyDataSetChanged();


二、Adapter.notifyDataSetChanged 方法

listView中item布局视图刷新采用观察者模式(订阅-发布模式)。ListView.setAdapter核心源码如下:

public void setAdapter(ListAdapter adapter) {
    super.setAdapter(adapter);
     mItemCount = mAdapter.getCount();
     mDataSetObserver = new AdapterDataSetObserver();//创建一个订阅者
     mAdapter.registerDataSetObserver(mDataSetObserver);//将订阅者注册到发布者里(实质是添加到一个数组,发布者维护了一个订阅者数组)
     mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
     requestLayout();
}
public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {    
    public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }
public abstract class Observable<T> {
    /**
     * The list of observers.  An observer can be in the list at most
     * once and will never be null.
     */
    protected final ArrayList<T> mObservers = new ArrayList<T>();

    public void registerObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            if (mObservers.contains(observer)) {
                throw new IllegalStateException("Observer " + observer + " is already registered.");
            }
            mObservers.add(observer);
        }
    }

    public void unregisterObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            int index = mObservers.indexOf(observer);
            if (index == -1) {
                throw new IllegalStateException("Observer " + observer + " was not registered.");
            }
            mObservers.remove(index);
        }
    }

Adapter通知列表刷新方法

public void notifyDataSetChanged() {
    mDataSetObservable.notifyChanged();
}

DataSetObservable中调用

public class DataSetObservable extends Observable<DataSetObserver> {

    public void notifyChanged() {
        synchronized(mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

最终是调用订阅者自己的onChanged方法。

AdapterDataSetObserver具体实现类主要是集成自AdapterView类的内部类

class AdapterDataSetObserver extends DataSetObserver {

    private Parcelable mInstanceState = null;

    @Override
    public void onChanged() {
        mDataChanged = true;
        mOldItemCount = mItemCount;
        mItemCount = getAdapter().getCount();

        // Detect the case where a cursor that was previously invalidated has
        // been repopulated with new data.
        if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                && mOldItemCount == 0 && mItemCount > 0) {
            AdapterView.this.onRestoreInstanceState(mInstanceState);//取保存的状态值
            mInstanceState = null;
        } else {
            rememberSyncState();
        }
        checkFocus();
        requestLayout();//重新执行measure、layout、draw来绘制listview
    }