Universal-Image-Loader完全解析--从源代码分析Universal-Image-Loader中的线程池

一般来讲一个网络访问就需要App创建一个线程来执行,但是这也导致了当网络访问比较多的情况下,线程的数目可能积聚增多,虽然Android系统理论上说可以创建无数个线程,但是某一时间段,线程数的急剧增加可能导致系统OOM。在UIL中引入了线程池这种技术来管理线程。合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

前面我们有讲到ImageLoader.displayImage(…)函数中的图片处理流程,但当时有意忽略了线程方面的额处理。UIL中将线程池相关的东西封装在ImageLoaderEngine类中了。让我们回到图片下载的源代码中,也就是ImageLoader.displayImage(…)函数。

 1     public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
 2             ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
 3         //检查UIL的配置是否被初始化
 4         checkConfiguration();
 5         if (imageAware == null) {
 6             throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
 7         }
 8         if (listener == null) {
 9             listener = emptyListener;
10         }
11         if (options == null) {
12             options = configuration.defaultDisplayImageOptions;
13         }
14 
15         if (TextUtils.isEmpty(uri)) {
16             engine.cancelDisplayTaskFor(imageAware);
17             listener.onLoadingStarted(uri, imageAware.getWrappedView());
18             if (options.shouldShowImageForEmptyUri()) {
19                 imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
20             } else {
21                 imageAware.setImageDrawable(null);
22             }
23             listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
24             return;
25         }
26         //计算Bitmap的大小,以便后面解析图片时用
27         ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
28         String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
29         engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
30 
31         listener.onLoadingStarted(uri, imageAware.getWrappedView());
32         //Bitmap是否缓存在内存?
33         Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
34         if (bmp != null && !bmp.isRecycled()) {
35             L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
36 
37             if (options.shouldPostProcess()) {
38                 ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
39                         options, listener, progressListener, engine.getLockForUri(uri));
40                 //处理并显示图片
41                 ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
42                         defineHandler(options));
43                 if (options.isSyncLoading()) {
44                     displayTask.run();
45                 } else {
46                     engine.submit(displayTask);
47                 }
48             } else {
49                 //显示图片
50                 options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
51                 listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
52             }
53         } else {
54             if (options.shouldShowImageOnLoading()) {
55                 imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
56             } else if (options.isResetViewBeforeLoading()) {
57                 imageAware.setImageDrawable(null);
58             }
59             
60             ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
61                     options, listener, progressListener, engine.getLockForUri(uri));
62             //启动一个线程,加载并显示图片
63             LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
64                     defineHandler(options));
65             if (options.isSyncLoading()) {
66                 displayTask.run();
67             } else {
68                 engine.submit(displayTask);
69             }
70         }
71     }

注意上面代码块中的第48行和第68行,当需要加载显示图片的时候,相关的task通过engine.submit(...)函数提交执行,那么submit之后发生了什么呢?engine是ImageLoaderEngine类的一个实例,他主要用来响应displayTask的执行。我们跟进ImageLoaderEngine中看看相关的字段和方法。

 1 class ImageLoaderEngine {
 2 
 3     final ImageLoaderConfiguration configuration;
 4 
 5     private Executor taskExecutor;
 6     private Executor taskExecutorForCachedImages;
 7     private Executor taskDistributor;
 8 
 9     private final Map<Integer, String> cacheKeysForImageAwares = Collections
10             .synchronizedMap(new HashMap<Integer, String>());
11     private final Map<String, ReentrantLock> uriLocks = new WeakHashMap<String, ReentrantLock>();
12 
13     private final AtomicBoolean paused = new AtomicBoolean(false);
14     private final AtomicBoolean networkDenied = new AtomicBoolean(false);
15     private final AtomicBoolean slowNetwork = new AtomicBoolean(false);
16 
17     private final Object pauseLock = new Object();
18 
19     ImageLoaderEngine(ImageLoaderConfiguration configuration) {
20         this.configuration = configuration;
21 
22         taskExecutor = configuration.taskExecutor;
23         taskExecutorForCachedImages = configuration.taskExecutorForCachedImages;
24 
25         taskDistributor = DefaultConfigurationFactory.createTaskDistributor();
26     }
27 
28     /** Submits task to execution pool */
29     void submit(final LoadAndDisplayImageTask task) {
30         taskDistributor.execute(new Runnable() {
31             @Override
32             public void run() {
33                 File image = configuration.diskCache.get(task.getLoadingUri());
34                 boolean isImageCachedOnDisk = image != null && image.exists();
35                 initExecutorsIfNeed();
36                 if (isImageCachedOnDisk) {
37                     taskExecutorForCachedImages.execute(task);
38                 } else {
39                     taskExecutor.execute(task);
40                 }
41             }
42         });
43     }
44 
45     /** Submits task to execution pool */
46     void submit(ProcessAndDisplayImageTask task) {
47         initExecutorsIfNeed();
48         taskExecutorForCachedImages.execute(task);
49     }
50 
51     private void initExecutorsIfNeed() {
52         if (!configuration.customExecutor && ((ExecutorService) taskExecutor).isShutdown()) {
53             taskExecutor = createTaskExecutor();
54         }
55         if (!configuration.customExecutorForCachedImages && ((ExecutorService) taskExecutorForCachedImages)
56                 .isShutdown()) {
57             taskExecutorForCachedImages = createTaskExecutor();
58         }
59     }
60 
61     private Executor createTaskExecutor() {
62         return DefaultConfigurationFactory
63                 .createExecutor(configuration.threadPoolSize, configuration.threadPriority,
64                 configuration.tasksProcessingType);
65     }
66     //省略部分代码....
67 }

