引入问题
最近的工作又用到了Imageloader,之前修改过源码以便于imageloader能够接受加密和解密的图片数据显示,最近在写历史数据界面的时候,发现RecyclerView里面item公用ImageView显示本地和网络图片相间的时候,滑动过快导致图片异步加载出现之后错位的情况。
分析问题并确定问题
出现了这个问题有三种导致这个的情况:
1,RecyclerView的item复用机制出现问题(onBindViewHolder方法的问题)。
2,在item显示逻辑里面出现Visible和Gone之类没有设置默认值的问题。
3,imageloader异步回来之后Imageview判断是否为同一个组件有误导致的问题。
首先检查一下onBindViewHolder,工程使用的是hongyangAndroid开源的万能Adapter,通过查看onBindViewHolder方法,发现并不会导致错位的情况;其次观察item的显示逻辑也是做到了都进行初始化的赋值操作,那么问题就只能怀疑Imageloader框架图片异步加载回来之后的问题了,初步确定问题是出现在第三点,导致这个问题也会有两个原因,要么使用上有问题,要么就是源码有问题,偏向于第一种原因。
解决问题
我们来寻找一下这个Imageloader在显示图片的时候是怎么判断是否是当前组件的,通过查看displayImage方法,寻得ImageAware对象,这个接口有两个实现类:NonViewAware和ViewAware,前者不持有View对象,而且我的工程里使用的displayImage不持有NonViewAware,所以先不讨论这个对象。后者用Reference软引用一个View,这个软引用的View在ImageLoaderEngine里面用Map对象缓存了多个view的软引用,那么到底怎么判断当前View是否为我需要的View呢,我们查看源码发现View的Map里面是通过接口ImageAware的getId()方法作为key,所以我们看看getId的方法:
@Override
public int getId() {
View view = viewRef.get();
return view == null ? super.hashCode() : view.hashCode();
}
可以看到这里明显使用了view的hashCode作为view的唯一标识,我们也可以看到这个方法的注释:
/**
* Returns ID of image aware view. Point of ID is similar to Object's hashCode. This ID should be unique for every
* image view instance and should be the same for same instances. This ID identifies processing task in ImageLoader
* so ImageLoader won't process two image aware views with the same ID in one time. When ImageLoader get new task
* it cancels old task with this ID (if any) and starts new task.
* <p/>
* It's reasonable to return hash code of wrapped view (if any) to prevent displaying non-actual images in view
* because of view re-using.
*/
int getId();
明确提出了“This ID should be unique for every image view instance and should be the same for same instances.”,说这个id必须是唯一的,就这一点我觉得我找到了问题,这个hashCode在我的RecyclerView里面指定不是唯一的,复用item的view导致了view的hashCode也会被复用,我怀疑可能是我不应该这样用,因为NonViewAware类的getId实现是正确的:
@Override
public String getId() {
return TextUtils.isEmpty(imageUri) ? super.hashCode() : imageUri.hashCode();
}
我怀疑这个方法才是我应该使用的,但是我又发现它并不持有View对象,那么我并不能使用这个类,那么我迸发出了改变这个局面的想法,首先基于NonViewAware的启发,我可以直接自己更新UI:
public void displayImage(final String uri, final ImageView imageView, final DisplayImageOptions options) {
imageView.setTag(uri);
displayImage(uri, imageView, options, new ImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {
if (uri.equals(imageView.getTag())) {
imageView.setImageResource(options.getImageResOnLoading());
}
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
if (uri.equals(imageView.getTag())) {
imageView.setImageResource(options.getImageResOnFail());
}
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
if (uri.equals(imageView.getTag())) {
imageView.setImageBitmap(loadedImage);
}
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
if (uri.equals(imageView.getTag())) {
imageView.setImageResource(options.getImageResOnFail());
}
}
});
}
这样很容易解决这个问题,可是我的界面可不止简单的显示view,还又设置.displayer(new RoundedBitmapDisplayer(360)),所以我不接受这个修改方法,因为这样做还得自己实现圆角图片,我们都知道view要显示的url都是唯一的,当时可能多个view显示同一个url还会导致一些问题,在存软引用key的时候,那么我们就把id改为String,这样组合hashCode和url,一并作为key,这样应该能够应对我遇到的问题,也能够解决我目前想得到的异常情况,那么我们帖一下修改代码:
ImageAware:
public interface ImageAware {
/**
* Returns width of image aware view. This value is used to define scale size for original image.
* Can return 0 if width is undefined.<br />
* Is called on UI thread if ImageLoader was called on UI thread. Otherwise - on background thread.
*/
int getWidth();
/**
* Returns height of image aware view. This value is used to define scale size for original image.
* Can return 0 if height is undefined.<br />
* Is called on UI thread if ImageLoader was called on UI thread. Otherwise - on background thread.
*/
int getHeight();
/**
* Returns {@linkplain import com.fly.arm.utils.imageloader.core.assist.ViewScaleType scale type} which is used for
* scaling image for this image aware view. Must <b>NOT</b> return <b>null</b>.
*/
ViewScaleType getScaleType();
/**
* Returns wrapped Android {@link android.view.View View}. Can return <b>null</b> if no view is wrapped or view was
* collected by GC.<br />
* Is called on UI thread if ImageLoader was called on UI thread. Otherwise - on background thread.
*/
View getWrappedView();
/**
* Returns a flag whether image aware view is collected by GC or whatsoever. If so then ImageLoader stop processing
* of task for this image aware view and fires
* {@link import com.fly.arm.utils.imageloader.core.listener.ImageLoadingListener#onLoadingCancelled(String,
* android.view.View) ImageLoadingListener#onLoadingCancelled(String, View)} callback.<br />
* Mey be called on UI thread if ImageLoader was called on UI thread. Otherwise - on background thread.
*
* @return <b>true</b> - if view is collected by GC and ImageLoader should stop processing this image aware view;
* <b>false</b> - otherwise
*/
boolean isCollected();
/**
* Returns ID of image aware view. Point of ID is similar to Object's hashCode. This ID should be unique for every
* image view instance and should be the same for same instances. This ID identifies processing task in ImageLoader
* so ImageLoader won't process two image aware views with the same ID in one time. When ImageLoader get new task
* it cancels old task with this ID (if any) and starts new task.
* <p/>
* It's reasonable to return hash code of wrapped view (if any) to prevent displaying non-actual images in view
* because of view re-using.
*/
String getId();
/**
* Sets image drawable into this image aware view.<br />
* Displays drawable in this image aware view
* {@linkplain import com.fly.arm.utils.imageloader.core.DisplayImageOptions.Builder#showImageForEmptyUri(
*android.graphics.drawable.Drawable) for empty Uri},
* {@linkplain import com.fly.arm.utils.imageloader.core.DisplayImageOptions.Builder#showImageOnLoading(
*android.graphics.drawable.Drawable) on loading} or
* {@linkplain import com.fly.arm.utils.imageloader.core.DisplayImageOptions.Builder#showImageOnFail(
*android.graphics.drawable.Drawable) on loading fail}. These drawables can be specified in
* {@linkplain import com.fly.arm.utils.imageloader.core.DisplayImageOptions display options}.<br />
* Also can be called in {@link import com.fly.arm.utils.imageloader.core.display.BitmapDisplayer BitmapDisplayer}.< br />
* Is called on UI thread if ImageLoader was called on UI thread. Otherwise - on background thread.
*
* @return <b>true</b> if drawable was set successfully; <b>false</b> - otherwise
*/
boolean setImageDrawable(Drawable drawable);
/**
* Sets image bitmap into this image aware view.<br />
* Displays loaded and decoded image {@link android.graphics.Bitmap} in this image view aware.
* Actually it's used only in
* {@link import com.fly.arm.utils.imageloader.core.display.BitmapDisplayer BitmapDisplayer}.< br />
* Is called on UI thread if ImageLoader was called on UI thread. Otherwise - on background thread.
*
* @return <b>true</b> if bitmap was set successfully; <b>false</b> - otherwise
*/
boolean setImageBitmap(Bitmap bitmap);
}
既然更改了这个紧接着必须更改其实现该接口的类(只贴修改的部分):
ViewAware:
@Override
public String getId() {
View view = viewRef.get();
return view == null ? String.valueOf(super.hashCode()) : (view.hashCode() + String.valueOf(view.getTag()));
}
因为改了key,导致map也应该做相对的改动,这个类在ImageLoaderEngine类,也只贴修改部分吧:
class ImageLoaderEngine {
...省略部分···
private final Map<String, String> cacheKeysForImageAwares = Collections
.synchronizedMap(new HashMap<String, String>());
...省略部分···
/**
* Returns URI of image which is loading at this moment into passed {@link import com.fly.arm.utils.imageloader.core.imageaware.ImageAware}
*/
String getLoadingUriForView(ImageAware imageAware) {
return cacheKeysForImageAwares.get(String.valueOf(imageAware.getId()));
}
/**
* Associates <b>memoryCacheKey</b> with <b>imageAware</b>. Then it helps to define image URI is loaded into View at
* exact moment.
*/
void prepareDisplayTaskFor(ImageAware imageAware, String memoryCacheKey) {
cacheKeysForImageAwares.put(String.valueOf(imageAware.getId()), memoryCacheKey);
}
/**
* Cancels the task of loading and displaying image for incoming <b>imageAware</b>.
*
* @param imageAware {@link import com.fly.arm.utils.imageloader.core.imageaware.ImageAware} for which display task
* will be cancelled
*/
void cancelDisplayTaskFor(ImageAware imageAware) {
cacheKeysForImageAwares.remove(String.valueOf(imageAware.getId()));
}
...省略部分···
}
因为上述代码中出现了setTag,所以在对外ImageLoader的displayImage方法,那么我要设置Tag:
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize,
ImageLoadingListener listener,
ImageLoadingProgressListener progressListener) {
imageAware.getWrappedView().setTag(uri);
...代码省略...
}
需要注意的是,在不需要异步图片的item里面,请一定要设置默认TAG:
// 默认设置
holder.setVisible(R.id.iv_unread,false);
holder.getView(R.id.iv_message_type).setTag("");
holder.getView(R.id.iv_message_more).setTag("");
这样修改之后再测试之前错位的情况,一直没有复现,因为也没落下hashCode,我觉得应该不会有所影响。
后记
修改这个问题的时候我总觉得ImageLoader这个大众的框架不应该出现问题,以至于我都就前面两点去解决问题,可是到头来都没能解决问题,但是更改Imageloader之后我目前也没复现这个错位的问题。可能这样改会有问题,如果您正好看到了,希望指正,邮箱redzkh@gmail.com。