圖片框架使用問題之一: 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.lxweimin.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))

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容