美文网首页
图片框架使用问题之一: UIL导致的OOM

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

作者: 元亨利贞o | 来源:发表于2016-12-06 18:38 被阅读1158次

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

    我们项目中用的图片加载框架是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))

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/qlkrmttx.html