Android AVIF改造,助力企业节省cdn流量

817 阅读35分钟

一、背景

图片加载虽然在使用方案上已接近成熟,但在如今越来越看重体验的大环境下,对图片库的要求也日益攀升。

从成本的角度来看,使用AVIF格式可以节省大量的网络带宽和存储空间,减少图片加载时间,改善用户体验,从而节约大量的费用。

AVIF格式能够带来许多优势:

首先,AVIF格式具有明显的压缩率优势,可以比其他常用图片格式(如JPEG、PNG、WEBP)节省更多的存储空间,减少图片加载所需时间和带宽,提高网站加载速度,提高访问者的体验;

其次,AVIF格式丰富的特性支持,可以支持更多的设备和浏览器,提高图片的可用性,并可以免专利费的优势;

第三,AVIF格式相对于HEIC格式,在安卓上的稳定性和兼容性更高更好,从android12开始,官方开始支持AVIF,同时android5-android13的第三方解码库表现非常稳定,没出现过闪退的情况;

最后,AVIF格式支持图片的质量优化,可以保证图片的质量,同时节省更多的容量。

二、AVIF图片格式研究

1、什么是AVIF

 AVIF( AV1 Image File Format)是一种由AOM( Alliance for Open Media)开发的基于AV1编解码器的网络图像格式。这是一种开源免版税的图像格式。AVIF支持全分辨率的10位和12位色彩以及HDR。

 大家可以理解为是一种新的图片编码格式,更省流量,具体有多省?或者还有没有其他的优势?接下来会讲到。

2、AVIF在我们app上的落地调研

在此之前,我们使用主流图片格式是webp,在降本增效的浪潮下,通过对基础设施成本的回顾和挖掘,iOS已成功将iOS端图片格式改为HEIC,节省流量的效果显著;

而Android在HEIC图片格式转码的实践并不顺顺利,我们通过尝试不同的解码器,包括阿里,腾讯,以及开源的heic库后,由于碎片化的问题,依然存在部分设备,部分机型,

以及部分HEIC图片情况下出现闪退的情况,通过和阿里云多次沟通,最后发现改造成本和风险太高,我们也没有自定义HEIC解码器的基础,所以Android HEIC图片转换实践在S1期间宣告失败;

但是,随着我们腾出时间对AVIF展开调研后,我们发现AVIF在安卓端的落地性可能非常大。

为了验证可行性,我们确认了以下几个目标和执行顺序:

1、初步实现AVIF解码功能,集成到全局的图片加载器中,目标是能成功显示AVIF

2、开发AVIF Demo,展示50张不同类型的webp图片转AVIF

3、测试设备兼容性 android5-android13

4、测试AVIF对比WebP多少流量

5、测试图片加载速度

2.1 第一步,我们使用使用自定义Fresco解码库,集成到底层框架的ImageLoader里,经过测试:成功实现解码AVIF图片

在这里有一个小问题,阿里云提供的avif解码库无法直接集成到app,应该是用了更高版本的JDK或者kotlin,我们采用了反编译,将其用源码的形式集成进来;

第一个版本的解码器代码如下,详见:MeetYouFrescoAvifDecoder

public class MeetYouFrescoAvifDecoder implements ImageDecoder {
    private static final String TAG="MeetYouFrescoAvifDecoder";
    public MeetYouFrescoAvifDecoder(){
    }

    @Override
    public CloseableImage decode(
            EncodedImage encodedImage,
            int length,
            QualityInfo qualityInfo,
            ImageDecodeOptions options) {
        try {
           ByteBuffer byteBuffer = ByteBufferUtil.fromStream(encodedImage.getInputStream());//ByteBufferUtil.fromFile(file); 
           //获取宽高
            AvifDecoder.Info info = new AvifDecoder.Info();
            if (!AvifDecoder.getInfo(byteBuffer, byteBuffer.remaining(), info)) {
                LogUtils.e(TAG,"获取avif宽高失败!!");
                return null;
            }
            CloseableReference<Bitmap> bitmap = Fresco.getImagePipelineFactory().getPlatformBitmapFactory()
                    .createBitmapInternal(encodedImage.getWidth(),
                            encodedImage.getHeight(), Bitmap.Config.ARGB_8888);
            //解码BITMAP
            if(!AvifDecoder.decode(byteBuffer,byteBuffer.remaining(),bitmap,4)){
                LogUtils.e(TAG,"解码avif失败,return null");
                return null;
            }
            
           CloseableStaticBitmap result= new CloseableStaticBitmap(
                        bitmap,
                        SimpleBitmapReleaser.getInstance(),
                        qualityInfo,
                        encodedImage.getRotationAngle(),
                        encodedImage.getExifOrientation());
          return result;
        }catch (Exception ex){
            ex.printStackTrace();
        }
        return null;
    }

