欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android Volley图片加载功能详解

程序员文章站 2024-03-06 13:15:50
gituhb项目 volley源码中文注释项目我已经上传到github,欢迎大家fork和start. 为什么写这篇博客 本来文章是维护在github上的,但是我在分...

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);
  }
}

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