前言
基于工作原因,最近一直在做Android Lolipop的ROM,本周在测试时发现了一个问题:重置、刷机后第一次启动,在开机时概率出现system_server进程多次挂掉,断电重启后有机会恢复正常。正常后再开机问题不出现。
分析
开机时system_server进程挂掉,基本是framework/service/目录下的服务出了异常。抓到的打印也符合我们的判断,有两种异常:
- 第一种
E/AndroidRuntime( 619): *** FATAL EXCEPTION IN SYSTEM PROCESS: Thread-56
E/AndroidRuntime( 619): java.lang.ArrayIndexOutOfBoundsException: length=398; index=398
E/AndroidRuntime( 619): at java.util.Collections.sort(Collections.java:1888)
E/AndroidRuntime( 619): at com.android.server.AssetAtlasService.computeBestConfiguration(AssetAtlasService.java:428)
E/AndroidRuntime( 619): at com.android.server.AssetAtlasService.chooseConfiguration(AssetAtlasService.java:481)
E/AndroidRuntime( 619): at com.android.server.AssetAtlasService.access$100(AssetAtlasService.java:66)
E/AndroidRuntime( 619): at com.android.server.AssetAtlasService$Renderer.run(AssetAtlasService.java:226)
E/AndroidRuntime( 619): at java.lang.Thread.run(Thread.java:818)
E/AndroidRuntime( 2237): FATAL EXCEPTION: main
- 第二种
E/AndroidRuntime( 617): *** FATAL EXCEPTION IN SYSTEM PROCESS: Thread-56
E/AndroidRuntime( 617): java.util.ConcurrentModificationException
E/AndroidRuntime( 617): at java.util.AbstractList$FullListIterator.set(AbstractList.java:148)
E/AndroidRuntime( 617): at java.util.Collections.sort(Collections.java:1888)
E/AndroidRuntime( 617): at com.android.server.AssetAtlasService.computeBestConfiguration(AssetAtlasService.java:428)
E/AndroidRuntime( 617): at com.android.server.AssetAtlasService.chooseConfiguration(AssetAtlasService.java:481)
E/AndroidRuntime( 617): at com.android.server.AssetAtlasService.access$100(AssetAtlasService.java:66)
E/AndroidRuntime( 617): at com.android.server.AssetAtlasService$Renderer.run(AssetAtlasService.java:226)
E/AndroidRuntime( 617): at java.lang.Thread.run(Thread.java:818)
根据异常log来看,出问题的点都在AssetAtlasService中,根据说明,此服务是在启动时将一些系统图片资源合并成一个纹理图传给GPU达到硬件加速的效果。
不过它干了什么跟这错误没太大关系,回归正题,我们能看出引起问题的原因应该是多线程竞争。ConcurrentModificationException,是当前线程使用迭代器对集合进行迭代时有另外线程对此集合做了更改引起的。
看下源码。
private static Configuration computeBestConfiguration(
ArrayList<Bitmap> bitmaps, int pixelCount) {
......
// Don't bother with an extra thread if there's only one processor
int cpuCount = Runtime.getRuntime().availableProcessors();
if (cpuCount == 1) {
new ComputeWorker(MIN_SIZE, MAX_SIZE, STEP, bitmaps, pixelCount, results, null).run();
} else {
int start = MIN_SIZE;
int end = MAX_SIZE - (cpuCount - 1) * STEP;
int step = STEP * cpuCount;
final CountDownLatch signal = new CountDownLatch(cpuCount);
for (int i = 0; i < cpuCount; i++, start += STEP, end += STEP) {
ComputeWorker worker = new ComputeWorker(start, end, step,
bitmaps, pixelCount, results, signal);
new Thread(worker, "Atlas Worker #" + (i + 1)).start();
}
try {
signal.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Log.w(LOG_TAG, "Could not complete configuration computation");
return null;
}
}
// Maximize the number of packed bitmaps, minimize the texture size
Collections.sort(results, new Comparator<WorkerResult>() {
@Override
public int compare(WorkerResult r1, WorkerResult r2) {
int delta = r2.count - r1.count;
if (delta != 0) return delta;
return r1.width * r1.height - r2.width * r2.height;
}
});
......
return new Configuration(result.type, result.width, result.height, result.count);
}
/**
* A compute worker will try a finite number of variations of the packing
* algorithms and save the results in a supplied list.
*/
private static class ComputeWorker implements Runnable {
......
@Override
public void run() {
if (DEBUG_ATLAS) Log.d(LOG_TAG, "Running " + Thread.currentThread().getName());
Atlas.Entry entry = new Atlas.Entry();
for (Atlas.Type type : Atlas.Type.values()) {
for (int width = mStart; width < mEnd; width += mStep) {
for (int height = MIN_SIZE; height < MAX_SIZE; height += STEP) {
// If the atlas is not big enough, skip it
if (width * height <= mThreshold) continue;
final int count = packBitmaps(type, width, height, entry);
if (count > 0) {
mResults.add(new WorkerResult(type, width, height, count));
// If we were able to pack everything let's stop here
// Increasing the height further won't make things better
if (count == mBitmaps.size()) {
break;
}
}
}
}
}
if (mSignal != null) {
mSignal.countDown();
}
}
......
}
可以看出,代码中根据cpu核心数量启动相应个数的线程,在线程中往results集合里面添加元素。在所有线程执行完毕后对集合进行排序。如果锁等待出错,则直接返回。
- 题外话:代码中的CountDownLatch提供了一个同步计数器,每次调用countDown计数减一,为0时await不再阻塞等待。用来等待多线程工作全部结束非常好用。是concurrent包提供的并发工具之一。
看来就是这里的多线程代码出现了错误,当系统初次启动时,CPU工作较多,多线程工作没有全部做完,await指定的超时时间10s就已经到时。此时程序继续向下运行开始对集合排序,同时另外线程又可能正好对集合进行add操作,于是出现异常,挂掉。
原因
根据API描述,CountDownLatch的await(time, timeunit)方法会等待参数指定的时间,如果到时计数器仍不为0,则会返回false,反之返回true。所以,代码中并没有对该方法的返回值做判断,不论是等待超时亦或正常完成,都对集合开始排序,因此就导致了开头我们看到的问题现象。
解决
要解决此问题,应该对await方法的返回值做判断,如果为false,则直接返回,不再排序即可。在更改之前,我决定上AOSP看下Google有没有发现这个问题。
首先,找到platform/frameworks/base/这个Git仓库,我们要看的是最新代码,就进入master分支。
然后,打开service/core/java/com/android/server/AssetAtlasService.java文件,看看目前最新版本什么样子。
最后,我们发现:
Xiaohui Chen兄在15年4月已经修复了此问题。方法与我们上面所说一致。那么合进当前代码,编译验证OK,问题解决。
后记
这个问题教导了我们,写代码要认真看API!