注意到第29行submit(final LoadAndDisplayImageTask task)函数,我们发现这个函数通过taskDistributor.execute来执行一个Runnable对象的run(),从代码中不难知道它就是先试读取磁盘缓存,再根据isImageCachedOnDisk判断文件是否有缓存在磁盘中,最后通过不同的taskExecutor来执行对应的任务。我们注意到这个submit函数中出现了taskExecutorForCachedImages、taskExecutor、taskDistributor这三个对象。通过观察ImageLoaderEngine的类字段(第5~7行),我们发现它们其实都是Executor接口的实例。

     我们先来学习一下Executor接口,它规定了线程池的接口。

Executor接口执行已提交的 Runnable 任务的对象。此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。通常使用 Executor 而不是显式地创建线程。例如,可能会使用以下方法,而不是为一组任务中的每个任务调用 new Thread(new(RunnableTask())).start(): Executor executor = anExecutor; executor.execute(new RunnableTask1()); executor.execute(new RunnableTask2()); ...

 上面这段话,说白了我们就是通过Exccutor对象将线程放入线程池中运行的。

    通过观察我们发现,taskExecutorForCachedImages和taskExecutor都是在ImageLoaderEngine.createTaskExecutor()中创建,经过分析我们发现他在DefaultConfigurationFactory.createExecutor中被初始化成ThreadPoolExecutor类型的对象(这是默认情况)。需要注意的是,ThreadPoolExecutor其实是实现了ExecutorService接口的一个实体类。线程池实际表现为 ExecutorService 类的一個实例。通过使用 ExecutorService ,我们可以提交将在未来完成的任务。需要补充说明的是,ExecutorService继承自Executor接口。

    接下来让我们看看ThreadPoolExecutor初始化所需要的参数。

创建一个ThreadPoolExecutor需要的参数:

  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  • runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。
    • 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  • TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

接下来,再让我们分析taskDistributor的创建过程。分析发现,taskDistributor在DefaultConfigurationFactory.createTaskDistributor()中被创建,它是一个ThreadPoolExecutor类型的对象,通过Executors线程池工厂创建。官方文档推荐程序员用它来创建线程池,因为它已经配置好常见的线程池情景。接下来让我们来了解一下Executors工厂方法所能创建的线程池类型。

用Executors静态工厂方法创建的线程池类型:     a) newFixedThreadPool:创建一个定长的线程池。达到最大线程数后,线程数不再增长。如果一个线程由于非预期Exception而结束,线程池会补充一个新的线程。     b) newCachedThreadPool:创建一个可缓存的线程池。当池长度超过处理需求时,可以回收空闲的线程。     c) newSingleThreadPool:创建一个单线程executor。     d) newScheduledThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行。类似于Timer。但是,Timer是基于绝对时间,对系统时钟的改变是敏感的,而ScheduledThreadPoolExecutor只支持相对时间。          1) Timer是创建唯一的线程来执行所有的timer任务。如果一个任务超时了,会导致其他的TimerTask时间准确性出问题。          2)如果TimerTask抛出uncheck 异常,Timer将会产生无法预料的行为。因此,ScheduledThreadPoolExecutor可以完全代替Timer。

