Fresco圖片解碼部分源碼分析及webp vs jpeg指標對比

前言

之前的文章寫過webp圖片的調研,這篇分析一下fresco的decoder部分的源碼,同時從響應、下載、解碼、大小四個指標上對比同一張圖片的webp 與jpg格式。這里響應時間應該與圖片格式本身沒有關系,但這里為了對服務器接口做一個測試也加入了對比;下載時間應該與圖片size成正相關,這里也加入對比,看看結果是否符合預期。根據google官網介紹,目前WebP與JPG相比較,編解碼速度上,毫秒級別上:編碼速度webp比jpg慢10倍,解碼速度慢1.5倍。在我們的使用場景下,編碼速度的影響可以被忽略,因為服務器會在用戶第一次請求時,編碼生成jpg圖片對應的webp圖片,之后都會被緩存下來,可以認為幾乎所有用戶的請求都能命中緩存。解碼方面,則是每個用戶拿到webp圖片都必經的開銷,因此解碼速度是本次測試對比的關鍵指標。

Fresco WebP支持

我們的客戶端使用的是fresco圖片庫,根據其官方文檔說明

Android added webp support in version 4.0 and improved it in 4.2.1:
4.0+ (Ice Cream Sandwich): basic webp support
4.2.1+ (Jelly Beam MR1): support for transparency and losless wepb
Fresco handles webp images by default if the OS supports it. So you can use webp with 4.0+ and trasparency and losless webps from 4.2.1.
Fresco also supports webp for older OS versions. The only thing you need to do is add thewebpsupportlibrary to your dependencies. So if you want to use webps on Gingerbread just add the following line to your gradle build file:
compile 'com.facebook.fresco:webpsupport:1.3.0'

因此我們需要引入webpsupprot庫,這樣子fresco會處理對webp的支持。下面也會從源碼上分析,fresco是如何解碼webp的。

Fresco Producer源碼分析

Producer繼承結構

首先我們看一下Frecso中Producer的繼承結構圖:

fresco producer繼承關系

Producer流水線

ProducerSequenceFactory是專門將生成各類鏈接起來的Producer,根據其中的邏輯,這里將可能涉及層次最深的Uri——網絡Uri的Producer鏈在此列出,它會到每個緩存中查找數據,最后如果都沒有命中,則會去網絡上下載。

順序 Producer 是否必須 功能
1 PostprocessedBitmapMemoryCacheProducer 在Bitmap緩存中查找被PostProcess過的數據
2 PostprocessorProducer 對下層Producer傳上來的數據進行PostProcess
3 BitmapMemoryCacheGetProducer 使Producer序列只讀
4 ThreadHandoffProducer 使下層Producer工作在后臺進程中執行
5 BitmapMemoryCacheKeyMultiplexProducer 使多個相同已解碼內存緩存鍵的ImageRequest都從相同Producer中獲取數據
6 BitmapMemoryCacheProducer 從已解碼的內存緩存中獲取數據
7 DecodeProducer 將下層Producer產生的數據解碼
8 ResizeAndRotateProducer 將下層Producer產生的數據變換
9 EncodedCacheKeyMultiplexProducer 使多個相同未解碼內存緩存鍵的ImageRequest都從相同Producer中獲取數據
10 EncodedMemoryCacheProducer 從未解碼的內存緩存中獲取數據
11 DiskCacheProducer 從文件緩存中獲取數據
12 WebpTranscodeProducer Transcodes WebP to JPEG / PNG
13 NetworkFetchProducer 從網絡上獲取數據

<br />
為了獲得每一張網絡圖片的大小、響應時間、下載時間、decode時間,我們需要探索Fresco的源碼,掛上鉤子去得到這些指標;這里我們關心DecoderProducerNetworkFetchProducer,顧名思義,這兩個Producer分別用于解碼和網絡加載相關。

DecodeProducer解碼過程

