当前位置: 移动技术网 > 移动技术>移动开发>Android > Android Volley图片加载功能详解

Android Volley图片加载功能详解

2019年07月24日  | 移动技术网移动技术  | 我要评论

gituhb项目

volley源码中文注释项目我已经上传到github,欢迎大家fork和start.

为什么写这篇博客

本来文章是维护在github上的,但是我在分析imageloader源码过程中与到了一个问题,希望大家能帮助解答.

volley获取网络图片 

本来想分析universal image loader的源码,但是发现volley已经实现了网络图片的加载功能.其实,网络图片的加载也是分几个步骤:
1. 获取网络图片的url.
2. 判断该url对应的图片是否有本地缓存.
3. 有本地缓存,直接使用本地缓存图片,通过异步回调给imageview进行设置.
4. 无本地缓存,就先从网络拉取,保存在本地后,再通过异步回调给imageview进行设置.

我们通过volley源码,看一下volley是否是按照这个步骤实现网络图片加载的.

imagerequest.java

按照volley的架构,我们首先需要构造一个网络图片请求,volley帮我们封装了imagerequest类,我们来看一下它的具体实现:

/** 网络图片请求类. */
@suppresswarnings("unused")
public class imagerequest extends request<bitmap> {
  /** 默认图片获取的超时时间(单位:毫秒) */
  public static final int default_image_request_ms = 1000;

  /** 默认图片获取的重试次数. */
  public static final int default_image_max_retries = 2;

  private final response.listener<bitmap> mlistener;
  private final bitmap.config mdecodeconfig;
  private final int mmaxwidth;
  private final int mmaxheight;
  private imageview.scaletype mscaletype;

  /** bitmap解析同步锁,保证同一时间只有一个bitmap被load到内存进行解析,防止oom. */
  private static final object sdecodelock = new object();

  /**
   * 构造一个网络图片请求.
   * @param url 图片的url地址.
   * @param listener 请求成功用户设置的回调接口.
   * @param maxwidth 图片的最大宽度.
   * @param maxheight 图片的最大高度.
   * @param scaletype 图片缩放类型.
   * @param decodeconfig 解析bitmap的配置.
   * @param errorlistener 请求失败用户设置的回调接口.
   */
  public imagerequest(string url, response.listener<bitmap> listener, int maxwidth, int maxheight,
            imageview.scaletype scaletype, bitmap.config decodeconfig,
            response.errorlistener errorlistener) {
    super(method.get, url, errorlistener);
    mlistener = listener;
    mdecodeconfig = decodeconfig;
    mmaxwidth = maxwidth;
    mmaxheight = maxheight;
    mscaletype = scaletype;
  }

  /** 设置网络图片请求的优先级. */
  @override
  public priority getpriority() {
    return priority.low;
  }

  @override
  protected response<bitmap> parsenetworkresponse(networkresponse response) {
    synchronized (sdecodelock) {
      try {
        return doparse(response);
      } catch (outofmemoryerror e) {
        return response.error(new volleyerror(e));
      }
    }
  }

