Fresco源码分析-图片解码

作者: susion哒哒 | 来源:发表于2018-05-29 09:26 被阅读10次

图片编解码

在看Fresco对图片解码处理之前,先大致了解一下什么是图片与图片编解码。

图片编码与压缩

图像编码与压缩的本质就是对将要处理的图像源数据按照一定的规则进行变换和组合,从而使得可以用尽可能少的符号来表示尽可能多的信息。这是因为源图像中常常存在各种各样的冗余:空间冗余、时间冗余、信息熵冗余、结构冗余、知识冗余等,这就使得通过编码来进行压缩称为了可能。

根据压缩效果可以分为有损编码和无损编码。有损编码在编码的过程中把不相干的信息都删除了,只能对原图像进行近似的重建;而无损编码的压缩算法中不仅删除了图像数据中的冗余信息,解压缩时能够精确恢复原图像

图片的解码

即把编码的图片还原为原图像。

常见的图片格式

  • JPEG

最常见的图片格式,它只支持有损压缩,其压缩算法可以精确控制压缩比,以图像质量换得存储空间。

  • PNG

PNG只支持无损压缩,所以它的压缩比是有上限的。相对于 JPEG 和 GIF 来说,它最大的优势在于支持完整的透明通道。

  • GIF

只支持256种颜色、透明通道只有1bit、文件压缩比不高。它唯一的优势就是支持多帧动画,凭借这个特性,它得以从 Windows 1.0 时代流行至今,而且仍然大受欢迎。

  • WebP

产生的目的是以更高的压缩比替代 JPEG,它支持有损和无损压缩、支持完整的透明通道、也支持多帧动画。

在分析图片的解码前,先理一下前面几节已经分析的图片加载逻辑:

  1. 首先会直接去缓存读取,查看时候需要加载的图片已经存在 -> BitmapMemoryCacheGetProducer
  2. 切换到后台线程。 -> ThreadHandoffProducer
  3. 经过多路复用处理。(同一个请求公用一次加载)。 -> BitmapMemoryCacheKeyMultiplexProducer
  4. 缓存获取,其实这里主要是wrapper了Consumer,方便以后图片加载之后缓存到缓存。 -> BitmapMemoryCacheProducer

Fresco 图片解码逻辑概览

在Fresco中图片解码对应的Producer是 -> DecodeProducer。首先可以猜想一点的是,按照前面分析的 producer 的执行逻辑,如果在第一次加载图片,那么DecodeProducer肯定是不会去做图片解码的,图片都没有解码什么呢?它的处理应该是 wrapper consumer,监听图片加载结果,然后做图片的解码。
还是从produceResults看起:

@Override
public void produceResults(final Consumer<CloseableReference<CloseableImage>> consumer, final ProducerContext producerContext) {
  final ImageRequest imageRequest = producerContext.getImageRequest();
  ProgressiveDecoder progressiveDecoder;
  if (!UriUtil.isNetworkUri(imageRequest.getSourceUri())) {
    progressiveDecoder = new LocalImagesProgressiveDecoder(
        consumer,
        producerContext,
        mDecodeCancellationEnabled);
  } else {
    ProgressiveJpegParser jpegParser = new ProgressiveJpegParser(mByteArrayPool);
    progressiveDecoder = new NetworkImagesProgressiveDecoder(
        consumer,
        producerContext,
        jpegParser,
        mProgressiveJpegConfig,
        mDecodeCancellationEnabled);
  }
  mInputProducer.produceResults(progressiveDecoder, producerContext);
}

大致逻辑是请求是本地图片的话那就使用LocalImagesProgressiveDecoder做图片解码,如果是网络图片那就使用NetworkImagesProgressiveDecoder做图片解码。这里重点关注网络图片的解码,即NetworkImagesProgressiveDecoder

NetworkImagesProgressiveDecoder

