錯誤堆棧:
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.