再回到上文提到的ImageLoaderEngine.submit(...),从函数中分析可以得知:taskDistributor用来尝试读取磁盘中是否有图片缓存,因为涉及磁盘操作,需要用线程来执行。根据是否有对应的图片缓存,将图片加载的任务分发到对应的执行器。如果图片已经缓存在磁盘,则通过taskExecutorForCachedImages执行,如果图片没有缓存在磁盘,则通过taskExecutor执行。我们注意到这三个都实现了Executor接口,那么为什么要将任务细分在三个线程池中进行呢?这其实这跟线程池的调优有关,如果我们将所有的任务都放在同一个线程池中运行当然是可以的,但是这样的话所有的任务就都只能采取同一种任务优先级和运行策略。显然果要有更好的性能,在线程数比较多并且线程承担的任务不同的情况下,App中最好还是按任务的类别来划分线程池。

     上面的分析又引出一个问题,我们究竟应该如何配置自己的线程池。

合理的配置线程池 要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。 执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。 依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。 建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。

接下来,让我们看看UIL中线程池的配置。

让我们来分析一下,taskDistributor由于在每创建一个新的线程的时候都需要读取一下磁盘,属于IO操作。需要图片缓存的应用一般在需要加载图片的时候,同时创建很多(>5)线程,这些线程一般来得猛去的也快,存活时间不必太长。taskDistributor和taskExecutorForCachedImages涉及网络和磁盘的读取和写入操作,比较耗时。主线程数默认为3,感觉定的低了,实际上IO密集的操作应该定得高一点,以便合理利用CPU的。线程优先级(10为最高,1为最低)为4是比较合理的,因为这些操作只需要后台完成即可,优先级太高可能让界面失去响应。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏移动端开发

iOS 从实际出发理解多线程

前言 ----       多线程很多开发者多多少少相信也都有了解,以前有些东西理解的不是很透,慢慢的积累之后,这方面的东西也需要自己好好的总结一下。多线程从我...

2127
来自专栏Android 研究

OKHttp源码解析(三)--中阶之线程池和消息队列

android的异步任务一般都是用Thread+Handler或者AsyncTask来实现,其中笔者当初经历过各种各样坑,特别是内存泄漏,当初笔者可是相当的欲死...

2133
来自专栏Linux驱动

第1阶段——uboot分析之硬件初始化start.S(4)

分析uboot第一个执行函数_start(cpu/arm920t/start.S)  打开cpu/arm920t/start.S 1 .globl _start...

2488
来自专栏Java帮帮-微信公众号-技术文章全总结

Web-第十天 Cookie&Session学习

当用户访问某些Web应用时,经常会显示出该用户上一次的访问时间。例如,QQ登录成功后,会显示用户上次的登录时间。通过本任务,读者将学会如何使用Cookie技术实...

1523
来自专栏酷玩时刻

支付宝Wap支付你了解多少?

为了方便开发者生成一对RSA密钥支付宝提供一键生成工具,具体如何生成与配置密钥详见签名专区。

2992
来自专栏乐沙弥的世界

快速体验mongoDB分片

1、mongodb分片的实质是将数据分散到不同的物理机器,以分散IO,提供并发与吞吐量 2、mongodb分片依赖于片键,即任意一个需要开启的集合都需要创建...

1432
来自专栏程序员八阿哥

Python从入门到摔门(6):Python Web服务器Tornado使用小结

举例来说,假设某个银行网站有这样的 URL: http://bank.example.com/withdraw?amount=1000000&for=Eve ...

1892
来自专栏james大数据架构

资源等待类型sys.dm_os_wait_stats

动态管理视图  sys.dm_os_wait_stats  返回执行的线程所遇到的所有等待的相关信息。可以使用该聚合视图来诊断 SQL Server 以及特定查...

2217
来自专栏微信公众号:Java团长

从使用到原理学习Java线程池

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销...

953
来自专栏刘望舒

Android系统启动流程(四)Launcher启动过程与系统启动流程

前言 此前的文章我们学习了init进程、Zygote进程和SyetemServer进程的启动过程,这一篇文章我们就来学习Android系统启动流程的最后一步:L...

2348

扫码关注云+社区

领取腾讯云代金券