 关于ImageLoader全局拦截,并且还要保证之前OSS URL裁剪拦截器不受影响;
 
 我们在ImageLoader里新设计了一个拦截器,放在了OSS拦截器之后,实现全局ImageLoader图片加载拦截,详见ImageLoader实现,流程大致如下:

image.png

 

2.2、开发AVIF Demo,展示50张不同类型的webp图片转AVIF

    我们从线上找了50张webp的图片,并设置拦截器,将webp的格式转换为avif的,并使用RecyclerView去显示这50张图片,此demo在后续问题上极大的提高了调试的效率;

    效果如下:

image.png

2.3、测试设备兼容性 android5-android13,结论是:都是可以正常展示

          开发好demo之后,找测开分别对Android 5到Android13的系统进行自动化测试。每个系统脚本跑50遍。各系统都可以正常显示avif的图片

 

2.4、测试AVIF对比WebP多少流量,结论是:AVIF的图片比webp的图片大小大概优化50%~60%左右,图片越大优化的效果越明显,甚至达到了70%

   

         基于线上的50张图片,我们通过脚本的形式去对比这50张图片的webp格式和avif格式的大小, 然后我们发现avif的图片比webp的图片大小优化在50%-60%。图片越大效果越明显

       

   示例图如下:

  image.png

    

 2.5、测试图片加载速度,结论是:AVIF解码的时间从各Android系统上看,基本是在200毫秒以内,AVIF的图片解码时间会比webp的长一点。增加在1~40毫秒左右

  基于测开的脚本,每个系统跑50遍之后,会让测开点击生成文件,本地会生成一个解码时间的文件。解码时间是50次的平均值。最终测开通过复制文件路径,进行adb pull出来给到开发。开发将所有系统的解码时间,填写到wiki上方便查看。

  最终我们对比发现,Avif的解码时间比webp的解码时间要长。时间相差在40毫秒左右

结果如下图:  

image.png

image.png

基于以上调研,确认AVIF具备可上线实验的条件,我们开始集成到主项目上实践,同时也遇到了一些问题...

三、实践过程中的一些问题

1、为什么加载过的AVIF图片每次都会重新请求,就好像没命中缓存一样?

     

     经过测试,虽然可以展示图片,但是确实肉眼感知比较慢,怀疑是Fresco的问题;

     我们通过在拦截器里打印:

                                boolean inMemoryCache= FrescoPainter.workspace().isInBitmapMemoryCache(url);
boolean inFileCache = FrescoPainter.workspace().isInDiskCacheSync(url);

     确认了,所有的avif url都没用上内存缓存和磁盘缓存;

    通过分析,我们知道了Fresco的内存缓存和磁盘缓存逻辑分别在EncodeMemoryCacheProducer和DiskCacheWriteProducer里

    这两个类都有一个逻辑,一旦识别图片格式是UNKNOWN,就不会进行下一步,将其交给下一轮的Producer,解决办法是修改此判断条件,将其处理为支持AVIF图片格式

         重新打包fresco后,重新加载的图片不会再请求了,说明命中了缓存。

2、为什么加载图片每次都会大概率重新decode?

解决了缓存的问题后,我们通过对比,发现AVIF快速滑动的时候 图片重新加载会比较慢,肉眼可以感知,经过日志打印,我们发现每次快速滑动都会触发大量的decode。

我们继续在拦截器里打印:

                                boolean inMemoryCache= FrescoPainter.workspace().isInBitmapMemoryCache(url);
boolean inFileCache = FrescoPainter.workspace().isInDiskCacheSync(url);

确认内存缓存非常容易失效,所以造成每次都要从磁盘拿取file进行decode,造成了加载缓慢的原因,那肯定是内存缓存失效了;

3、为什么memory cache那么容易失效

     