DecodeProducer負責將未解碼的數據生產出解碼的數據。先看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);
  }```

通過判斷uri的類型 選擇不同的漸近式解釋器,local和network都繼承自ProgressiveDecoder

在`ProgressiveDecoder`的構造方法中,doDecode(encodedImage, isLast) 進行解析。而真正解析的則是`ImageDecode`r#decodeImage方法,這個方法將encodedImage解析成`CloseableImage`:
```Java
    /** Performs the decode synchronously. */
    private void doDecode(EncodedImage encodedImage, @Status int status) {
      if (isFinished() || !EncodedImage.isValid(encodedImage)) {
        return;
      }
      final String imageFormatStr;
      ImageFormat imageFormat = encodedImage.getImageFormat();
      if (imageFormat != null) {
        imageFormatStr = imageFormat.getName();
      } else {
        imageFormatStr = "unknown";
      }
      final String encodedImageSize;
      final String sampleSize;
      final boolean isLast = isLast(status);
      final boolean isLastAndComplete = isLast && !statusHasFlag(status, IS_PARTIAL_RESULT);
      final boolean isPlaceholder = statusHasFlag(status, IS_PLACEHOLDER);
      if (encodedImage != null) {
        encodedImageSize = encodedImage.getWidth() + "x" + encodedImage.getHeight();
        sampleSize = String.valueOf(encodedImage.getSampleSize());
      } else {
        // We should never be here
        encodedImageSize = "unknown";
        sampleSize = "unknown";
      }
      final String requestedSizeStr;
      final ResizeOptions resizeOptions = mProducerContext.getImageRequest().getResizeOptions();
      if (resizeOptions != null) {
        requestedSizeStr = resizeOptions.width + "x" + resizeOptions.height;
      } else {
        requestedSizeStr = "unknown";
      }
      try {
        long queueTime = mJobScheduler.getQueuedTime();
        long decodeDuration = -1;
        String imageUrl = encodedImage.getEncodedCacheKey().getUriString();
        int length = isLastAndComplete || isPlaceholder ? encodedImage.getSize() : getIntermediateImageEndOffset(encodedImage);
        QualityInfo quality = isLastAndComplete || isPlaceholder ? ImmutableQualityInfo.FULL_QUALITY : getQualityInfo();

        mProducerListener.onProducerStart(mProducerContext.getId(), PRODUCER_NAME);
        CloseableImage image = null;
        try {
          long nowTime = System.currentTimeMillis();
          image = mImageDecoder.decode(encodedImage, length, quality, mImageDecodeOptions);
          decodeDuration = System.currentTimeMillis() - nowTime;
        } catch (Exception e) {
          Map<String, String> extraMap = getExtraMap(image, imageUrl, queueTime, decodeDuration, quality, isLast, imageFormatStr, encodedImageSize, requestedSizeStr, sampleSize);
          mProducerListener.onProducerFinishWithFailure(mProducerContext.getId(), PRODUCER_NAME, e, extraMap);
          handleError(e);
          return;
        }
        Map<String, String> extraMap = getExtraMap(image, imageUrl, queueTime, decodeDuration, quality, isLast, imageFormatStr, encodedImageSize, requestedSizeStr, sampleSize);
        mProducerListener.onProducerFinishWithSuccess(mProducerContext.getId(), PRODUCER_NAME, extraMap);
        handleResult(image, status);
      } finally {
        EncodedImage.closeSafely(encodedImage);
      }
    }

因此我們在#doDecoder方法在decode前后插入解碼時長計算。

  long nowTime = System.currentTimeMillis();
  image = mImageDecoder.decode(encodedImage, length, quality, mImageDecodeOptions);
  decodeDuration = System.currentTimeMillis() - nowTime;

ImageDecoder

DecoderProducer 中是依賴ImageDecoder類,用來將未解碼的EncodeImage解碼成對應的CloseableImageImageDecoder中先判斷未解碼的圖片類型:

  private final ImageDecoder mDefaultDecoder = new ImageDecoder() {
    @Override
    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 IllegalArgumentException("unknown image format");
      }
      return decodeStaticImage(encodedImage, options);
    }
  };

ImageFormatChecker

