对Android中的多图片异步加载的重新思考



对本文有任何问题,可加我的个人微信询问:kymjs666

起因

不知道大家有没有发现,在2.0.4.1(37)版本之前的开源中国客户端首次加载图片的时候,会很慢,尤其是动弹列表中的图片。甚至网速慢的时候感觉图片根本加载不出来。

原因是在下载网络图片的时候使用了多线程并发执行的方式,什么意思呢,也就是开启了多个线程同时去下载多张图片。按照正常的思维来想,做图片加载操作使用多线程,这应该是很正常的,因为在我们的思维中,多线程资源利用率更高,程序响应更快。

举个例子:一个应用程序需要从本地文件系统中读取和处理文件的情景。比方说,从磁盘读取一个文件需要5秒,处理一个文件需要2秒。处理两个文件则需要

  5秒读取文件A
  2秒处理文件A
  5秒读取文件B
  2秒处理文件B
---------------------
  总共需要14秒

从磁盘中读取文件的时候,大部分的CPU时间用于等待磁盘去读取数据。在这段时间里,CPU非常的空闲。它可以做一些别的事情。通过改变操作的顺序,就能够更好的使用CPU资源。看下面的顺序

  5秒读取文件A
  5秒读取文件B + 2秒处理文件A
  2秒处理文件B
---------------------
  总共需要12秒

CPU等待第一个文件被读取完。然后开始读取第二个文件。当第二文件在被读取的时候,CPU会去处理第一个文件。记住,在等待磁盘读取文件的时候,CPU大部分时间是空闲的。总的说来,CPU能够在等待IO的时候做一些其他的事情。这个不一定就是磁盘IO。它也可以是网络的IO,或者用户输入。通常情况下,网络和磁盘的IO比CPU和内存的IO慢的多。

既然采用多线程并发执行可以提高CPU的利用率,可是为什么反倒用在Android的图片加载上变得更慢了?

看到这里,你应该已经想到原因了。就好像我们写代码,一个人同时写两个Android应用,并发布上线。假设应用发布不需要上线审核,且A与B的功能几乎相同。如果我们去先写应用A的一个功能,当A某一个功能写完的时候再去写应用B相同的功能,那么写起来自然是要快很多。这就是并发执行,同时去执行A与B。但是这样A与B完成的总时间是缩短了,可以单个A应用完成的时间却被拉长了。因为写到一半的时候被迫去完成B应用。

所以这也是为什么你会发现37版本之前的OSC客户端图片几乎是同时出来的。

回到Android上,我之前的博客曾讲过:AsyncTask在android2.3的时候线程池是一个核心数为5线程,队列可容纳10线程,最大执行128个任务,这存在一个问题,当你真的有138个并发时,即使手机没被你撑爆,那么超出这个指标应用绝对crash掉。 后来升级到3.0,为了避免并发带来的一些列问题,AsyncTask竟然成为序列执行器了,也就是你即使你同时execute N个AsyncTask,它也是挨个排队执行的。 这一点请同学们一定注意,AsyncTask在3.0以后,是异步的没错,但不是并发的。不知道的同学可以去看看《Thread并发请求封装——深入理解AsyncTask类》《一套完善的Android异步任务类》

现在想来,Android SDK把并行执行改为串行执行也并不无道理。

也许你会担心,那改回串行以后会不会又出现同时加载129个图片的时候崩溃的问题。当然,是不会的。在图片加载的时候已经做了下载任务取消的设计,在listView中加载图片的时候,如果滚动出这一屏,图片的下载任务会马上停止,而去加载新的图片下载。总不会一个屏幕就显示129张图吧。

      /**
       * 取消一个下载请求
       *
       * @param view
       */
      public void cancle(View view) {
        for (BitmapWorkerTask task : taskCollection) {
          if (task.imageView.equals(view)) {
          task.cancelTask();
          taskCollection.remove(task);
          break;
          }
        }
      }