本文记录了我使用Glide中的自定义类型数据加载来解决,复杂数据源下的图像加载问题.
背景
我们的app有一个功能是图书馆搜索,就是模拟网页搜索,从我校的图书馆网页来获取搜索结果.在图书馆的搜索结果页面需要显示图书的封面和图书的基本信息.图书的基本信息,比如作者,出版社,ISBN等信息都能从图书馆的网页上获取,但是图书馆的网页却不提供图书封面,于是我就想到可以使用豆瓣图书的搜索ISBN功能来获得书的封面的url,因为ISBN是唯一的,所以可以确定图书的封面也是唯一的.
但是如果把搜索图书和从豆瓣获取图书封面图片的url这两步都放在一起,就会造成显示速度很慢.我就想,可以先显示搜索结果,获取图片的url的工作在显示搜索结果之后进行.
尝试一
我第一次想到方案是在显示数据的时候去获取图片的url.来看代码
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
/*把ISBN作为数据输入*/
Observable.just("ISBN").flatMap(new Func1<String, Observable<String>>() {
@Override
public Observable<String> call(String s) {
/* 从获取图片url*/
return DoubanApi.getDoubanSearch().doubanSearch(s);
}
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(String o) {
/*获取了URL之后进行加载*/
Glide.with(context).load(doubanBookCoverImage).into((SearchViewHodler) holder).bookImage);
}
});
}
这个方法看起来很好用,使用Rxjava,先在io线程获得url,再在ui线程调用Glide来加载图片.实际用起来就看出问题了—-图片会发生错乱.我们使用Glide就是为了解决从网络加载图片时的错乱问题,他能够解决是因为,它能控制图片出现在屏幕上的时候去请求网络加载,当图片离开屏幕时取消加载,而我们却在他外面包裹了一个Rxjava,打乱了它的控制,所以会出现错乱.
尝试一失败
尝试二
通过搜索,我发现了这一片文章加载网络图片但没URL?不要紧,通过ModelLoader,让Glide直接加载任何奇葩数据源看完之后发现,这正是我要找的.
主要原理
查看官方的wiki
github.com/bumptech/gl…
官方的这个例子是用来自定义请求的图片大小的,不过我们可以看一下它是怎么做的,它继承了BaseGlideUrlLoader,我们看一下这个BaseGlideUrlLoader
都做了什么
/**
* A base class for loading images over http/https. Can be subclassed for use with any model that can be translated
* in to {@link java.io.InputStream} data.
*
* @param <T> The type of the model.
*/
public abstract class BaseGlideUrlLoader<T> implements StreamModelLoader<T> {
private final ModelLoader<GlideUrl, InputStream> concreteLoader;
private final ModelCache<T, GlideUrl> modelCache;
public BaseGlideUrlLoader(Context context) {
this(context, null);
}
public BaseGlideUrlLoader(Context context, ModelCache<T, GlideUrl> modelCache) {
this(Glide.buildModelLoader(GlideUrl.class, InputStream.class, context), modelCache);
}
public BaseGlideUrlLoader(ModelLoader<GlideUrl, InputStream> concreteLoader) {
this(concreteLoader, null);
}
public BaseGlideUrlLoader(ModelLoader<GlideUrl, InputStream> concreteLoader, ModelCache<T, GlideUrl> modelCache) {
this.concreteLoader = concreteLoader;
this.modelCache = modelCache;
}
@Override
public DataFetcher<InputStream> getResourceFetcher(T model, int width, int height) {
GlideUrl result = null;
if (modelCache != null) {
result = modelCache.get(model, width, height);
}
if (result == null) {
String stringURL = getUrl(model, width, height);
if (TextUtils.isEmpty(stringURL)) {
return null;
}
result = new GlideUrl(stringURL, getHeaders(model, width, height));
if (modelCache != null) {
modelCache.put(model, width, height, result);
}
}
return concreteLoader.getResourceFetcher(result, width, height);
}
/**
* Get a valid url http:// or https:// for the given model and dimensions as a string.
*
* @param model The model.
* @param width The width in pixels of the view/target the image will be loaded into.
* @param height The height in pixels of the view/target the image will be loaded into.
* @return The String url.
*/
protected abstract String getUrl(T model, int width, int height);
/**
* Get the headers for the given model and dimensions as a map of strings to sets of strings.
*
* @param model The model.
* @param width The width in pixels of the view/target the image will be loaded into.
* @param height The height in pixels of the view/target the image will be loaded into.
* @return The Headers object containing the headers, or null if no headers should be added.
*/
protected Headers getHeaders(T model, int width, int height) {
return Headers.DEFAULT;
}
}
可以看到这个类实现了StreamModelLoader
接口,转到这个接口,发现这个接口是继承了ModelLoader
,转到`ModelLoader
,看到他有一个需要重写的方法getResourceFetcher
.好了我们回到BaseGlideUrlLoader
中,看它是怎么重写这个方法的.再来看一下代码
/**
* Obtains an {@link DataFetcher} that can fetch the data required to decode the resource represented by this model.
* The {@link DataFetcher} will not be used if the resource is already cached.
*
* <p>
* Note - If no valid data fetcher can be returned (for example if a model has a null URL), then it is
* acceptable to return a null data fetcher from this method. Doing so will be treated any other failure or
* exception during the load process.
* </p>
* @param model The model representing the resource.
* @param width The width in pixels of the view or target the resource will be loaded into, or
* {@link com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate that the resource should
* be loaded at its original width.
* @param height The height in pixels of the view or target the resource will be loaded into, or
* {@link com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate that the resource should
* be loaded at its original height.
* @return A {@link DataFetcher} that can obtain the data the resource can be decoded from if the resource is not
* cached, or null if no valid {@link com.bumptech.glide.load.data.DataFetcher} could be constructed.
*/
@Override
public DataFetcher<InputStream> getResourceFetcher(T model, int width, int height) {
/*新建一个GlideUrl对象*/
GlideUrl result = null;
/*判断是否存在缓存*/
if (modelCache != null) {
/*从缓存中拿到对象*/
result = modelCache.get(model, width, height);
}
/*如果缓存中没有这个对象*/
if (result == null) {
/*拿到Url*/
String stringURL = getUrl(model, width, height);
if (TextUtils.isEmpty(stringURL)) {
return null;
}
/*新建一个GlideUrl对象*/
result = new GlideUrl(stringURL, getHeaders(model, width, height));
if (modelCache != null) {
/*加入缓存*/
modelCache.put(model, width, height, result);
}
}
/*最后返回一个`DataFetcher<InputStream>`对象*/
return concreteLoader.getResourceFetcher(result, width, height);
}
我们可以看到,这就是一个从缓存或是其他位置(网络,储存)获得数据的方法.他会经过一系列的判断,最后才会返回一个DataFetcher<InputStream>
对象,我们再来看一下这个DataFetcher
是干什么的.
**
* An interface for lazily retrieving data that can be used to load a resource. A new instance is created per
* resource load by {@link com.bumptech.glide.load.model.ModelLoader}. {@link #loadData(Priority)} may or may not be
* called for any given load depending on whether or not the corresponding resource is cached. Cancel also may or may
* not be called. If {@link #loadData(Priority)} is called, then so {@link #cleanup()} will be called.
*
* @param <T> The type of data to be loaded (InputStream, byte[], File etc).
*/
public interface DataFetcher<T> {
/**
* Asynchronously fetch data from which a resource can be decoded. This will always be called on
* background thread so it is safe to perform long running tasks here. Any third party libraries called
* must be thread safe since this method will be called from a thread in a
* {@link java.util.concurrent.ExecutorService} that may have more than one background thread.
*
* This method will only be called when the corresponding resource is not in the cache.
*
* <p>
* Note - this method will be run on a background thread so blocking I/O is safe.
* </p>
*
* @param priority The priority with which the request should be completed.
* @see #cleanup() where the data retuned will be cleaned up
*/
T loadData(Priority priority) throws Exception;
/**
* Cleanup or recycle any resources used by this data fetcher. This method will be called in a finally block
* after the data returned by {@link #loadData(Priority)} has been decoded by the
* {@link com.bumptech.glide.load.ResourceDecoder}.
*
* <p>
* Note - this method will be run on a background thread so blocking I/O is safe.
* </p>
*
*/
void cleanup();
/**
* Returns a string uniquely identifying the data that this fetcher will fetch including the specific size.
*
* <p>
* A hash of the bytes of the data that will be fetched is the ideal id but since that is in many cases
* impractical, urls, file paths, and uris are normally sufficient.
* </p>
*
* <p>
* Note - this method will be run on the main thread so it should not perform blocking operations and should
* finish quickly.
* </p>
*/
String getId();
/**
* A method that will be called when a load is no longer relevant and has been cancelled. This method does not need
* to guarantee that any in process loads do not finish. It also may be called before a load starts or after it
* finishes.
*
* <p>
* The best way to use this method is to cancel any loads that have not yet started, but allow those that are in
* process to finish since its we typically will want to display the same resource in a different view in
* the near future.
* </p>
*
* <p>
* Note - this method will be run on the main thread so it should not perform blocking operations and should
* finish quickly.
* </p>
*/
void cancel();
}
它有4个需要重写的方法loadData(Priority priority)
,void cleanup()
,String getId()
,void cancel()
,通过看注释我们可以知道这几个方法是控制图片的获取流程的.这不就是我们需要的嘛,我们可以把获取图片url的过程放在这里.很好,我们只需要通过实现ModelLoader
接口来写一个自定义的Loader,再写一个自定义的DataFetcher
来控制图片下载过程,就能达到自己的目的了.
CustomGlideImageLoader
我模仿着写了一下代码
public class CustomGlideImageLoader implements ModelLoader<DoubanBookCoverImage, InputStream> {
private final ModelCache<DoubanBookCoverImage, DoubanBookCoverImage> modelCache;
public CustomGlideImageLoader(ModelCache<DoubanBookCoverImage, DoubanBookCoverImage> modelCache) {
this.modelCache = modelCache;
}
public CustomGlideImageLoader() {
this(null);
}
@Override
public DataFetcher<InputStream> getResourceFetcher(DoubanBookCoverImage model, int width, int height) {
DoubanBookCoverImage doubanBookCoverImage = (DoubanBookCoverImage) model;
if (modelCache != null) {
doubanBookCoverImage = modelCache.get((DoubanBookCoverImage) model, 0, 0);
if (doubanBookCoverImage == null) {
modelCache.put(model, 0, 0, model);
doubanBookCoverImage = model;
}
}
return new CustomGlideFetcher(doubanBookCoverImage);
}
public static class Factory implements ModelLoaderFactory<DoubanBookCoverImage, InputStream> {
//设置缓存
private final ModelCache<DoubanBookCoverImage, DoubanBookCoverImage> mModelCache = new ModelCache<>(500);
@Override
public ModelLoader<DoubanBookCoverImage, InputStream> build(Context context, GenericLoaderFactory factories) {
return new CustomGlideImageLoader(mModelCache);
}
@Override
public void teardown() {
}
}
}
可以看到getResourceFetcher
都是差不多的.里面的DoubanBookCoverImage
类型是我自定义的数据类型,里面主要储存着要搜索的ISBN和获取到的图片Url,对应着BaseGlideUrlLoader
里的GlideUrl
,如果你看了GlideUrl
的定义,会发现里面主要是储存Url,也不是很复杂.
我自定义的类里面还有一个Factory
类,这个和把你自定义的Loader加载入Glide有关,随后我会讲.
CustomGlideFetcher
看我自定义的DataFetcher
public class CustomGlideFetcher implements DataFetcher<InputStream> {
private final DoubanBookCoverImage mdoubanBookCoverImage;
private volatile boolean mIsCanceled;
private InputStream mInputStream;
private Subscriber urlSubscriber;
private Subscriber downSubscriber;
public CustomGlideFetcher(DoubanBookCoverImage doubanBookCoverImage) {
mdoubanBookCoverImage = doubanBookCoverImage;
urlSubscriber = new Subscriber<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(String s) {
mdoubanBookCoverImage.setUrl(s);
}
};
downSubscriber = new Subscriber<ResponseBody>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(ResponseBody responseBody) {
mInputStream = responseBody.byteStream();
}
};
}
@Override
public InputStream loadData(Priority priority) throws Exception {
String url = mdoubanBookCoverImage.getUrl();
if (url == null) {
if (mIsCanceled) {
return null;
}
/*获取url*/
DoubanApi.getDoubanSearch().doubanSearch(mdoubanBookCoverImage.getISBN().substring(9).replace("-", "")).map(new Func1<String, String>() {
@Override
public String call(String s) {
return DoubanParse.getbookcover(s);
}
}).subscribe(urlSubscriber);
if (mdoubanBookCoverImage.getUrl() == null) {
return null;
}
}
if (mIsCanceled) {
return null;
}
/*下载图片*/
DoubanApi.getDoubanDownImage().downImage(mdoubanBookCoverImage.getUrl()).subscribe(downSubscriber);
return mInputStream;
}
/*关闭数据流*/
@Override
public void cleanup() {
if (mInputStream != null) {
try {
mInputStream.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
mInputStream = null;
}
}
}
@Override
public String getId() {
return mdoubanBookCoverImage.getId();
}
/*取消请求*/
@Override
public void cancel() {
mIsCanceled = true;
if (urlSubscriber.isUnsubscribed()) {
urlSubscriber.unsubscribe();
}
if (downSubscriber.isUnsubscribed()) {
downSubscriber.unsubscribe();
}
}
}
写这个的时候我参考了Glide自带的HttpUrlFetcher
的写法.数据获取的部分我使用的是Retrofit+Rxjava,可以很方便的控制数据加载与取消.如果你对这俩不了解,用其他的网络加载库也是可以的.
把自定义的CustomGlideImageLoader加载入Glide
我们要想办法在Glide中使用我们自定义的Loader,有两种方法
- 每次在使用Glide时使用.using(new MyUrlLoader())方法.
Glide.with(yourFragment) .using(new MyUrlLoader()) .load(yourModel) .into(yourView);
2.在mainfests中注册一下
你需要实现一个GlideModule,在里面就需要用到我们刚刚讲到的Factory
类了
public class CustomGllideMoudle implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
}
@Override
public void registerComponents(Context context, Glide glide) {
glide.register(DoubanBookCoverImage.class, InputStream.class, new CustomGlideImageLoader.Factory());
}
}
然后在mainfests中注册
<meta-data
android:name="com.swuos.ALLFragment.library.libsearchs.search.model.GlideV.CustomGllideMoudle"
android:value="GlideModule"/>
用的时候你需要用from方法来引入你自定义的数据类型,在load方法里使用你的自定义类型数据就可以了.
Glide.with(context).from(DoubanBookCoverImage.class).load(new DoubanBookCoverImage()).into(your_view);
我没有贴我自定义的数据类型DoubanBookCoverImage
的代码,不过你可以来看完整的.
完整代码在这里
github.com/swuos/opens…
第一次写这样的文章,有没说明白的地方可以email我.
thx
上一篇:使用Intellij IDEA 编写一个Android Studio插件
下一篇:Retrofit — Token Authentication on Androidcore