  private response<bitmap> doparse(networkresponse response) {
    byte[] data = response.data;
    bitmapfactory.options decodeoptions = new bitmapfactory.options();
    bitmap bitmap;
    if (mmaxwidth == 0 && mmaxheight == 0) {
      decodeoptions.inpreferredconfig = mdecodeconfig;
      bitmap = bitmapfactory.decodebytearray(data, 0, data.length, decodeoptions);
    } else {
      // 获取网络图片的真实尺寸.
      decodeoptions.injustdecodebounds = true;
      bitmapfactory.decodebytearray(data, 0, data.length, decodeoptions);
      int actualwidth = decodeoptions.outwidth;
      int actualheight = decodeoptions.outheight;

      int desiredwidth = getresizeddimension(mmaxwidth, mmaxheight,
          actualwidth, actualheight, mscaletype);
      int desireheight = getresizeddimension(mmaxwidth, mmaxheight,
          actualwidth, actualheight, mscaletype);

      decodeoptions.injustdecodebounds = false;
      decodeoptions.insamplesize =
          findbestsamplesize(actualwidth, actualheight, desiredwidth, desireheight);
      bitmap tempbitmap = bitmapfactory.decodebytearray(data, 0, data.length, decodeoptions);

      if (tempbitmap != null && (tempbitmap.getwidth() > desiredwidth ||
          tempbitmap.getheight() > desireheight)) {
        bitmap = bitmap.createscaledbitmap(tempbitmap, desiredwidth, desireheight, true);
        tempbitmap.recycle();
      } else {
        bitmap = tempbitmap;
      }
    }

    if (bitmap == null) {
      return response.error(new volleyerror(response));
    } else {
      return response.success(bitmap, httpheaderparser.parsecacheheaders(response));
    }
  }

  static int findbestsamplesize(
      int actualwidth, int actualheight, int desiredwidth, int desireheight) {
    double wr = (double) actualwidth / desiredwidth;
    double hr = (double) actualheight / desireheight;
    double ratio = math.min(wr, hr);
    float n = 1.0f;
    while ((n * 2) <= ratio) {
      n *= 2;
    }
    return (int) n;
  }

  /** 根据imageview的scaletype设置图片的大小. */
  private static int getresizeddimension(int maxprimary, int maxsecondary, int actualprimary,
                      int actualsecondary, imageview.scaletype scaletype) {
    // 如果没有设置imageview的最大值,则直接返回网络图片的真实大小.
    if ((maxprimary == 0) && (maxsecondary == 0)) {
      return actualprimary;
    }

    // 如果imageview的scaletype为fix_xy,则将其设置为图片最值.
    if (scaletype == imageview.scaletype.fit_xy) {
      if (maxprimary == 0) {
        return actualprimary;
      }
      return maxprimary;
    }

    if (maxprimary == 0) {
      double ratio = (double)maxsecondary / (double)actualsecondary;
      return (int)(actualprimary * ratio);
    }

    if (maxsecondary == 0) {
      return maxprimary;
    }

    double ratio = (double) actualsecondary / (double) actualprimary;
    int resized = maxprimary;

    if (scaletype == imageview.scaletype.center_crop) {
      if ((resized * ratio) < maxsecondary) {
        resized = (int)(maxsecondary / ratio);
      }
      return resized;
    }

    if ((resized * ratio) > maxsecondary) {
      resized = (int)(maxsecondary / ratio);
    }

    return resized;
  }


  @override
  protected void deliverresponse(bitmap response) {
    mlistener.onresponse(response);
  }
}

因为volley本身框架已经实现了对网络请求的本地缓存,所以imagerequest做的主要事情就是解析字节流为bitmap,再解析过程中,通过静态变量保证每次只解析一个bitmap防止oom,使用scaletype和用户设置的maxwidth和maxheight来设置图片大小.
总体来说,imagerequest的实现非常简单,这里不做过多的讲解.imagerequest的缺陷在于:

1.需要用户进行过多的设置,包括图片的大小的最大值.
2.没有图片的内存缓存,因为volley的缓存是基于disk的缓存,有对象反序列化的过程. 

imageloader.java

鉴于以上两个缺点,volley又提供了一个更牛逼的imageloader类.其中,最关键的就是增加了内存缓存.
再讲解imageloader的源码之前,需要先介绍一下imageloader的使用方法.和之前的request请求不同,imageloader并不是new出来直接扔给requestqueue进行调度,它的使用方法大体分为4步:

 •创建一个requestqueue对象. 

requestqueue queue = volley.newrequestqueue(context);

 •创建一个imageloader对象.

imageloader构造函数接收两个参数,第一个是requestqueue对象,第二个是imagecache对象(也就是内存缓存类,我们先不给出具体实现,讲解完imageloader源码之后,我会提供一个利用lru算法的imagecache实现类) 

