近期在工作时发现了一个有趣的bug,切换语言时,概率性出现crash ,查看相关log,发现是调用到
@VisibleForTesting
protected void fireConfigChanged(ZenModeConfig config) {
Utils.safeForeach(mCallbacks, c -> c.onConfigChanged(config));
}
使用lamda表达式去迭代回调数组时,发生了NullPointerException,mCallback是一个回调对象的ArrayList,就查看了safeForeach方法的实现,发现一个有趣的地方
/**
* Allows lambda iteration over a list. It is done in reverse order so it is safe
* to add or remove items during the iteration.
*/
public static <T> void safeForeach(List<T> list, Consumer<T> c) {
for (int i = list.size() - 1; i >= 0; i--) {
c.accept(list.get(i));
}
}
谷歌工程师通过比较反向遍历ArrayList的方式,去避免迭代期删减条目,导致数组越界的问题。但是,我们都知道,ArrayList是线程不安全的,这一点看源码就可以知道
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
// Android-note: Also accessed from java.util.Collections
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
ArrayList的实现主要就是用了一个Object的数组,用来保存所有的元素,以及一个size变量用来保存当前数组中已经添加了多少元素。接下来在看一下add是如何实现的:
public void add(int index, E e) {
if (index < 0 || index > this.size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
}
由此看到get元素的时候,会比较index和当前的size,而这里的size会因为其他线程的add、remove发生改变,故出现文章最开始的IndexOutOfBoundsException。
那么回过头来看safeForeach,尽管使用倒序,但它在迭代过程中,仍然可能会出现被其他线程修改的情况。无法保证size是否发生改变,因此无法保证线程安全,该bug该如何解决呢:
其实直接使用我们常用的foreach 即可,同时,使用
CopyOnWriteArrayList来替代ArrayList,因为它是线程安全的,
而由于foreach对于数组,是普通的for循环,但对于集合,却使用了Iterater迭代器。因此,把代码改写为:
@VisibleForTesting
protected void fireConfigChanged(ZenModeConfig config) {
for(Callback cb : mCallbacks){
cb.onConfigChanged(config));
}
}