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();
复制代码
因为篇幅的关系,这里没有展示很多代码细节 如果有兴趣的话可以参考
简单地说 大概流程如下
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);
最后发现 原来是没空间缓存了。。。。。
具体为什么缓存空间不足这个设计到复杂的缓存清除机制 有兴趣的可以参考上面所推荐的几篇文章(以后有空我会另外开一篇文章进行分析)
下面介绍可以解决这个问题的几种办法
- setImageURI()之前先判断设置的uri跟正在显示的uri是否一致 一致的话不进行设置
- 压缩图片资源 已达到减少内存占用的目的
- 增大缓存空间 让更多的图片可以加入到缓存中
- 手动释放缓存
判断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 加载相同图片时闪烁的问题分析的主要内容,如果未能解决你的问题,请参考以下文章
Android - 膨胀类 com.facebook.drawee.view.SimpleDraweeView 的异常错误