imageloader imageloader = new imageloader(queue, new imagecache() {
  @override
  public void putbitmap(string url, bitmap bitmap) {}
  @override
  public bitmap getbitmap(string url) { return null; }
});

 •获取一个imagelistener对象. 

imagelistener listener = imageloader.getimagelistener(imageview, r.drawable.default_imgage, r.drawable.failed_image); 

•调用imageloader的get方法加载网络图片. 

imageloader.get(mimageurl, listener, maxwidth, maxheight, scaletype);

有了imageloader的使用方法,我们结合使用方法来看一下imageloader的源码:

@suppresswarnings({"unused", "stringbufferreplaceablebystring"})
public class imageloader {
  /**
   * 关联用来调用imageloader的requestqueue.
   */
  private final requestqueue mrequestqueue;

  /** 图片内存缓存接口实现类. */
  private final imagecache mcache;

  /** 存储同一时间执行的相同cachekey的batchedimagerequest集合. */
  private final hashmap<string, batchedimagerequest> minflightrequests =
      new hashmap<string, batchedimagerequest>();

  private final hashmap<string, batchedimagerequest> mbatchedresponses =
      new hashmap<string, batchedimagerequest>();

  /** 获取主线程的handler. */
  private final handler mhandler = new handler(looper.getmainlooper());


  private runnable mrunnable;

  /** 定义图片k1缓存接口,即将图片的内存缓存工作交给用户来实现. */
  public interface imagecache {
    bitmap getbitmap(string url);
    void putbitmap(string url, bitmap bitmap);
  }

  /** 构造一个imageloader. */
  public imageloader(requestqueue queue, imagecache imagecache) {
    mrequestqueue = queue;
    mcache = imagecache;
  }

  /** 构造网络图片请求成功和失败的回调接口. */
  public static imagelistener getimagelistener(final imageview view, final int defaultimageresid,
                         final int errorimageresid) {
    return new imagelistener() {
      @override
      public void onresponse(imagecontainer response, boolean isimmediate) {
        if (response.getbitmap() != null) {
          view.setimagebitmap(response.getbitmap());
        } else if (defaultimageresid != 0) {
          view.setimageresource(defaultimageresid);
        }
      }

      @override
      public void onerrorresponse(volleyerror error) {
        if (errorimageresid != 0) {
          view.setimageresource(errorimageresid);
        }
      }
    };
  }

  public imagecontainer get(string requesturl, imagelistener imagelistener,
                int maxwidth, int maxheight, scaletype scaletype) {
    // 判断当前方法是否在ui线程中执行.如果不是,则抛出异常.
    throwifnotonmainthread();

    final string cachekey = getcachekey(requesturl, maxwidth, maxheight, scaletype);

    // 从l1级缓存中根据key获取对应的bitmap.
    bitmap cachebitmap = mcache.getbitmap(cachekey);
    if (cachebitmap != null) {
      // l1缓存命中,通过缓存命中的bitmap构造imagecontainer,并调用imagelistener的响应成功接口.
      imagecontainer container = new imagecontainer(cachebitmap, requesturl, null, null);
      // 注意:因为目前是在ui线程中,因此这里是调用onresponse方法,并非回调.
      imagelistener.onresponse(container, true);
      return container;
    }

    imagecontainer imagecontainer =
        new imagecontainer(null, requesturl, cachekey, imagelistener);
    // l1缓存命中失败,则先需要对imageview设置默认图片.然后通过子线程拉取网络图片,进行显示.
    imagelistener.onresponse(imagecontainer, true);

    // 检查cachekey对应的imagerequest请求是否正在运行.
    batchedimagerequest request = minflightrequests.get(cachekey);
    if (request != null) {
      // 相同的imagerequest正在运行,不需要同时运行相同的imagerequest.
      // 只需要将其对应的imagecontainer加入到batchedimagerequest的mcontainers集合中.
      // 当正在执行的imagerequest结束后,会查看当前有多少正在阻塞的imagerequest,
      // 然后对其mcontainers集合进行回调.
      request.addcontainer(imagecontainer);
      return imagecontainer;
    }

    // l1缓存没命中,还是需要构造imagerequest,通过requestqueue的调度来获取网络图片
    // 获取方法可能是:l2缓存(ps:disk缓存)或者http网络请求.
    request<bitmap> newrequest =
        makeimagerequest(requesturl, maxwidth, maxheight, scaletype, cachekey);
    mrequestqueue.add(newrequest);
    minflightrequests.put(cachekey, new batchedimagerequest(newrequest, imagecontainer));

    return imagecontainer;
  }