这个类继承自 ProgressiveDecoder, 它对于父类主要的扩展是,增加了实时对jpeg图片解码的能力,提供在只解码了部分jpeg图片时,数据也可以显示的能力:

@Override
protected synchronized boolean updateDecodeJob(EncodedImage encodedImage, @Status int status) {
  boolean ret = super.updateDecodeJob(encodedImage, status);
  if ((isNotLast(status) || statusHasFlag(status, IS_PARTIAL_RESULT))
      && !statusHasFlag(status, IS_PLACEHOLDER)
      && EncodedImage.isValid(encodedImage)
      && encodedImage.getImageFormat() == DefaultImageFormats.JPEG) {
    if (!mProgressiveJpegParser.parseMoreData(encodedImage)) {
      return false;
    }
    //......
}

因此追踪解码功能,这里主要看它的父类ProgressiveDecoder,先看一下父类的继承结构:

private abstract class ProgressiveDecoder extends DelegatingConsumer<EncodedImage, CloseableReference<CloseableImage>> 

看到继承自DelegatingConsumer,这里就大致明白了,ProgressiveDecoder主要是用来在图片加载完成后对图片做解码处理,因此先看一下onNewResultImpl方法:

 @Override
public void onNewResultImpl(EncodedImage newResult, @Status int status) {
  final boolean isLast = isLast(status);
  if (isLast && !EncodedImage.isValid(newResult)) {
    handleError(new ExceptionWithNoStacktrace("Encoded image is not valid."));
    return;
  }
  //用来检查解码工作是否完成,如果完成,则返回
  if (!updateDecodeJob(newResult, status)) {
    return;
  }
  final boolean isPlaceholder = statusHasFlag(status, IS_PLACEHOLDER);
  if (isLast || isPlaceholder || mProducerContext.isIntermediateResultExpected()) {
    mJobScheduler.scheduleJob(); //解码
  }
}

即如果加载完成图片,则进行图片解码,解码的工作是在指定线程进行的,从mJobScheduler.scheduleJob();可以看出。其实在解码前还做了一个比较重要的事情,如果设置了网络图片sample或者是本地图片,那么就要设置sample value

  if (mDownsampleEnabledForNetwork || !UriUtil.isNetworkUri(request.getSourceUri())) {
    encodedImage.setSampleSize(DownsampleUtil.determineSampleSize(request, encodedImage));
  }
  doDecode(encodedImage, status);

DownsampleUtil.determineSampleSize(request, encodedImage)方法的目的就是依照下载图片的尺寸和请求的大小或旋转参数来决定一个 sample size。继续看核心doDecode

doDecode

前面看了这么多,终于看到图片解码的地方了。。。 这个方法的核心实现是:

  1. 对于不完整的jpeg图片不进行解码操作,图片没有请求完成或图片出错也不进行解码操作
  2. 根据encode图片的 quality 和 decode options做解码操作
  image = mImageDecoder.decode(encodedImage, length, quality, mImageDecodeOptions);

mImageDecoder在这里实际工作的类是DefaultImageDecoder。看一下其decode方法:

 public CloseableImage decode(EncodedImage encodedImage,int length,QualityInfo qualityInfo,ImageDecodeOptions options) {
    ImageFormat imageFormat = encodedImage.getImageFormat();
    if (imageFormat == DefaultImageFormats.JPEG) {
      return decodeJpeg(encodedImage, length, qualityInfo, options);
    } else if (imageFormat == DefaultImageFormats.GIF) {
      return decodeGif(encodedImage, length, qualityInfo, options);
    } else if (imageFormat == DefaultImageFormats.WEBP_ANIMATED) {
      return decodeAnimatedWebp(encodedImage, length, qualityInfo, options);
    } else if (imageFormat == ImageFormat.UNKNOWN) {
      throw new DecodeException("unknown image format", encodedImage);
    }
    return decodeStaticImage(encodedImage, options);
  }

staticImage 与 JPEG 的解码

对于不同的平台使用的解码算法是不同的,追踪解码实现到: ArtDecoder。 这两种格式解码的最终实现方法是

CloseableReference<Bitmap> decodeStaticImageFromStream(InputStream inputStream, BitmapFactory.Options options, @Nullable Rect regionToDecode) 

不同的就是对于 inputStream

对于StaticImage:

decodeStaticImageFromStream(encodedImage.getInputStream(), options, regionToDecode);

对于jpeg:

    InputStream jpegDataStream = encodedImage.getInputStream();
    // At this point the InputStream from the encoded image should not be null since in the
    // pipeline,this comes from a call stack where this was checked before. Also this method needs
    // the InputStream to decode the image so this can't be null.
    Preconditions.checkNotNull(jpegDataStream);
    if (encodedImage.getSize() > length) {
      jpegDataStream = new LimitedInputStream(jpegDataStream, length);
    }
    if (!isJpegComplete) {
      jpegDataStream = new TailAppendingInputStream(jpegDataStream, EOI_TAIL);
    }
    boolean retryOnFail=options.inPreferredConfig != Bitmap.Config.ARGB_8888;

    decodeStaticImageFromStream(jpegDataStream, options, regionToDecode);

这主要是因为jpeg图片支持下载中部分预览。

但是这两种格式的图片的解码最终都会调用 decodeStaticImageFromStream

这个方法内部会去根据 @Nullable Rect regionToDecode 判断是局部解码还是直接整张图片解码,调用相关解码方法:

    Bitmap decodedBitmap = null;
    ByteBuffer byteBuffer = mDecodeBuffers.acquire();
    if (byteBuffer == null) {
      byteBuffer = ByteBuffer.allocate(DECODE_BUFFER_SIZE);
    }
    options.inTempStorage = byteBuffer.array();

    //局部解码参数不为null,则进行局部解码
    if (regionToDecode != null) {
        BitmapRegionDecoder bitmapRegionDecoder = null;
        bitmapToReuse.reconfigure(targetWidth, targetHeight, options.inPreferredConfig);
        bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, true);
        decodedBitmap = bitmapRegionDecoder.decodeRegion(regionToDecode, options);
      } catch (IOException e) {
        FLog.e(TAG, "Could not decode region %s, decoding full bitmap instead.", regionToDecode);
      } finally {
        if (bitmapRegionDecoder != null) {
          bitmapRegionDecoder.recycle();
        }
      }
    }
    //局部解码参数为null或者局部解码失败,则进行整张图片的解码
    if (decodedBitmap == null) {
      decodedBitmap = BitmapFactory.decodeStream(inputStream, null, options);
    }