這個類是根據輸入流來確定圖片的類型。基本原理是根據頭標識去確定類型。根據代碼能看出,這里分為幾種。

  public static final ImageFormat JPEG = new ImageFormat("JPEG", "jpeg");
  public static final ImageFormat PNG = new ImageFormat("PNG", "png");
  public static final ImageFormat GIF = new ImageFormat("GIF", "gif");
  public static final ImageFormat BMP = new ImageFormat("BMP", "bmp");
  public static final ImageFormat WEBP_SIMPLE = new ImageFormat("WEBP_SIMPLE", "webp");
  public static final ImageFormat WEBP_LOSSLESS = new ImageFormat("WEBP_LOSSLESS", "webp");
  public static final ImageFormat WEBP_EXTENDED = new ImageFormat("WEBP_EXTENDED", "webp");
  public static final ImageFormat WEBP_EXTENDED_WITH_ALPHA = new ImageFormat("WEBP_EXTENDED_WITH_ALPHA", "webp");
  public static final ImageFormat WEBP_ANIMATED = new ImageFormat("WEBP_ANIMATED", "webp");

本篇我們關心以下幾種:

  • JPEG
  • WEBP_SIMPLE
  • GIF
  • WEBP_ANIMATED

從是否靜態圖上來看,為兩種:

  • 可動 ,用AnimatedImageFactory進行解析
  • 不可動,用PlatformDecoder進行解析

AnimatedImageFactory

AnimatedImageFactory是一個接口,他的實現類是AnimatedImageFactoryImpl。
在這個類的靜態方法塊種,通過如下代碼 來構造其他依賴包中的對象,這個小技巧我們可以get一下。

  private static AnimatedImageDecoder loadIfPresent(final String className) {
    try {
      Class<?> clazz = Class.forName(className);
      return (AnimatedImageDecoder) clazz.newInstance();
    } catch (Throwable e) {
      return null;
    }
  }

  static {
    sGifAnimatedImageDecoder = loadIfPresent("com.facebook.animated.gif.GifImage");
    sWebpAnimatedImageDecoder = loadIfPresent("com.facebook.animated.webp.WebPImage");
  }

AnimatedImageDecoder又分別有兩個實現:

  • WebpImage
  • GifImage
WebpImage與GifImage

解析分為兩個步驟:

  1. 通過AnimatedImageDecoder解析出AnimatedImage
  2. 利用getCloseableImage從AnimatedImage中構造出CloseableAnimatedImage。這是CloseableImage的之類。
    getCloseableImage的邏輯如下:
  3. 用decodeAllFrames解析出所有幀
  4. 用createPreviewBitmap構造預覽的bitmap
  5. 構造AnimatedImageResult對象
  6. AnimatedImageResult構造CloseableAnimatedImage對象。

PlatformDecoder

PlatformDecoder是一個接口,代表不同平臺。我們看他的實現類有哪些:

PlatformDecoder具體實現
public interface PlatformDecoder {
  /**
   * Creates a bitmap from encoded bytes. Supports JPEG but callers should use {@link
   * #decodeJPEGFromEncodedImage} for partial JPEGs.
   *
   * @param encodedImage the reference to the encoded image with the reference to the encoded bytes
   * @param bitmapConfig the {@link android.graphics.Bitmap.Config} used to create the decoded
   * Bitmap
   * @return the bitmap
   * @throws TooManyBitmapsException if the pool is full
   * @throws java.lang.OutOfMemoryError if the Bitmap cannot be allocated
   */
  CloseableReference<Bitmap> decodeFromEncodedImage(final EncodedImage encodedImage, Bitmap.Config bitmapConfig);

  /**
   * Creates a bitmap from encoded JPEG bytes. Supports a partial JPEG image.
   *
   * @param encodedImage the reference to the encoded image with the reference to the encoded bytes
   * @param bitmapConfig the {@link android.graphics.Bitmap.Config} used to create the decoded
   * Bitmap
   * @param length the number of encoded bytes in the buffer
   * @return the bitmap
   * @throws TooManyBitmapsException if the pool is full
   * @throws java.lang.OutOfMemoryError if the Bitmap cannot be allocated
   */
  CloseableReference<Bitmap> decodeJPEGFromEncodedImage(EncodedImage encodedImage, Bitmap.Config bitmapConfig, int length);
}


