Let’s look at the phenomenon

(GiFs are large and may take a while to load…)

It is obvious that every time an image is selected or unselected it will change the selection state of the image and the image will blink first

The code execution flow when the select button is clicked looks like this

  • After changing the data, call RecyclerView#notifyItemChanged(int position) to change the state of the item, and finally call onBindViewHolder

  • Adapter#onBindViewHolder calls SimpleDraweeView#setImageURI(Uri Uri) to reset the same Uri

  • SimpleDraweeView#setImageURI calls DraweedHoler#setController() to load the image resource

The same SimpleDraweeView won’t flicker unless it’s loaded with another Uri,

In this case, I can only find the answer in the source code…

SimpleDraweeView the process of loading images

Since SimpleDraweeView

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

  public void setImageURI(Uri uri, @Nullable Object callerContext) {
    // The Controller class is important as C in MVC. The old controler is used
    // Resets the previous state to configure the context of the current image request and the content provider DataSource
    DraweeController controller =
        mControllerBuilder
            .setCallerContext(callerContext)
            .setUri(uri)
            .setOldController(getController())
            .build();
    // Call setController of the parent class
    setController(controller);
  }

  public void setController(@Nullable DraweeController draweeController) {
    // Call setController of DraweeHolder
    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);
    }
		// Normally, the view is attached
    if(wasAttached) { attachController(); }}private void attachController(a) {
    if (mIsControllerAttached) {
      return;
    }
    mEventTracker.recordEvent(Event.ON_ATTACH_CONTROLLER);
    mIsControllerAttached = true;
    if(mController ! =null&& mController.getHierarchy() ! =null) {
      // Call controller's onAttachmController.onAttach(); }} #AbstractDraweeController
 public void onAttach(a) {... mIsAttached =true;
    if(! mIsRequestSubmitted) {// Submit the image loading request
      submitRequest();
    }
    if(FrescoSystrace.isTracing()) { FrescoSystrace.endSection(); }}protected void submitRequest(a) {
    if (FrescoSystrace.isTracing()) {
      FrescoSystrace.beginSection("AbstractDraweeController#submitRequest");
    }
    // Get the cache
    final T closeableImage = getCachedImage();
    if(closeableImage ! =null) {...// Call onNewResultInternal if the cache exists
      onNewResultInternal(mId, mDataSource, closeableImage, 1.0 f.true.true.true); ...return; }...// Get the data providermDataSource = 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) {
              // Call onNewResultInternal to set the image
              onNewResultInternal(
                  id, dataSource, image, progress, isFinished, wasImmediate, hasMultipleResults);
            } else if (isFinished) {
              onFailureInternal(id, dataSource, new NullPointerException(), /* isFinished */ true); ...}}};/ / to subscribe to the dataSourcemDataSource.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;
          Drawable setImage drawable 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();
    // Set the drawable of the Actual layer
    mActualImageWrapper.setDrawable(drawable);
    mFadeDrawable.beginBatchMode();
    // Hide the Drawable for each layer
    fadeOutBranches();
    // Display the Drawable of the ACTUAL layer
    fadeInLayer(ACTUAL_IMAGE_INDEX);
    setProgress(progress);
    // If the image is loaded immediately (can be interpreted as cache access), it is displayed directly without animation
    if (immediate) {
      mFadeDrawable.finishTransitionImmediately();
    }
    mFadeDrawable.endBatchMode();
  }

Copy the code

I’m not going to show you a lot of code detail here because of space if you’re interested

Fresco: the process of loading an image

Android open-source framework source code: Fresco

Understand Fresco design principles

In a nutshell, the general process is as follows

  • setImageURI()It creates the controller
  • The controller is configured with a series of information including image requests and so on
  • If DraweeView has attachedsubmitRequest()Initiating image loading
  • First,getCachedImage()Get the cache and display it if it exists
  • If the cache does not exist, subscribe to mDataSourcesetImage()Show the picture

I still can’t see why it’s flashing when I set the image so what did I do when I created the controller

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

So basically you reuse the old Concorller and you do all the initialization and all the Settings and then you call reset() and you hide the image that you’re showing

public void reset(a) {
  // Set the ActualImage to a transparent ColorDrawable
  resetActualImages();
  // Hide everything else and display placeHolder
  resetFade();
}
Copy the code

Maybe it makes sense here that our controller hides the image that we’re showing when we initialize it and then loads the image

But! There’s a Fresco cache! The logic is that the cache is very fast and it hits the cache as soon as it’s initialized and then it’s set up on the interface and the user shouldn’t be aware of it.

But what is the truth

// Make a break point here
final T closeableImage = getCachedImage();
Copy the code

The cache fetched is null

So why is it not in the cache and I wonder if it’s not in the cache

Find the implementation class for memory cache, CountingMemoryCache

// The function that stores the cache
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 returns flase to see why
    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;
}

	// This function is implemented to determine whether there is enough space for caching and returns true if so and flase if not
  private synchronized boolean canCacheNewValue(V value) {
    int newValueSize = mValueDescriptor.getSizeInBytes(value);
    return (newValueSize <= mMemoryCacheParams.maxCacheEntrySize)
        && (getInUseCount() <= mMemoryCacheParams.maxCacheEntries - 1)
        && (getInUseSizeInBytes() <= mMemoryCacheParams.maxCacheSize - newValueSize);
  }


Copy the code

Finally, it turns out that there is no space for cache…

If you are interested in why the cache space is insufficient, you can refer to the above articles (I will write another article to analyze it later).

Here are several ways to solve this problem

  1. SetImageURI () does not set the uri if the uri is the same as the one being displayed
  2. Compression of image resources has achieved the goal of reducing memory footprint
  3. Increasing the cache space allows more images to be added to the cache
  4. Manually releasing cache

Check whether urIs are consistent

In this example, you can make the following changes

        // Check whether the URI in the tag is consistent before loading the image
        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);
        }
Copy the code

Note: notifyItemChanged(int Position) triggers the recyclerView reuse mechanism

NotifyItemChanged (int Position, @nullable Object Payload) can be used to indicate that a partial refresh does not trigger the reuse mechanism

This method can achieve the purpose of no flicker, but the cost of adaptation is relatively high

Compress image resources

Set ResizeOptions to compress the image before it loads

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
Copy the code

This method can achieve the goal of no flicker, but when the number of images occupying the cache increases the flicker problem will recede and the image quality may decrease

Increase cache space

In Fresco “initialization time setting ImagePipelineConfig in config can set BitmapMemoryCacheParamsSupplier to specify the cache size

Make a copy of the default BitmapMemoryCacheParamsSupplier Changing maxMemory / 4 to maxMemory / 3 has achieve the goal of increasing cache space

The Supplier is set as follows

public class MemoryCacheParamsSupplier implements Supplier<MemoryCacheParams> {

    private ActivityManager mActivityManager;

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

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

    private int getMaxCacheSize(a) {
        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 {
              	// Change the cache size here
                return maxMemory / 3; }}}}Copy the code

** This method can achieve the purpose of no flicker, but the flicker problem will recur when the number of images occupying the cache increases and will increase the risk of OOM if the occupied cache is not released **

Manually releasing cache

Call Fresco. GetImagePipeline ().clearMemorycaches () when it’s time to use it. It’s simple, it works faster than you have to

Suggested method (not yet figured out)

Why are references not added to the cache collection queue

Or a suitable time to call CloseableReference. Release () release references

—END—-

If there is anything I understand wrong place welcome to correct, any suggestions welcome to below 👇 comment exchange