这两个方法在进行解码是又分别会调用native相关方法去进行解码:

    //BitmapRegionDecoder.java
    private static native Bitmap nativeDecodeRegion(long lbm,
            int start_x, int start_y, int width, int height,
            BitmapFactory.Options options);

    //BitmapFactory.java
    private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
            Rect padding, Options opts);

GIF 与 WebP 的解码

这两种图片的最终解码是由:GifImageWebPImage实现的,解码的细节其实就是创建这两个对象的实例:

  @Override
  public AnimatedImage decode(long nativePtr, int sizeInBytes) {
    return GifImage.create(nativePtr, sizeInBytes);
  }

  public static GifImage create(long nativePtr, int sizeInBytes) {
    ensure();
    Preconditions.checkArgument(nativePtr != 0);
    return nativeCreateFromNativeMemory(nativePtr, sizeInBytes);
  }

  @Override
  public AnimatedImage decode(long nativePtr, int sizeInBytes) {
    return WebPImage.create(nativePtr, sizeInBytes);
  }

  public static WebPImage create(long nativePtr, int sizeInBytes) {
    ensure();
    Preconditions.checkArgument(nativePtr != 0);
    return nativeCreateFromNativeMemory(nativePtr, sizeInBytes);
  }

但实际上这两种图片经过native方法创建后,Fresco经过一系列处理来支持GIF或webp的显示,以GIF为例:

核心是 : getCloseableImage(options, gifImage, bitmapConfig)

 private CloseableImage getCloseableImage(
      ImageDecodeOptions options,
      AnimatedImage image,
      Bitmap.Config bitmapConfig) {
    List<CloseableReference<Bitmap>> decodedFrames = null;
    CloseableReference<Bitmap> previewBitmap = null;
    try {
      final int frameForPreview = options.useLastFrameForPreview ? image.getFrameCount() - 1 : 0;
      if (options.forceStaticImage) {
        return new CloseableStaticBitmap(
            createPreviewBitmap(image, bitmapConfig, frameForPreview),
            ImmutableQualityInfo.FULL_QUALITY,
            0);
      }

      if (options.decodeAllFrames) {
        //拿出一个GIF中所有的帧,每一帧都是一个bigmap
        decodedFrames = decodeAllFrames(image, bitmapConfig);
         //预览帧
        previewBitmap = CloseableReference.cloneOrNull(decodedFrames.get(frameForPreview));
      }

      if (options.decodePreviewFrame && previewBitmap == null) {
        previewBitmap = createPreviewBitmap(image, bitmapConfig, frameForPreview);
      }

      //使用预览帧和所有帧构造一个 AnimatedImageResult
      AnimatedImageResult animatedImageResult = AnimatedImageResult.newBuilder(image)
          .setPreviewBitmap(previewBitmap)
          .setFrameForPreview(frameForPreview)
          .setDecodedFrames(decodedFrames)
          .build();

      //创建可显示的图片
      return new CloseableAnimatedImage(animatedImageResult);
    } finally {
      CloseableReference.closeSafely(previewBitmap);
      CloseableReference.closeSafely(decodedFrames);
    }
  }

上面整个流程可以总结为下面两张图:

image image

相关文章

  • Fresco源码分析-图片解码

    图片编解码 在看Fresco对图片解码处理之前,先大致了解一下什么是图片与图片编解码。 图片编码与压缩 图像编码与...

  • Fresco图片解码部分源码分析及webp vs jpeg指标对

    前言 之前的文章写过webp图片的调研,这篇分析一下fresco的decoder部分的源码,同时从响应、下载、解码...

  • Fresco架构设计赏析

    本文是Fresco源码分析系列的开篇,主要分析Fresco的整体架构、各个组成模块的功能以及图片加载流程,希望通过...

  • 2019Android面试Fresco架构详解

    本文是Fresco源码分析系列的开篇,主要分析Fresco的整体架构、各个组成模块的功能以及图片加载流程,希望通过...

  • Fresco缓存设计分析

    (第一篇)Fresco架构设计赏析 本文是Fresco源码分析系列第二篇文章,主要来看一下Fresco中有关图片缓...

  • Fresco源码分析-Fresco初始化

    本文为分析Fresco源码第一篇,基于fresco 1.8.1。 Fresco官网对于Fresco设计的基本概述 ...

  • Fresco图片显示原理浅析

    (第一篇)Fresco架构设计赏析 (第二篇)Fresco缓存架构分析 本文是Fresco源码分析系列第三篇文章,...

  • Fresco的源码学习

    前言 Fresco android图片加载的框架,facebook出品。 本文是对Fresco框架源码的阅读学习后...

  • Fresco源码分析-图片加载初探

    在使用Fresco时,最常用的莫过于这个方法:void setImageURI(@Nullable String ...

  • Fresco源码分析之Controller

    如果你是第一次看我的Fresco的源码分析系列文章,这里强烈推荐你先阅读我的前面两篇文章Fresco源码分析之Dr...

网友评论

    本文标题:Fresco源码分析-图片解码

    本文链接:https://www.haomeiwen.com/subject/ufjojftx.html