     3.1 我们怀疑是Bitmap占用太大引起的,通过断点分析,果然avif的bitmap非常大,几乎都是原图,没有进行压缩

      因此可以断定,Fresco针对AVIF的Bitmap压缩策略失效了。

     3.2  我们知道针对Bitamp的压缩是需要对samplesize进行计算的,Fresco里的关键方法: DownsampleUtil.determineSampleSize()

这里走了一些弯路(我们刚开始通过从ResizeAndRotatoProducer去查找绕了好大一圈),过程不表,最后我们通过断点方法这个方法,找到了关键点:

    
   public static int determineSampleSize(
    final RotationOptions rotationOptions,
    @Nullable final ResizeOptions resizeOptions,
    final EncodedImage encodedImage,
    final int maxBitmapSize) {
  if (!EncodedImage.isMetaDataAvailable(encodedImage)) {
    return DEFAULT_SAMPLE_SIZE;
  }
...
}

       其中这里边的EncodedImage.isMetaDataAvailable(encodedImage)永远返回false,为什么呢?

       public static boolean isMetaDataAvailable(EncodedImage encodedImage) {
return encodedImage.mRotationAngle >= 0 && encodedImage.mWidth >= 0 && encodedImage.mHeight >= 0;
}

       通过断点我们发现这个EncodedImage的宽高永远是-1;

       然后我们就继续跟踪EncodedImage,为什么宽高没有值,最终发现了这样一个方法:EncodedImage#parseMetaData

   public void parseMetaData() {

   final ImageFormat imageFormat = ImageFormatChecker.getImageFormat_WrapIOException(
      getInputStream());
  mImageFormat = imageFormat;
  // BitmapUtil.decodeDimensions has a bug where it will return 100x100 for some WebPs even though
  // those are not its actual dimensions
  final Pair<Integer, Integer> dimensions;
  if (DefaultImageFormats.isWebpFormat(imageFormat)) {
    dimensions = readWebPImageSize();
  } else {
    dimensions = readImageMetaData().getDimensions();
  }
  if (imageFormat == DefaultImageFormats.JPEG && mRotationAngle == UNKNOWN_ROTATION_ANGLE) {
    // Load the JPEG rotation angle only if we have the dimensions
    if (dimensions != null) {
      mExifOrientation = JfifUtil.getOrientation(getInputStream());
      mRotationAngle = JfifUtil.getAutoRotateAngleFromOrientation(mExifOrientation);
    }
  } else if (imageFormat == DefaultImageFormats.HEIF
      && mRotationAngle == UNKNOWN_ROTATION_ANGLE) {
    mExifOrientation = HeifExifUtil.getOrientation(getInputStream());
    mRotationAngle = JfifUtil.getAutoRotateAngleFromOrientation(mExifOrientation);
  } else {
    mRotationAngle = 0;
  }
}

      其中 readWebPImageSize和readImageMetaData都是读取avif文件的stream后来获取宽高的,

      而系统是不支持直接解码的,因此无法读到宽高;而且由于宽高为-1会导致这个方法调用非常频繁,每次都会进行IO读取,如果不处理的话将会造成严重的性能问题。

      所以我们最后通过在这个方法头部加了一个逻辑:判断一下是不是AVIF,如果是的话,用外部的解码器解析出对应的宽高来填充即可;

      但是EncodedImage这个类并不持有url,所以我们对这个类进行了改造,将所有引入的地方传入URL,最后将此方法改造成如下:

   public void parseMetaData() {
  if(AvifUtil.isAvif(mUri)){
      //avif交外部decode
    if (mWidth < 0 || mHeight < 0) {
          if(encodedImageParseMetaListener!=null){
            encodedImageParseMetaListener.onResult(mUri,this);
        }
    }
    return;
  }
  ...
}


   外部设置如下:

