Fresco SimpleDraweeView 加载相同图片时闪烁的问题分析

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Fresco SimpleDraweeView 加载相同图片时闪烁的问题分析相关的知识,希望对你有一定的参考价值。

先看一下现象


(gif图比较大,可能要加载一小会。。。)

很明显发现 每次选择或取消图片 会改变图片的选择状态 改变选择状态的同时图片会出先闪烁的问题

点击选择按钮时候的代码执行流程是这样子的

  • 改变数据后 调用RecyclerView#notifyItemChanged(int position)改变item的状态,最终会调用onBindViewHolder

  • Adapter#onBindViewHolder里面调用SimpleDraweeView#setImageURI(Uri uri)重新设一次相同的Uri

  • SimpleDraweeView#setImageURI里面调用了DraweedHoler#setController() 进行图片资源的加载

这个时候就一脸蒙蔽了 在我认知里 同一个SimpleDraweeView除非是加载别的Uri 不然是不会出现闪烁问题的,

既然这样,我就只能在源码中找答案了。。。。

SimpleDraweeView 加载图片的流程

从SimpleDraweeView开始

  public void setImageURI(Uri uri) 
    setImageURI(uri, null);
  

  public void setImageURI(Uri uri, @Nullable Object callerContext) 
    //Controller这个类比较重要 充当着MVC中C的角色 一般是复用老的controler
    //重置之前的状态 配置当前图片请求的上下文和内容提供者DataSource
    DraweeController controller =
        mControllerBuilder
            .setCallerContext(callerContext)
            .setUri(uri)
            .setOldController(getController())
            .build();
    //调用父类的setController
    setController(controller);
  

  public void setController(@Nullable DraweeController draweeController) 
    //调用DraweeHolder的setController
    mDraweeHolder.setController(draweeController);
    super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());
  

	#DraweeHolder
  public void setController(@Nullable DraweeController draweeController) 
    boolean wasAttached = mIsControllerAttached;
    if (wasAttached) 
      detachController();
    

    // Clear the old controller
    if (isControllerValid()) 
      mEventTracker.recordEvent(Event.ON_CLEAR_OLD_CONTROLLER);
      mController.setHierarchy(null);
    
    mController = draweeController;
    if (mController != null) 
      mEventTracker.recordEvent(Event.ON_SET_CONTROLLER);
      mController.setHierarchy(mHierarchy);
     else 
      mEventTracker.recordEvent(Event.ON_CLEAR_CONTROLLER);
    
		//一般情况下 view已经是attach的状态
    if (wasAttached) 
      attachController();
    
  

  private void attachController() 
    if (mIsControllerAttached) 
      return;
    
    mEventTracker.recordEvent(Event.ON_ATTACH_CONTROLLER);
    mIsControllerAttached = true;
    if (mController != null && mController.getHierarchy() != null) 
      //调用controller的onAttach
      mController.onAttach();
    
  

 #AbstractDraweeController
 public void onAttach() 
···
    mIsAttached = true;
    if (!mIsRequestSubmitted) 
      //提交图片加载请求
      submitRequest();
    
    if (FrescoSystrace.isTracing()) 
      FrescoSystrace.endSection();
    
  

  protected void submitRequest() 
    if (FrescoSystrace.isTracing()) 
      FrescoSystrace.beginSection("AbstractDraweeController#submitRequest");
    
    //获取缓存
    final T closeableImage = getCachedImage();
    if (closeableImage != null) 
···
  		//如果缓存存在 直接调用onNewResultInternal
      onNewResultInternal(mId, mDataSource, closeableImage, 1.0f, true, true, true);
···
      return;
    

···
  	//获取数据提供者 
    mDataSource = getDataSource();
···
    final DataSubscriber<T> dataSubscriber =
        new BaseDataSubscriber<T>() 
          @Override
          public void onNewResultImpl(DataSource<T> dataSource) 
            // isFinished must be obtained before image, otherwise we might set intermediate result
            // as final image.
            boolean isFinished = dataSource.isFinished();
            boolean hasMultipleResults = dataSource.hasMultipleResults();
            float progress = dataSource.getProgress();
            T image = dataSource.getResult();
            if (image != null) 
              //调用onNewResultInternal设置图片
              onNewResultInternal(
                  id, dataSource, image, progress, isFinished, wasImmediate, hasMultipleResults);
             else if (isFinished) 
              onFailureInternal(id, dataSource, new NullPointerException(), /* isFinished */ true);
            
          
····
        ;

    //订阅dataSource
    mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor);
···
  

  private void onNewResultInternal(
      String id,
      DataSource<T> dataSource,
      @Nullable T image,
      float progress,
      boolean isFinished,
      boolean wasImmediate,
      boolean deliverTempResult) 
