1. 程式人生 > >ArrayMap java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]

ArrayMap java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]

錯誤堆疊:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]
    at android.support.v4.util.SimpleArrayMap.allocArrays(SourceFile:183)
    at android.support.v4.util.SimpleArrayMap.put(SourceFile:437)

錯誤原因:
由於SimpleArrayMap 裡面使用了一個靜態變數的快取,mBaseCache,

    static Object[] mBaseCache;

該變數預設有兩個資料,第1個元素是一個object[],用於存放上次的快取的mBaseCache
第二個元素是int[],用於存在hash。具體賦值程式碼可以看下面

synchronized (ArrayMap.class) {
                if (mBaseCacheSize < CACHE_SIZE) {
                    array[0] = mBaseCache;
                    array[1] = hashes;
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    mBaseCache = array;
                    mBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
                            + " now have " + mBaseCacheSize + " entries");
                }
            }

使用該陣列的地方在:
SimpleArrayMap 的allocArrays 方法裡

synchronized (ArrayMap.class) {
                if (mBaseCache != null) {
                    final Object[] array = mBaseCache;
                    mArray = array;
                    mBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
                            + " now have " + mBaseCacheSize + " entries");
                    return;
                }
            }

下面這段程式碼是有風險的,如果mBaseCache 在多執行緒被修改了,就會把ClassCastException 異常。

        mBaseCache = (Object[])array[0];

解決方法:
如果專案某個地方報這個錯誤,請把這個地方的ArrayMap替換成 HasMap. HasMap 多執行緒不會崩潰,雖然,他不是特別完好的支援。不需要把專案中所有的地方都替換掉,沒有必要。單獨執行緒,ArrayMap 完全沒有問題。

錯誤復現:這個復現起來超級麻煩,我花了一週的時間,才找到復現的漏洞,分享給大家:

    /**
     * 復現該問題  用了四個執行緒
     *     java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]
     *         at android.support.v4.util.SimpleArrayMap.allocArrays(SimpleArrayMap.java:157)
     *         at android.support.v4.util.SimpleArrayMap.put(SimpleArrayMap.java:399)
     *         at com.example.fragment.MainFragment$14.run(MainFragment.java:280)
     *        1.首先 執行緒1 執行到put 方法的

     *         mArray[index<<1] = key;
     *         mArray[(index<<1)+1] = value;
     *         mSize++;
     *         return null;
     *         最上面這個位置  目的是讓這個陣列不再是空的
     *
     *         2.執行執行緒2  也執行到
     *         mArray[index<<1] = key;
     *         mArray[(index<<1)+1] = value;
     *         mSize++;
     *         return null;
     *         最上面這個位置  目的是讓這個put 的東西,放在第0個位置,因為put裡面會生成index,
     *         讓兩個執行緒都放到index 是0 的位置
     *
     *         3.把執行緒1執行完,這樣資料裡面已經放進去一個數據了
     *
     *         4.執行執行緒3 到removeAt 方法的 freeArrays 的  mBaseCache = array; 之前
     *             public V removeAt(int index) {
     *              final Object old = mArray[(index << 1) + 1];
     *              if (mSize <= 1) {
     *             // Now empty.
     *             if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
     *             freeArrays(mHashes, mArray, mSize);
     *
     *             mBaseCache = array;----------- freeArrays
     *
     *             這個的目的是呼叫freeArray 方法,讓當前的map釋放當前的陣列。這樣就可以生成mBaseCache了
     *
 *             5.把執行緒2  執行完
 *             這樣就會把mBaseCache 賦值的陣列,重新賦值
 *
 *             6.把執行緒3執行完
 *             ok,現在mBaseCache已經被汙染了
 *
 *             7.執行執行緒4
 *
 */
    private void CMETestCastException() {
        final ArrayMap testArrayMap = new ArrayMap();
        final ArrayMap testArrayMap2 = new ArrayMap();

        new  Thread("執行緒1"){
            @Override
            public void run() {
                super.run();
                    testArrayMap.put("2324","fffff");
            }

        }.start();

        new  Thread("執行緒2"){
            @Override
            public void run() {
                super.run();
                    testArrayMap.put("test","string");
            }

        }.start();

        new  Thread("執行緒3"){
            @Override
            public void run() {
                super.run();
                    testArrayMap.removeAt(0);
            }

        }.start();

        new  Thread("執行緒4"){
            @Override
            public void run() {
                super.run();
                    testArrayMap2.put("aaa","string");
            }

        }.start();
    }

復現這個問題的時候,關鍵是把mBaseCache 汙染掉。這裡四個執行緒的話,需要除錯,除錯步驟就是上面我註釋的。

總結:
如果當前的map 會有多個執行緒訪問,請使用HasMap. 該問題,google 並沒有解決。在高版本上,直接扔CME ConcurrentModificationException.