图片框架使用问题之一: UIL导致的OOM

一. 最近项目中的图片加载遇到了不少问题.

我们项目中用的图片加载框架是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导致的OOM_第1张图片
4460A427-B271-4BF1-884C-B76F766A15B3.png

在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)这类线程的堆栈, 都是下面这样的:

图片框架使用问题之一: UIL导致的OOM_第2张图片
EF673A36-079E-4562-86C0-AF775E4BFAB1.png

没错! 确实阻塞了, 阻塞在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个线程池:

图片框架使用问题之一: UIL导致的OOM_第3张图片
1A869798-949F-4A10-993C-EF35F8D07A52.png

三个线程池的任务分别是 (从上至下): 分发任务(图片展示任务), 加载和缓存图片, 网络请求图片

taskDistributor就是前面DefaultConfigurationFactory类的createTaskDistributor()方法创建的分发线程池. 上面的submit方法中的分发逻辑(Runnable中的逻辑)是:

  1. 从磁盘缓存中去取图片文件(此处涉及到文件访问, 任务的阻塞正是由此导致, 后面详细解释)
  2. 图片文件存在, 任务提交给缓存线程池去处理 (加载图片文件, 主要IO操作)
  3. 图片文件不存在, 任务提交给下载线程池去处理 (请求网络, 下载图片, 主要是网络操作)

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!!

二. 解决

  1. 解决方案一
    扩展UIL或修改UIL源码, 将发布线程池的最大线程数限制在某个合理的范围内. (不过这并不能从根本上解决问题, 因为阻塞还是会发生(系统API问题), 只是不会导致OOM问题, 图片加载会失败.)

  2. 解决方案二
    由于UIL的作者已经停止维护了, 所以可以将图片框架更新为Fresco/Glide/Picasso等.

我们的项目就将图片框架更新为Fresco了, 在使用Fresco的时候也遇到问题了. 欲知问题为何, 请看图片框架使用问题之二: Fresco框架导致的OOM (com.facebook.imagepipeline.memory.BitmapPool.alloc(BitmapPool.java:4055))

你可能感兴趣的:(图片框架使用问题之一: UIL导致的OOM)