一. 最近项目中的图片加载遇到了不少问题.
我们项目中用的图片加载框架是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(),
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:
* {@code corePoolSize < 0}
* {@code keepAliveTime < 0}
* {@code maximumPoolSize <= 0}
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue 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.jianshu.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))