  /** 构造l1缓存的key值. */
  private string getcachekey(string url, int maxwidth, int maxheight, scaletype scaletype) {
    return new stringbuilder(url.length() + 12).append("#w").append(maxwidth)
        .append("#h").append(maxheight).append("#s").append(scaletype.ordinal()).append(url)
        .tostring();
  }

  public boolean iscached(string requesturl, int maxwidth, int maxheight) {
    return iscached(requesturl, maxwidth, maxheight, scaletype.center_inside);
  }

  private boolean iscached(string requesturl, int maxwidth, int maxheight, scaletype scaletype) {
    throwifnotonmainthread();

    string cachekey = getcachekey(requesturl, maxwidth, maxheight, scaletype);
    return mcache.getbitmap(cachekey) != null;
  }


  /** 当l1缓存没有命中时,构造imagerequest,通过imagerequest和requestqueue获取图片. */
  protected request<bitmap> makeimagerequest(final string requesturl, int maxwidth, int maxheight,
                        scaletype scaletype, final string cachekey) {
    return new imagerequest(requesturl, new response.listener<bitmap>() {
      @override
      public void onresponse(bitmap response) {
        ongetimagesuccess(cachekey, response);
      }
    }, maxwidth, maxheight, scaletype, bitmap.config.rgb_565, new response.errorlistener() {
      @override
      public void onerrorresponse(volleyerror error) {
        ongetimageerror(cachekey, error);
      }
    });
  }

  /** 图片请求失败回调.运行在ui线程中. */
  private void ongetimageerror(string cachekey, volleyerror error) {
    batchedimagerequest request = minflightrequests.remove(cachekey);
    if (request != null) {
      request.seterror(error);
      batchresponse(cachekey, request);
    }
  }

  /** 图片请求成功回调.运行在ui线程中. */
  protected void ongetimagesuccess(string cachekey, bitmap response) {
    // 增加l1缓存的键值对.
    mcache.putbitmap(cachekey, response);

    // 同一时间内最初的imagerequest执行成功后,回调这段时间阻塞的相同imagerequest对应的成功回调接口.
    batchedimagerequest request = minflightrequests.remove(cachekey);
    if (request != null) {
      request.mresponsebitmap = response;
      // 将阻塞的imagerequest进行结果分发.
      batchresponse(cachekey, request);
    }
  }

  private void batchresponse(string cachekey, batchedimagerequest request) {
    mbatchedresponses.put(cachekey, request);
    if (mrunnable == null) {
      mrunnable = new runnable() {
        @override
        public void run() {
          for (batchedimagerequest bir : mbatchedresponses.values()) {
            for (imagecontainer container : bir.mcontainers) {
              if (container.mlistener == null) {
                continue;
              }

              if (bir.geterror() == null) {
                container.mbitmap = bir.mresponsebitmap;
                container.mlistener.onresponse(container, false);
              } else {
                container.mlistener.onerrorresponse(bir.geterror());
              }
            }
          }
          mbatchedresponses.clear();
          mrunnable = null;
        }
      };
      // post the runnable
      mhandler.postdelayed(mrunnable, 100);
    }
  }

