图片框架使用问题之一: 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个, 如下:

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

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个线程池:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容