getBitmapFactoryOptions 獲取BitmapFactory.Options
decodeByteArrayAsPurgeable 獲取bitmap
pinBitmap 真正的decode

NetworkFetchProducer

NetworkFetchProducer負責從網絡層獲取圖片流,持有NetworkFetcher的實現類;測試代碼中,我們添加了OkHttp3的OkHttpNetworkFetcher作為fetcher;我們關心NetworkFetchProducer中的這個方法:

  private void handleFinalResult(PooledByteBufferOutputStream pooledOutputStream, FetchState fetchState) {
    Map<String, String> extraMap = getExtraMap(fetchState, pooledOutputStream.size());
    ProducerListener listener = fetchState.getListener();
    listener.onProducerFinishWithSuccess(fetchState.getId(), PRODUCER_NAME, extraMap);
    listener.onUltimateProducerReached(fetchState.getId(), PRODUCER_NAME, true);
    notifyConsumer(pooledOutputStream, Consumer.IS_LAST | fetchState.getOnNewResultStatusFlags(), fetchState.getResponseBytesRange(), fetchState.getConsumer());
  }

該方法中FetchState記錄了一張圖片從服務端響應到IO讀取的耗時記錄。同樣的,也是通過ProducerListener的#onProducerFinishWithSuccess方法回調出去。

計算decode & fecth的時間

每個Producer的接口實現,都會持有ProducerContext,?其中的ProducerListener會回調Producer各個階段的事件。我們關心這個方法:

/**

  • Called when a producer successfully finishes processing current unit of work.
  • @param extraMap Additional parameters about the producer. This map is immutable and will
  • throw an exception if attempts are made to modify it.
    */
    void onProducerFinishWithSuccess(String requestId, String producerName, @Nullable Map<String, >String> extraMap);

該方法會在Producer結束時回調出來,我們利用Fresco包里的RequestLoggingListener,便可監聽到DecoderProducerNetworkFetchProducer的回調。

    @Override
    public void onCreate() {
        super.onCreate();
        FLog.setMinimumLoggingLevel(FLog.VERBOSE);
        Set<RequestListener> listeners = new HashSet<>(1);
        listeners.add(new RequestLoggingListener());
        ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
            .setRequestListeners(listeners)
            .setNetworkFetcher(new OkHttpNetworkFetcher(OKHttpFactory.getInstance().getOkHttpClient()))
            .build();
        DraweeConfig draweeConfig = DraweeConfig.newBuilder().setDrawDebugOverlay(DebugOverlayHelper.isDebugOverlayEnabled(this)).build();
        Fresco.initialize(this, config, draweeConfig);
        Fresco.getImagePipeline().clearDiskCaches();
    }

我們通過在Fresco初始化Builder中加入RequestLoggingListener,并改造RequestLoggingListener的onProducerFinishWithSuccess方法:

  @Override
  public synchronized void onProducerFinishWithSuccess(String requestId, String producerName, @Nullable Map<String, String> extraMap) {
    if (FLog.isLoggable(FLog.VERBOSE)) {
      Pair<String, String> mapKey = Pair.create(requestId, producerName);
      Long startTime = mProducerStartTimeMap.remove(mapKey);
      long currentTime = getTime();
      long producerDuration = -1;
      FLog.v(TAG, "time %d: onProducerFinishWithSuccess: " + "{requestId: %s, producer: %s, elapsedTime: %d ms, extraMap: %s}", currentTime, requestId, producerName, producerDuration = getElapsedTime(startTime, currentTime), extraMap);
      if (sOnProducer != null) {
        sOnProducer.onProducer(producerDuration, extraMap, producerName);
      }
    }
  }

通過將Producer的信息回調給外面,至此我們就拿到了每一個Producer的回調信息,通過producerName的過濾就可以拿到關心的信息,這里我們關心DecoderProducerNetworkFetchProducer的信息。

decode時間計算

DecoderProducer的doDecode方法中插入:

 long nowTime = System.currentTimeMillis();
 image = mImageDecoder.decode(encodedImage, length, quality, mImageDecodeOptions);
 decodeDuration = System.currentTimeMillis() - nowTime;