  private void throwifnotonmainthread() {
    if (looper.mylooper() != looper.getmainlooper()) {
      throw new illegalstateexception("imageloader must be invoked from the main thread.");
    }
  }

  /** 抽象出请求成功和失败的回调接口.默认可以使用volley提供的imagelistener. */
  public interface imagelistener extends response.errorlistener {
    void onresponse(imagecontainer response, boolean isimmediate);
  }

  /** 网络图片请求的承载对象. */
  public class imagecontainer {
    /** imageview需要加载的bitmap. */
    private bitmap mbitmap;

    /** l1缓存的key */
    private final string mcachekey;

    /** imagerequest请求的url. */
    private final string mrequesturl;

    /** 图片请求成功或失败的回调接口类. */
    private final imagelistener mlistener;

    public imagecontainer(bitmap bitmap, string requesturl, string cachekey,
               imagelistener listener) {
      mbitmap = bitmap;
      mrequesturl = requesturl;
      mcachekey = cachekey;
      mlistener = listener;

    }

    public void cancelrequest() {
      if (mlistener == null) {
        return;
      }

      batchedimagerequest request = minflightrequests.get(mcachekey);
      if (request != null) {
        boolean canceled = request.removecontainerandcancelifnecessary(this);
        if (canceled) {
          minflightrequests.remove(mcachekey);
        }
      } else {
        request = mbatchedresponses.get(mcachekey);
        if (request != null) {
          request.removecontainerandcancelifnecessary(this);
          if (request.mcontainers.size() == 0) {
            mbatchedresponses.remove(mcachekey);
          }
        }
      }
    }

    public bitmap getbitmap() {
      return mbitmap;
    }

    public string getrequesturl() {
      return mrequesturl;
    }
  }

  /**
   * cachekey相同的imagerequest请求抽象类.
   * 判定两个imagerequest相同包括:
   * 1. url相同.
   * 2. maxwidth和maxheight相同.
   * 3. 显示的scaletype相同.
   * 同一时间可能有多个相同cachekey的imagerequest请求,由于需要返回的bitmap都一样,所以用batchedimagerequest
   * 来实现该功能.同一时间相同cachekey的imagerequest只能有一个.
   * 为什么不使用requestqueue的mwaitingrequestqueue来实现该功能?
   * 答:是因为仅靠url是没法判断两个imagerequest相等的.
   */
  private class batchedimagerequest {
    /** 对应的imagerequest请求. */
    private final request<?> mrequest;

    /** 请求结果的bitmap对象. */
    private bitmap mresponsebitmap;

    /** imagerequest的错误. */
    private volleyerror merror;

    /** 所有相同imagerequest请求结果的封装集合. */
    private final linkedlist<imagecontainer> mcontainers = new linkedlist<imagecontainer>();

    public batchedimagerequest(request<?> request, imagecontainer container) {
      mrequest = request;
      mcontainers.add(container);
    }

    public volleyerror geterror() {
      return merror;
    }

    public void seterror(volleyerror error) {
      merror = error;
    }

    public void addcontainer(imagecontainer container) {
      mcontainers.add(container);
    }

    public boolean removecontainerandcancelifnecessary(imagecontainer container) {
      mcontainers.remove(container);
      if (mcontainers.size() == 0) {
        mrequest.cancel();
        return true;
      }
      return false;
    }
  }
}

重大疑问

个人对imageloader的源码有两个重大疑问?

 •batchresponse方法的实现. 

我很奇怪,为什么imageloader类里面要有一个hashmap来保存batchedimagerequest集合呢?

 private final hashmap<string, batchedimagerequest> mbatchedresponses =
    new hashmap<string, batchedimagerequest>();