   EncodedImage.setEncodedImageParseMetaListener(new EncodedImage.EncodedImageParseMetaListener() {
    @Override
    public EncodedImage onResult(String url, EncodedImage image) {
        try {
            InputStream inputStream  = image.getInputStream();
                if(url!=null
                        && url.contains("format,avif")
                        && inputStream!=null){
                    ByteBuffer byteBuffer = ByteBufferUtil.fromStream(inputStream);//ByteBufferUtil.fromFile(file);
                    AvifDecoder.Info info = new AvifDecoder.Info();
                    if (AvifDecoder.getInfo(byteBuffer, byteBuffer.remaining(), info)) {
                        image.setHeight(info.height);
                        image.setWidth(info.width);
                        LogUtils.e(TAG,"填充avif宽高成功:"+info.width+":"+info.height+" threadid:"+Thread.currentThread().getId());
                    }else{
                        LogUtils.e(TAG,"填充avif宽高失败");
                    }
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }
        return image;
    }
});


    详情可见fresco提交:dea54fd2ac201032bdb38d99df12b504de829dd1

    


 
3.3 最后,我们在解码器里就能拿到正确的sampleSize,然后解码成功后,对bitmap进行压缩:

@Override
public CloseableImage decode(
        EncodedImage encodedImage,
        int length,
        QualityInfo qualityInfo,
        ImageDecodeOptions options) {
    try {
       
        if (encodedImage.getWidth()==0 || encodedImage.getHeight()==0) {
            LogUtils.e(TAG,"encodedImage 宽高为0,不符合,return null");
            return null;
        }
        ByteBuffer byteBuffer = ByteBufferUtil.fromStream(encodedImage.getInputStream());
        Bitmap bitmap =  Bitmap.createBitmap(encodedImage.getWidth(), encodedImage.getHeight(), Bitmap.Config.ARGB_8888)
        if(!AvifDecoder.decode(byteBuffer,byteBuffer.remaining(),bitmap,4)){
            LogUtils.e(TAG,"解码avif失败,return null");
            return null;
        }
        LogUtils.i(TAG,"解码avif成功,宽高:"+bitmap.getWidth()+":"+bitmap.getHeight()+" sampleSize:"+encodedImage.getSampleSize());

        if(encodedImage.getSampleSize()>1){
            Bitmap bitmap1 = compressBitmapBySampleSize(bitmap,encodedImage.getSampleSize());
            CloseableStaticBitmap result= new CloseableStaticBitmap(
                    bitmap1,
                    SimpleBitmapReleaser.getInstance(),
                    qualityInfo,
                    encodedImage.getRotationAngle(),
                    encodedImage.getExifOrientation());
            LogUtils.i(TAG,"压缩avif成功,返回");
            bitmap.recycle();
            return result;
        }else{
            CloseableStaticBitmap result= new CloseableStaticBitmap(
                    bitmap,
                    SimpleBitmapReleaser.getInstance(),
                    qualityInfo,
                    encodedImage.getRotationAngle(),
                    encodedImage.getExifOrientation());
            LogUtils.i(TAG,"不必压缩,直接返回");
            return result;
        }

    }catch (Exception ex){
        ex.printStackTrace();
    }
    return null;
}

4、为什么感觉加载效率比webp低,是哪里出了问题;

   这个问题和问题3一同解了;

5、为何有时候降级加载会失败

我们对ImageLoader开放了自定义解码器,但是当失败后,需要手动将解码器设置为空才可以;

6、经过不断测试,我们发现有许多业务,都没进行URL裁剪,于是推进对应的地方进行了裁剪,主要有以下几种情况:

  • 调用方加载图片的时,设置了ImageLoadParams里的forbidenModifyUrl=true,禁止了cdn裁剪参数拼接,从而禁止了avif的转换;

  • 虽然进行cdn拼接,但是ImageLoadParams的width和height参数没传,导致图片没有根据控件宽高裁剪,造成了流量浪费;

7、发现有些业务,广告图片转 avif后会出现闪烁的情况,但是经过打印 内存和磁盘都存在缓存,这是为什么?

  • 1、经过排查,我们发现首页金刚区被刷新多次,也就是调用了adpater.notifysetChanged方法,然后造成了重复调用,但是为什么webp图片不会闪烁呢?我们的第一反应就是内存缓存又失效了;

  • 2、我们通过在拦截器拦截的时候进行打印:

                                    boolean inMemoryCache= FrescoPainter.workspace().isInBitmapMemoryCache(url);
    boolean inFileCache = FrescoPainter.workspace().isInDiskCacheSync(url);

               发现其实图片是存在内存缓存的,看来在跟踪一下Fresco加载流程了;

  • 3、分析流程

          从ImageLoader #displayImage开始跟踪,跟踪到如下代码,最终发现submitRequest里的getCachedImage每次都返回空,说明没命中缓存;

AbstractDraweeController # submitRequest() {
if (FrescoSystrace.isTracing()) {
FrescoSystrace.beginSection("AbstractDraweeController#submitRequest");
}
final T closeableImage = getCachedImage();
if (closeableImage != null) {
if (FrescoSystrace.isTracing()) {
FrescoSystrace.beginSection("AbstractDraweeController#submitRequest->cache");
}
...

          然后我们来看一下这个getCachedImage为什么每次都返回空?

PipelineDraweeController

@Override
protected @Nullable CloseableReference getCachedImage() {
if (FrescoSystrace.isTracing()) {
FrescoSystrace.beginSection("PipelineDraweeController#getCachedImage");
}
try {
if (mMemoryCache == null || mCacheKey == null) {
return null;
}
// We get the CacheKey

CloseableReference closeableImage = mMemoryCache.get(mCacheKey);
if(closeableImage==null){
Log.e("==>cacheTrace","找不到缓存1 getCachedImage mCacheKey"+mCacheKey.getUriString()+" hashCode:"+mCacheKey.hashCode() +" mMemoryCache hashcode:"+mMemoryCache.hashCode());
}else{
Log.e("==>cacheTrace","找到缓存了! getCachedImage mCacheKey"+mCacheKey.getUriString()+" hashCode:"+mCacheKey.hashCode() +" mMemoryCache hashcode:"+mMemoryCache.hashCode());
}

         原因是mMemoryCache.get(mCacheKey);这个一直返回空,我们很自然的联想到是不是两次加载的key变了,于是,我们所有缓存相关的key进行了打印,

         发现,果然两次刷新之间的CacheKey的hashcode是不一样的,而webp加载的时候,CacheKey的hashCode是一样的,如下:

      

第一次进入:

 测试环境打印:inMemoryCache:false inFileCache:true url:sc.seeyouyima.com/eimg/adimg/…
找不到缓存1 getCachedImage mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,avif hashCode:1046099125 mMemoryCache hashcode:220642999
EncodedMemoryCacheProducer mMemoryCache.cache mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,avif hashCode:1037658768 mMemoryCache hashcode:44672193
InstrumentedMemoryCache mDelegate.cache mCacheKey  hashCode:1037658768 mDelegate hashcode:11805798
BitmapMemoryCacheProducer mMemoryCache.cache mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,avif hashCode:1046099125 mMemoryCache hashcode:220642999
InstrumentedMemoryCache mDelegate.cache mCacheKey  hashCode:1046099125 mDelegate hashcode:150598467

 测试环境打印:inMemoryCache:true inFileCache:true url:sc.seeyouyima.com/eimg/adimg/…
找不到缓存1 getCachedImage mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,avif hashCode:-657669023 mMemoryCache hashcode:220642999
BitmapMemoryCacheProducer mMemoryCache.cache mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,avif hashCode:-657669023 mMemoryCache hashcode:220642999
InstrumentedMemoryCache mDelegate.cache mCacheKey  hashCode:-657669023 mDelegate hashcode:150598467

退出页面后进入

 测试环境打印:inMemoryCache:true inFileCache:true url:sc.seeyouyima.com/eimg/adimg/…
找不到缓存1 getCachedImage mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,avif hashCode:-672642013 mMemoryCache hashcode:220642999
BitmapMemoryCacheProducer mMemoryCache.cache mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,avif hashCode:-672642013 mMemoryCache hashcode:220642999
InstrumentedMemoryCache mDelegate.cache mCacheKey  hashCode:-672642013 mDelegate hashcode:150598467

 测试环境打印:inMemoryCache:true inFileCache:true url:sc.seeyouyima.com/eimg/adimg/…
找不到缓存1 getCachedImage mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,avif hashCode:-435512995 mMemoryCache hashcode:220642999
BitmapMemoryCacheProducer mMemoryCache.cache mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,avif hashCode:-435512995 mMemoryCache hashcode:220642999
InstrumentedMemoryCache mDelegate.cache mCacheKey  hashCode:-435512995 mDelegate hashcode:150598467

 使用webp

 找不到缓存1 getCachedImage mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,webp hashCode:-1204284208 mMemoryCache hashcode:222523016
EncodedMemoryCacheProducer mMemoryCache.cache mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,webp hashCode:1038297626 mMemoryCache hashcode:58689184
InstrumentedMemoryCache mDelegate.cache mCacheKey  hashCode:1038297626 mDelegate hashcode:135348825
BitmapMemoryCacheProducer mMemoryCache.cache mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,webp hashCode:-1204284208 mMemoryCache hashcode:222523016
InstrumentedMemoryCache mDelegate.cache mCacheKey  hashCode:-1204284208 mDelegate hashcode:193637141

 找到缓存了! getCachedImage mCacheKeyhttp://sc.seeyouyima.com/eimg/adimg/2023/2/63f2d22652391_640_264.jpg?x-oss-process=image/format,webp hashCode:-1204284208 mMemoryCache hashcode:222523016

然后我们来看一下CacheKey是怎么生成的,我们最终定位到:

PipelineDraweeControllerBuilder#getCacheKey

cacheKey = cacheKeyFactory.getBitmapCacheKey(
imageRequest,
getCallerContext());

其中:

public CacheKey getBitmapCacheKey(ImageRequest request, Object callerContext) {
  return new BitmapMemoryCacheKey(
      getCacheKeySourceUri(request.getSourceUri()).toString(),
      request.getResizeOptions(),
      request.getRotationOptions(),
      request.getImageDecodeOptions(),
      null,
      null,
      callerContext);
}
public BitmapMemoryCacheKey(
    String sourceString,
    @Nullable ResizeOptions resizeOptions,
    RotationOptions rotationOptions,
    ImageDecodeOptions imageDecodeOptions,
    @Nullable CacheKey postprocessorCacheKey,
    @Nullable String postprocessorName,
    Object callerContext) {
  mSourceString = Preconditions.checkNotNull(sourceString);
  mResizeOptions = resizeOptions;
  mRotationOptions = rotationOptions;
  mImageDecodeOptions = imageDecodeOptions;
  mPostprocessorCacheKey = postprocessorCacheKey;
  mPostprocessorName = postprocessorName;
  mHash = HashCodeUtil.hashCode(
      sourceString.hashCode(),
      (resizeOptions != null) ? resizeOptions.hashCode() : 0,
      rotationOptions.hashCode(),
      mImageDecodeOptions,
      mPostprocessorCacheKey,
      postprocessorName);
  mCallerContext = callerContext;
  mCacheTime = RealtimeSinceBootClock.get().now();
}

我们发现 mHash = HashCodeUtil.hashCode(
sourceString.hashCode(),
 (resizeOptions != null) ? resizeOptions.hashCode() : 0,
 rotationOptions.hashCode(),
 mImageDecodeOptions,
 mPostprocessorCacheKey,
 postprocessorName);

说明rotationOptions以及mImageDecodeOptions是影响hashcode的关键因素,然后我们就要排查一下两次刷新有什么一样的参数,最后发现了这样一个地方:

我们对每个url拦截的时候,都new了个decoder对象,导致每次刷新的时候,mImageDecodeOptions发生了变化,导致CacheKey hashCode出现了变化,然后就找不到对应的缓存了;

解决方案也非常简单:将MeetyouFrescoAvifDecoder做成单例就可以了;

五、上线策略;

虽然阿里云声称AVIF已经有大量客户在用,很多客户也从heic转向avif,甚至bilibili也在今年开始使用avif,内测android5-android13没有异常,但是我们为了保守起见,依然设计了一套实验+降级规则:

image.png

我们针对流量前15的域名进行测试,发现有些域名不支持AVIF转换,这部分通过和阿里云沟通已得到了解决:

六、上线后情况

1、流量开10%:每天超过30张解码失败的用户在100多个;

2、流量开10%:每天失败的图片次数在20000;用户数预计:5000;(图片会降级不影响展示)

3、发现部分图片异常以及解决方案:

  • 部分url和图片没拼接宽高的导致转码失败的,主要集中在社区模块,其使用getMearuseHeight或者Width来填充ImageLoadrParams里的宽高,但是可能拿到为0,导致图片没有拼接,大量的图片宽高都在3000以上,造成了流量浪费;解决方案:C端直接修复+服务端黑名单配置

  • 少数url转码失败,但将url贴到浏览器上可以解码,怀疑是多线程解码问题,需要进一步深入,这部分因为有降级策略所以对用户影响较小,且avif解码失败并不收费

经两周的放量观察,整体cdn流量优化约25%。