Android Picasso加载webp格式图片节省流量

最近产品经理一直抱怨图片加载慢,为此客户端开发这边也做了许多努力,比如重定向到CDN,使用webp减小图片大小,使用降低图片压缩质量,更换图片加载框架等等动作。现在讲一下webp格式图片这个方案。

WebP格式,谷歌(google)开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有JPEG的2/3,并能节省大量的服务器带宽资源和数据空间。Facebook Ebay等知名网站已经开始测试并使用WebP格式。
但WebP是一种有损压缩。相较编码JPEG文件,编码同样质量的WebP文件需要占用更多的计算资源。
桌面版Chrome,Android4.0以上可打开WebP格式。 ——摘自百度百科

Picasso原生支持webp格式图片的加载

理论上说,不需要修改任何代码,只要服务器提供支持,Picasso就能像加载jpg一样加载webp格式的图片。那么现在问题来了

1、如何识别是个下载的文件是不是webp格式。

2、如何将webp解析成bitmap的,会不会出现内存上的问题

带着这两个问题,我仔细阅读了一下Picasso的源码,摸清Picasso加载webp文件的流程。首先看一下下载代码,以okhttp下载器为例


@Override public Response load(Uri uri, int networkPolicy) throws IOException {
    CacheControl cacheControl = null;
    //...省略一部分缓存控制代码
    Request.Builder builder = new Request.Builder().url(uri.toString());
    if (cacheControl != null) {
      builder.cacheControl(cacheControl);
    }

    com.squareup.okhttp.Response response = client.newCall(builder.build()).execute();//发起一次新的网络请求
    int responseCode = response.code();
    if (responseCode >= 300) {//如果遇到网络错误
      response.body().close();
      throw new ResponseException(responseCode + " " + response.message(), networkPolicy,
          responseCode);
    }

    boolean fromCache = response.cacheResponse() != null;

    ResponseBody responseBody = response.body();//取出http响应的body,这个body就是图片文件
    return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength());
  }
从下载器中可以看出,okhttp交给Picasso的是一个InputStream,那么Picasso是怎样处理这个输入流的呢,于是继续读代码


 Bitmap hunt() throws IOException {
    Bitmap bitmap = null;

    if (shouldReadFromMemoryCache(memoryPolicy)) {
      //省略从本地LRU缓存获得bitmap的代码
    }

    data.networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;
    RequestHandler.Result result = requestHandler.load(data, networkPolicy);//通过downloader发起网络请求
    if (result != null) {
      loadedFrom = result.getLoadedFrom();//看看是不是来自磁盘缓存
      exifRotation = result.getExifOrientation();//查看exif信息,主要是看旋转角度

      bitmap = result.getBitmap();//尝试能否直接拿到bitmap

      // If there was no Bitmap then we need to decode it from the stream.
      if (bitmap == null) {//如果不能直接拿到bitmap,那么就需要从输入流中转换
        InputStream is = result.getStream();//获取文件输入流,可能是网络流,也可能是磁盘缓存文件的流
        try {
          bitmap = decodeStream(is, data);//重要的方法,从输入流中解析出bitmap
        } finally {
          Utils.closeQuietly(is);
        }
      }
    }

  //...省略根据exif信息调整图片的代码

    return bitmap;
  }
以上代码来自BitmapHunter这个类

其中我们需要的就是decodeStream()这个方法,是这个方法将网络获得的输入流转换成bitmap的,那么这个方法怎么写的呢?


/**
  * Decode a byte stream into a Bitmap. This method will take into account additional information
  * about the supplied request in order to do the decoding efficiently (such as through leveraging
  * {@code inSampleSize}).
  */
  static Bitmap decodeStream(InputStream stream, Request request) throws IOException {
    MarkableInputStream markStream = new MarkableInputStream(stream);
    stream = markStream;

    long mark = markStream.savePosition(65536); // TODO fix this crap.

    final BitmapFactory.Options options = RequestHandler.createBitmapOptions(request);
    final boolean calculateSize = RequestHandler.requiresInSampleSize(options);

    boolean isWebPFile = Utils.isWebPFile(stream);//判断是不是webp格式的图片文件,webp格式使用BitmapFactory与Jpg,png不同
    markStream.reset(mark);
    // When decode WebP network stream, BitmapFactory throw JNI Exception and make app crash.
    // Decode byte array instead
    if (isWebPFile) {
      byte[] bytes = Utils.toByteArray(stream);//将输入流读取成字节数组,此处会不会OOM呢?
      if (calculateSize) {
        BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
        RequestHandler.calculateInSampleSize(request.targetWidth, request.targetHeight, options,//根据imageView的大小计算应该读取的bitmap大小
            request);
      }
      return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);//webp格式的图片要使用decodeByteArray方法,否则会crash
    } else {
      //...省略jpg和png的转换代码
      return bitmap;
    }
  }这里有个很重要的方法是Utils.isWebPFile(),那么这个方法是怎么根据一个输入流判断是jpg还是webp格式的文件呢,肯定不能用扩展名吧,于是点进去看下


private static final int WEBP_FILE_HEADER_SIZE = 12;
private static final String WEBP_FILE_HEADER_RIFF = "RIFF";
private static final String WEBP_FILE_HEADER_WEBP = "WEBP";

 static boolean isWebPFile(InputStream stream) throws IOException {
    byte[] fileHeaderBytes = new byte[WEBP_FILE_HEADER_SIZE];
    boolean isWebPFile = false;
    if (stream.read(fileHeaderBytes, 0, WEBP_FILE_HEADER_SIZE) == WEBP_FILE_HEADER_SIZE) {
      // If a file's header starts with RIFF and end with WEBP, the file is a WebP file
      isWebPFile = WEBP_FILE_HEADER_RIFF.equals(new String(fileHeaderBytes, 0, 4, "US-ASCII"))
          && WEBP_FILE_HEADER_WEBP.equals(new String(fileHeaderBytes, 8, 4, "US-ASCII"));
    }
    return isWebPFile;
  }
实现原理也不麻烦,用的linux常用的判断文件类型的方法,就是读取一个文件的头几个字节,然后转换成ASCII字符看看是啥文件,这样正好也利用了inputStream读取的顺序,很简单的完成了jpg,png和webp文件的识别。

注意:不要在这个方法执行之前对inputStream执行读取操作,否则很有可能无法判断webp文件

如果你的webp文件加载不出来,不要忘了测试这个方法的返回值。

相关推荐