一. 最近項目中的圖片加載遇到了不少問題.
我們項目中用的圖片加載框架是UIL (Universal Image Loader).
最近fabric上報了一個OOM, 堆棧如下(內容太多, 后面部分省略):
Fatal Exception: java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
at java.lang.Thread.nativeCreate(Thread.java)
at java.lang.Thread.start(Thread.java:1078)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:921)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1339)
at com.nostra13.universalimageloader.core.ImageLoaderEngine.submit(ImageLoaderEngine.java:69)
at com.nostra13.universalimageloader.core.ImageLoader.displayImage(ImageLoader.java:299)
at com.nostra13.universalimageloader.core.ImageLoader.displayImage(ImageLoader.java:209)
at com.nostra13.universalimageloader.core.ImageLoader.displayImage(ImageLoader.java:316)
at com.a.b.c.adapter.ImagesGridAdapter.getView(ImagesGridAdapter.java:156)
at android.widget.AbsListView.obtainView(AbsListView.java:2370)
at android.widget.GridView.makeAndAddView(GridView.java:1441)
at android.widget.GridView.makeRow(GridView.java:368)
......
創建線程時OOM了!! fabric上可以看到程序crash時, 有哪些線程在運行, 還可以看到這些線程當時的堆棧信息. 從中發現UIL中有一類線程竟多達369個, 如下:
在UIL源碼的com.nostra13.universalimageloader.core.DefaultConfigurationFactory
類中有創建這類線程, 相關代碼如下:
public static Executor createTaskDistributor() {
return Executors.newCachedThreadPool(createThreadFactory(Thread.NORM_PRIORITY, "uil-pool-d-"));
}
private static ThreadFactory createThreadFactory(int threadPriority, String threadNamePrefix) {
return new DefaultThreadFactory(threadPriority, threadNamePrefix);
}
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private final int threadPriority;
DefaultThreadFactory(int threadPriority, String threadNamePrefix) {
this.threadPriority = threadPriority;
group = Thread.currentThread().getThreadGroup();
namePrefix = threadNamePrefix + poolNumber.getAndIncrement() + "-thread-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon()) t.setDaemon(false);
t.setPriority(threadPriority);
return t;
}
}
可以看到, 上面的代碼createTaskDistributor()
方法是在創建一個分發任務的線程池. 這是個什么樣的線程池呢? 我們來看看Executors類的newCachedThreadPool()方法到底創建了一個什么樣的線程池. 線程池的創建代碼如下:
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
下面是線程池類的構造方法:
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters and default rejected execution handler.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
可看到, 創建線程池時, 參數 "最大線程數"(maximumPoolSize)傳的是Integer.MAX_VALUE. 也就是說這個線程池中可以有Integer.MAX_VALUE個 (2147483647, 64位系統)線程 !!!! 如果無限制的創建線程那當然會耗盡資源, 從而導致OOM ! 我們應該盡量重用線程, 減少創建新線程(創建線程也需要耗時). 用線程池也就是為了達到這個目的.
上面創建的線程池是個可伸縮的線程池, 只有在沒有線程可用是才會創建新線程, 那么300多個線程都在用?! 這個地方就有問題了. 分發線程池的工作不會太耗時, 又怎么會有這沒多的任務沒有執行完? 可定是分發線程阻塞了!!
看了下fabric中uil-pool-d-n-thread-(index)
這類線程的堆棧, 都是下面這樣的:
沒錯! 確實阻塞了, 阻塞在libcore.io.Posix.access()這個方法 (有興趣的朋友可以看看這個類的源碼).
順著OOM的堆棧看UIL的源碼, 首先我們在app中會調用ImageLoader的displayImage方法來展示圖片. 這個方法有很多重載, 不過其他重載方法最終都會調用下面這個
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = defaultListener;
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
if (TextUtils.isEmpty(uri)) {
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
if (options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
return;
}
if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
if (options.shouldPostProcess()) {
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
} else {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
if (options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable(null);
}
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}
這個方法雖然很長, 但是邏輯并不復雜, 前面大部分都是檢查語句. 檢查完之后從內存緩存中取圖片文件, 如果有圖片文件存在的話就執行展示的邏輯. 否則的話執行第二部分邏輯. 最終會交給ImageLoaderEngine處理, 即最后調用ImageLoaderEngine的submit方法. 我們看看此方法:
/** Submits task to execution pool */
void submit(final LoadAndDisplayImageTask task) {
taskDistributor.execute(new Runnable() {
@Override
public void run() {
File image = configuration.diskCache.get(task.getLoadingUri());
boolean isImageCachedOnDisk = image != null && image.exists();
initExecutorsIfNeed();
if (isImageCachedOnDisk) {
taskExecutorForCachedImages.execute(task);
} else {
taskExecutor.execute(task);
}
}
});
}
ImageLoaderEngine類涉及到3個線程池:
三個線程池的任務分別是 (從上至下): 分發任務(圖片展示任務), 加載和緩存圖片, 網絡請求圖片
taskDistributor
就是前面DefaultConfigurationFactory類的createTaskDistributor()方法創建的分發線程池. 上面的submit方法中的分發邏輯(Runnable中的邏輯)是:
- 從磁盤緩存中去取圖片文件(此處涉及到文件訪問, 任務的阻塞正是由此導致, 后面詳細解釋)
- 圖片文件存在, 任務提交給緩存線程池去處理 (加載圖片文件, 主要IO操作)
- 圖片文件不存在, 任務提交給下載線程池去處理 (請求網絡, 下載圖片, 主要是網絡操作)
taskDistributor在分發任務時, 要訪問圖片文件.
/** Submits task to execution pool */
void submit(final LoadAndDisplayImageTask task) {
taskDistributor.execute(new Runnable() {
@Override
public void run() {
File image = configuration.diskCache.get(task.getLoadingUri());
boolean isImageCachedOnDisk = image != null && image.exists();
initExecutorsIfNeed();
if (isImageCachedOnDisk) {
taskExecutorForCachedImages.execute(task);
} else {
taskExecutor.execute(task);
}
}
});
}
上面代碼中的run方法的第一、二行, 最終會調用到libcore.io.Posix.access(). 這里就阻塞了. 有興趣的同學可去看看源碼. 這里和我以前遇到的問題有點類似, 也是這個libcore.io.Posix類的一個方法阻塞, 最終導致了問題. 想知道更多可以在這里查看http://www.lxweimin.com/p/78b42b3f0cb4.
當發布線程池中的線程阻塞后, 會沒有線程可用. 因此當新任務到來時, 會新創建線程. 然而此線程池有沒有"數量上的限制" (Integer.MAX_VALUE), 所以可以無限制的創建, 最終會導致OOM!!
二. 解決
解決方案一
擴展UIL或修改UIL源碼, 將發布線程池的最大線程數限制在某個合理的范圍內. (不過這并不能從根本上解決問題, 因為阻塞還是會發生(系統API問題), 只是不會導致OOM問題, 圖片加載會失敗.)解決方案二
由于UIL的作者已經停止維護了, 所以可以將圖片框架更新為Fresco/Glide/Picasso等.
我們的項目就將圖片框架更新為Fresco了, 在使用Fresco的時候也遇到問題了. 欲知問題為何, 請看圖片框架使用問題之二: Fresco框架導致的OOM (com.facebook.imagepipeline.memory.BitmapPool.alloc(BitmapPool.java:4055))