HeaderViewListAdapter.isEnabled數組越界問題分析

問題介紹

Android應用在bugly上偶現調用View.draw()時出現數據越界,根據bugly提供的解決方案以下,java

1.遍歷數組/字符串等集合前,要判斷遍歷對象的長度;
2.操做數組/字符串等集合前,要檢查角標是否在長度容許範圍內;
3.ListView操做不當也會引發該異常,這種狀況下通常是因爲List渲染的時候,外面的數據源發生變化致使的。舉例如ListView滾動時點擊刷新將會報錯,解決方法是ListView滾動時將刷新置爲不可點擊。或者改變數據源以前調用adapter的notifyDataSetInvalidated()方法將原數據源設置爲無效。

但因爲無項目源碼相關的堆棧提示信息,此問題一直被擱置。android

分析思路

出錯堆棧信息以下canvas

1 java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:255)
2 java.util.ArrayList.get(ArrayList.java:308)
3 android.widget.HeaderViewListAdapter.isEnabled(HeaderViewListAdapter.java:164)
4 android.widget.ListView.dispatchDraw(ListView.java:3329)
5 android.view.View.draw(View.java:16206)
6 android.widget.AbsListView.draw(AbsListView.java:4166)
7 android.view.View.updateDisplayListIfDirty(View.java:15200)
8 android.view.View.draw(View.java:15973)
//...
49 android.view.ViewRootImpl.draw(ViewRootImpl.java:2615)
50 android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2434)
51 android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2067)
52 android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1107)
53 android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6013)
//...
60 android.os.Looper.loop(Looper.java:148)
61 android.app.ActivityThread.main(ActivityThread.java:5417)
62 java.lang.reflect.Method.invoke(Native Method)
63 com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
64 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

這裏重點關注第3~4行信息內容,根據第3行可知是ListView的dispatchDraw()方法異常
經過第3行知道是HeaderViewListAdapter.isEnabled方法拋出的
查詢Android的ListView源碼可知,數組

public void setAdapter(ListAdapter adapter) {
    if (mAdapter != null && mDataSetObserver != null) {
        mAdapter.unregisterDataSetObserver(mDataSetObserver);
    }

    resetList();
    mRecycler.clear();

    if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
        //注意這裏:只有調用addHeaderView或者addFooterView時候纔會構建HeaderViewListAdapter
        mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
    } else {
        mAdapter = adapter;
    }

    mOldSelectedPosition = INVALID_POSITION;
    mOldSelectedRowId = INVALID_ROW_ID;

    // AbsListView#setAdapter will update choice mode states.
    super.setAdapter(adapter);
    //…省略
}

根據HeaderViewListAdapter類及堆棧信息可知出問題的確定是ListView且有調用addHeaderView或者addFooterView的地方
補充說明:
1.Bugly的」跟蹤數據」模塊也能幫忙排除某些頁面,大體定位出錯的地方
2.注意有些頁面是針對GridView或者RecyclerView自定義的add(Head/Foot)erView的地方,根據是ListView類報錯也能夠忽略app

分析ListView.dispatchDraw方法

@Override
protected void dispatchDraw(Canvas canvas) {
        final int count = getChildCount();
        if (!mStackFromBottom) {
            for (int i = 0; i < count; i++) {
                
                        if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
                                && (nextIndex >= headerCount)) && (isLastItem
                                || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter
                                        && (nextIndex < footerLimit)))) {
                            //...
                        }
                    }
                }
            }
        }
    }

    // Draw the indicators (these should be drawn above the dividers) and children
    super.dispatchDraw(canvas);
}

能夠看到,dispatchDraw是經過getChildCount去遍歷ide

分析HeaderViewListAdapter.isEnable()方法

public boolean isEnabled(int position) {
    // Header (negative positions will throw an IndexOutOfBoundsException)
    int numHeaders = getHeadersCount();
    if (position < numHeaders) {
        return mHeaderViewInfos.get(position).isSelectable;
    }

    // Adapter
    final int adjPosition = position - numHeaders;
    int adapterCount = 0;
    if (mAdapter != null) {
        adapterCount = mAdapter.getCount();
        if (adjPosition < adapterCount) {
            return mAdapter.isEnabled(adjPosition);
        }
    }

    // Footer (off-limits positions will throw an IndexOutOfBoundsException)
    return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable;
}

HeaderViewListAdapter.isEnable的方法實現很簡單,重點關注該方法使用的count=mAdapter.getCount()oop

結合上面兩個方法的分析能夠想到,isEnable()方法觸發」java.lang.IndexOutOfBoundsException」只有兩種可能:
1.調用remove(Header/Footer)View方法後沒有及時觸發更新adapter更新
2.數據源數據減小(不僅有remove這一種可能,也有多是賦值size更小的集合引用)後沒有及時更新spa

結合案例分析

項目全局搜索addFooterView|addHeaderView,排除無關結果後以下:
1.「MenuFragment」中menuClsAdapter.modules有remove、clear或者集合從新賦值的地方均有顯式調用notifyDataSetChanged方法通知ListView去更新
2.「AirTableFragment」中檢查tableViewProcessor.mareaDBModelList在AirTableFragment中有remove、clear或者變動引用的地方
2.1 doDeleteArea:該方法中可能有問題,一開始覺得是clear以後調用selectionArea方法中在notifyDataSetChanged以前有耗時操做,檢查了下代碼,排除該猜測
2.2 會不會是tableViewProcessor.mareaDBModelList在非AirTableFragment頁面中被修改?檢查發現loadLocalDatas()確實有傳遞tableViewProcessor對象引用且內部有變動mareaDBModelList數據。code

PS:問題並不是僅僅如此,具體緣由與公司封裝的base庫相關,就不深刻介紹了。orm

相關文章
相關標籤/搜索