····
      try 
        drawable = createDrawable(image);
       catch (Exception exception) 
        logMessageAndImage("drawable_failed @ onNewResult", image);
        releaseImage(image);
        onFailureInternal(id, dataSource, exception, isFinished);
        return;
      
      T previousImage = mFetchedImage;
      Drawable previousDrawable = mDrawable;
      mFetchedImage = image;
      mDrawable = drawable;
      try 
        // set the new image
        if (isFinished) 
          logMessageAndImage("set_final_result @ onNewResult", image);
          mDataSource = null;
          //主要看setImage这个函数 这把drawable
          mSettableDraweeHierarchy.setImage(drawable, 1f, wasImmediate);
          reportSuccess(id, image, dataSource);
         else if (deliverTempResult) 
          logMessageAndImage("set_temporary_result @ onNewResult", image);
          mSettableDraweeHierarchy.setImage(drawable, 1f, wasImmediate);
          reportSuccess(id, image, dataSource);
          // IMPORTANT: do not execute any instance-specific code after this point
         else 
          logMessageAndImage("set_intermediate_result @ onNewResult", image);
          mSettableDraweeHierarchy.setImage(drawable, progress, wasImmediate);
          reportIntermediateSet(id, image);
          // IMPORTANT: do not execute any instance-specific code after this point
        
       
    ····
  

  #GenericDraweeHierarchy 
  public void setImage(Drawable drawable, float progress, boolean immediate) 
    drawable = WrappingUtils.maybeApplyLeafRounding(drawable, mRoundingParams, mResources);
    drawable.mutate();
    //设置Actual图层的drawable
    mActualImageWrapper.setDrawable(drawable);
    mFadeDrawable.beginBatchMode();
    //隐藏每一层的Drawable
    fadeOutBranches();
    //显示ACTUAL图层的Drawable
    fadeInLayer(ACTUAL_IMAGE_INDEX);
    setProgress(progress);
    //如果是立即加载(可以理解是缓存取的)的图片 不展示动画 直接显示
    if (immediate) 
      mFadeDrawable.finishTransitionImmediately();
    
    mFadeDrawable.endBatchMode();
  

复制代码

因为篇幅的关系,这里没有展示很多代码细节 如果有兴趣的话可以参考

Fresco源码解析:一张图片加载的过程

Android开源框架源码鉴赏:Fresco

理解Fresco的设计原理

简单地说 大概流程如下

  • setImageURI()里面创建了controller
  • controller里面配置了包括图片请求等的一系列信息
  • 如果DraweeView已经attach就submitRequest()发起图片加载
  • getCachedImage()获取缓存,如果缓存存在的话直接展示
  • 如果缓存不存在则订阅mDataSource 拿到数据后setImage()展示图片

在这里 还是看不出来为什么设置图片后会闪烁 那么继续看创建controller的时候做了什么

AbstractDraweeControllerBuilder#build()->buildController()->PipelineDraweeControllerBuilder#obtainController()
->initialize()->super.initialize()->init()->GenericDraweeHierarchy#reset()

简单的说 就是复用了之前的concorller 然后做了各种初始化 各种设置 最后调用的reset()也把正在展示的图片也给隐藏了

public void reset() 
  //把ActualImage设置成透明的一个ColorDrawable
  resetActualImages();
  //把其他的都隐藏了 然后展示placeHolder
  resetFade();

也许到这里就明白了 也就是说我们controller 在做初始化操作的时候把目前展示的图片隐藏了 然后再去加载图片

但是!不是说有Fresco缓存的嘛 !按道理取缓存是很快的 初始化后马上命中缓存 然后设置再界面上 用户应该是感知不出来的。

但是事实是怎么样的呢

//在这打个断点看看 
final T closeableImage = getCachedImage();

发现 取到的缓存是null

那么为什么没取到缓存呢 这个时候怀疑是不是没存进去缓存

找到内存缓存的实现类CountingMemoryCache

//存缓存的函数
public @Nullable CloseableReference<V> cache(
    final K key, final CloseableReference<V> valueRef, final EntryStateObserver<K> observer) 
  Preconditions.checkNotNull(key);
  Preconditions.checkNotNull(valueRef);

  maybeUpdateCacheParams();

  Entry<K, V> oldExclusive;
  CloseableReference<V> oldRefToClose = null;
  CloseableReference<V> clientRef = null;
  synchronized (this) 
    // remove the old item (if any) as it is stale now
    oldExclusive = mExclusiveEntries.remove(key);
    Entry<K, V> oldEntry = mCachedEntries.remove(key);
    if (oldEntry != null) 
      makeOrphan(oldEntry);
      oldRefToClose = referenceToClose(oldEntry);
    
		//发现canCacheNewValue返回是flase 看看为什么
    if (canCacheNewValue(valueRef.get())) 
      Entry<K, V> newEntry = Entry.of(key, valueRef, observer);
      mCachedEntries.put(key, newEntry);
      clientRef = newClientReference(newEntry);
    
  
  CloseableReference.closeSafely(oldRefToClose);
  maybeNotifyExclusiveEntryRemoval(oldExclusive);

  maybeEvictEntries();
  return clientRef;


	//这个函数的实现是判断是否有足够的空间进行缓存 是的话返回true 反之flase
  private synchronized boolean canCacheNewValue(V value) 
    int newValueSize = mValueDescriptor.getSizeInBytes(value);
    return (newValueSize <= mMemoryCacheParams.maxCacheEntrySize)
        && (getInUseCount() <= mMemoryCacheParams.maxCacheEntries - 1)
        && (getInUseSizeInBytes() <= mMemoryCacheParams.maxCacheSize - newValueSize);
  