decodeDuration放入onProducerFinishWithSuccess的extraMap當中

network fecther時間計算

OkHttpNetworkFetcher中定義著幾個常量值:

  public static final String QUEUE_TIME = "resp_time"; //修改為響應時間
  public static final String FETCH_TIME = "fetch_time";
  public static final String TOTAL_TIME = "total_time";
  public static final String IMAGE_SIZE = "image_size";
  public static final String IMAGE_URL = "image_url"; //新增
  • QUEUE_TIME為請求丟入請求線程池到最后請求成功響應的時間
  • FETCH_TIME為從response讀完IO流的時間
  • IMAGE_SIZE為response header中的content-length,即圖片大小

這些數據都最終會被丟入extraMap,回調給外面。

數據對比

jpg&webp指標對比:

jpg webp指標對比
格式 圖片數 大小 響應時間 下載時間 解碼時間 總用時
jpg 100 33497748B / 31.9MB 3384ms 5582ms 7225ms 16191ms
webp 100 11127628B / 10.6MB 3388ms 2552ms 9806ms 15746ms

以上數據經過幾輪測試,都接近這個數據對比。 圖片源來自項目線上的圖片,圖片接口來自公司CND接口,使用相同quality參數,帶同一張圖片的不同格式參數。解碼總時間大概是JPG:WEBP = 1 : 1.3左右,接近官方的1.5倍性能差距。總大小上,webp幾乎只有jpg的1/3,遠超超官方的30%,這個估計是大多數jpg沒有經過壓縮就直接上傳了。下載時間基本上與size成正比。

http://p1.music.126.net/6OARlbfxOysQJU5iZ8WKSA==/18769762999688243.jpg?imageView=1&type=webp&quality=100

http://p1.music.126.net/6OARlbfxOysQJU5iZ8WKSA==/18769762999688243.jpg?imageView=1&quality=100

gif&anim-webp指標對比:

gif&anim-webp指標對比
格式 圖片數 大小 響應時間 下載時間 解碼時間 總用時
gif 85 66343142B / 63.26MB 2597ms 6052ms 272ms 8921ms
anim-webp 85 20342068B / 19.39MB 2687ms 3809ms 240ms 6736ms

同樣地, 分別取了同一張GIF圖片的,原始版本與WEBP版本來對比。

http://p1.music.126.net/rhGo28bJP19-T0xmtpg6jw==/19244752021149272.jpg
http://p1.music.126.net/rhGo28bJP19-T0xmtpg6jw==/19244752021149272.jpg?imageView=1&type=webp&tostatic=0

size壓縮對比也接近1:3;另外這里的解碼時間是不準確的,因為webp與gif在fresco中都是AnimatedImage,他們的decode調的是nativeCreateFromNativeMemory方法,這個方法返回是對應的WebPImageGifImage對象,表中的解碼時間也是構建這個對象的耗時;動圖渲染時,主要調用的是AnimatedDrawableBackendImpl中renderFrame方法。但我們可以粗略認為,每一幀的渲染耗時對比,接近jpg與webp的耗時;因為gif與anim-webp分別是由一幀一幀的jpg與webp組成。

總結

webp與jpg相比,包括anim-webp與gif, 在相同的圖片質量,圖片大小上,webp有著巨大的優勢,解碼速度毫秒級的差距也完全在接收范圍內, 而圖片大小最終轉化為帶寬、存儲空間、加載速度上的優勢。因此在有條件的情況,app中完全可以用webp來替代jpg格式以提升加載速度、降低存儲空間、節省帶寬費用。另外在android上,使用fresco作為圖片庫,可以幾乎無成本的接入webp。

參考
1.http://changety.github.io/blog/2016/01/31/webp-research/
2.https://developers.google.com/speed/webp/faq
3.https://guolei1130.github.io/2016/12/13/fresco%E5%9B%BE%E7%89%87decode%E7%9A%84%E5%A4%A7%E4%BD%93%E6%B5%81%E7%A8%8B/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容