毕竟batchresponse是在特定的imagerequest执行成功的回调中被调用的,调用代码如下:

  protected void ongetimagesuccess(string cachekey, bitmap response) {
    // 增加l1缓存的键值对.
    mcache.putbitmap(cachekey, response);

    // 同一时间内最初的imagerequest执行成功后,回调这段时间阻塞的相同imagerequest对应的成功回调接口.
    batchedimagerequest request = minflightrequests.remove(cachekey);
    if (request != null) {
      request.mresponsebitmap = response;
      // 将阻塞的imagerequest进行结果分发.
      batchresponse(cachekey, request);
    }
  }

从上述代码可以看出,imagerequest请求成功后,已经从minflightrequests中获取了对应的batchedimagerequest对象.而同一时间被阻塞的相同的imagerequest对应的imagecontainer都在batchedimagerequest的mcontainers集合中.
那我认为,batchresponse方法只需要遍历对应batchedimagerequest的mcontainers集合即可.
但是,imageloader源码中,我认为多余的构造了一个hashmap对象mbatchedresponses来保存batchedimagerequest集合,然后在batchresponse方法中又对集合进行两层for循环各种遍历,实在是非常诡异,求指导.
诡异代码如下:

  private void batchresponse(string cachekey, batchedimagerequest request) {
    mbatchedresponses.put(cachekey, request);
    if (mrunnable == null) {
      mrunnable = new runnable() {
        @override
        public void run() {
          for (batchedimagerequest bir : mbatchedresponses.values()) {
            for (imagecontainer container : bir.mcontainers) {
              if (container.mlistener == null) {
                continue;
              }

              if (bir.geterror() == null) {
                container.mbitmap = bir.mresponsebitmap;
                container.mlistener.onresponse(container, false);
              } else {
                container.mlistener.onerrorresponse(bir.geterror());
              }
            }
          }
          mbatchedresponses.clear();
          mrunnable = null;
        }
      };
      // post the runnable
      mhandler.postdelayed(mrunnable, 100);
    }
  }

我认为的代码实现应该是:

  private void batchresponse(string cachekey, batchedimagerequest request) {
    if (mrunnable == null) {
      mrunnable = new runnable() {
        @override
        public void run() {
          for (imagecontainer container : request.mcontainers) {
            if (container.mlistener == null) {
              continue;
            }

            if (request.geterror() == null) {
              container.mbitmap = request.mresponsebitmap;
              container.mlistener.onresponse(container, false);
            } else {
              container.mlistener.onerrorresponse(request.geterror());
            }
          }
          mrunnable = null;
        }
      };
      // post the runnable
      mhandler.postdelayed(mrunnable, 100);
    }
  }

 •使用imageloader默认提供的imagelistener,我认为存在一个缺陷,即图片闪现问题.当为listview的item设置图片时,需要增加tag判断.因为对应的imageview可能已经被回收利用了. 

自定义l1缓存类

首先说明一下,所谓的l1和l2缓存分别指的是内存缓存和硬盘缓存.
实现l1缓存,我们可以使用android提供的lru缓存类,示例代码如下:

import android.graphics.bitmap;
import android.support.v4.util.lrucache;

/** lru算法的l1缓存实现类. */
@suppresswarnings("unused")
public class imagelrucache implements imageloader.imagecache {
  private lrucache<string, bitmap> mlrucache;

  public imagelrucache() {
    this((int) runtime.getruntime().maxmemory() / 8);
  }

  public imagelrucache(final int cachesize) {
    createlrucache(cachesize);
  }

  private void createlrucache(final int cachesize) {
    mlrucache = new lrucache<string, bitmap>(cachesize) {
      @override
      protected int sizeof(string key, bitmap value) {
        return value.getrowbytes() * value.getheight();
      }
    };
  }

  @override
  public bitmap getbitmap(string url) {
    return mlrucache.get(url);
  }

  @override
  public void putbitmap(string url, bitmap bitmap) {
    mlrucache.put(url, bitmap);
  }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网