最后发现 原来是没空间缓存了。。。。。

具体为什么缓存空间不足这个设计到复杂的缓存清除机制 有兴趣的可以参考上面所推荐的几篇文章(以后有空我会另外开一篇文章进行分析)

下面介绍可以解决这个问题的几种办法

  1. setImageURI()之前先判断设置的uri跟正在显示的uri是否一致 一致的话不进行设置
  2. 压缩图片资源 已达到减少内存占用的目的
  3. 增大缓存空间 让更多的图片可以加入到缓存中
  4. 手动释放缓存

判断uri是否一致

在本例中 可以做如下修改

        //加载图片前判断tag里面的uri是否一致
        if (!uri.toString().equals(holder.mShowPictureSdv.getTraceTag())) 
            holder.mShowPictureSdv.setTraceTag(uri.toString());

            mRequestBuilder.setSource(uri);
            DraweeController controller = Fresco.newDraweeControllerBuilder()
                    .setOldController(holder.mShowPictureSdv.getController())
                    .setImageRequest(mRequestBuilder.build())
                    .setAutoPlayAnimations(false)
                    .build();
            holder.mShowPictureSdv.setController(controller);
        

注:notifyItemChanged(int position)会触发recyclerview的复用机制

可以使用 notifyItemChanged(int position, @Nullable Object payload) 表示局部刷新 不会触发复用机制

此方法可以达到不会闪烁的目的,但是适配成本比较高

压缩图片资源

在图片加载之前 设置ResizeOptions 来压缩图片

val mRequestBuilder = ImageRequestBuilder.newBuilderWithSource(uri)
    .setResizeOptions(ResizeOptions(sdvGoodsPic.width, sdvGoodsPic.height))

val controller: DraweeController = Fresco.newDraweeControllerBuilder()
    .setOldController(sdvGoodsPic.controller)
    .setImageRequest(mRequestBuilder.build())
    .build()

sdvGoodsPic.controller = controller

此方法可以达到不会闪烁的目的,但是当占用缓存的图片增多 闪烁的问题会复现 并且图片质量可能有所下降

增大缓存空间

在Fresco初始化的时候设置ImagePipelineConfig 在config里面可以设置BitmapMemoryCacheParamsSupplier来指定缓存大小

把默认的BitmapMemoryCacheParamsSupplier拷贝一份出来 把maxMemory / 4改成maxMemory / 3 已达到增加缓存空间的目的

设置的Supplier如下

public class MemoryCacheParamsSupplier implements Supplier<MemoryCacheParams> 

    private ActivityManager mActivityManager;

    public ZZMemoryCacheParamsSupplier(Context context) 
        mActivityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    

    @Override
    public MemoryCacheParams get() 
        int maxCacheSize = getMaxCacheSize();
        return new MemoryCacheParams(
                maxCacheSize,
                Integer.MAX_VALUE,
                maxCacheSize / 4,
                Integer.MAX_VALUE,
                Integer.MAX_VALUE);
    

    private int getMaxCacheSize() 
        final int maxMemory =
                Math.min(mActivityManager.getMemoryClass() * ByteConstants.MB, Integer.MAX_VALUE);
        if (maxMemory < 32 * ByteConstants.MB) 
            return 4 * ByteConstants.MB;
         else if (maxMemory < 64 * ByteConstants.MB) 
            return 6 * ByteConstants.MB;
         else 
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) 
                return 8 * ByteConstants.MB;
             else 
              	//这里修改缓存大小
                return maxMemory / 3;
            
        
    

此方法可以达到不会闪烁的目的,但是当占用缓存的图片增多 闪烁的问题会复现 并且如果占用的缓存一直不释放 会增加oom的风险

手动释放缓存

适时的时机调用Fresco.getImagePipeline().clearMemoryCaches() 此方法 简单粗暴 见效快 不到迫不得已的时候不建议这么用

建议的方法(还没想出来)

看到底是为什么还持有引用 导致没有被加入缓存回收队列

或者适合的时机调用CloseableReference.release() 释放掉引用

以上是关于Fresco SimpleDraweeView 加载相同图片时闪烁的问题分析的主要内容,如果未能解决你的问题,请参考以下文章

如何使用Fresco

开始使用 Fresco

android继续探索Fresco

RxJava+okhttp3

Picasso,Glide,Fresco那个好?

Android - 膨胀类 com.facebook.drawee.view.SimpleDraweeView 的异常错误