使用Glide来自定义加载数据过程

本文记录了我使用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
https://github.com/bumptech/glide/wiki/Downloading-custom-sizes-with-Glide
官方的的这个例子是用来自定义请求的图片大小的,不过我们可以看一下它是怎么做的,它继承了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,对应着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,有两种方法
1.每次在使用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的代码,不过你可以来看完整的.
完整代码在这里
https://github.com/swuos/openswu-android/tree/master/app/src/main/java/com/swuos/ALLFragment/library/libsearchs/search/model/GlideV

https://github.com/swuos/openswu-android/blob/master/app/src/main/java/com/swuos/ALLFragment/library/libsearchs/search/model/douabn/DoubanBookCoverImage.java

https://github.com/swuos/openswu-android/blob/master/app/src/main/java/com/swuos/ALLFragment/library/libsearchs/search/adapter/RecycleAdapterSearch.java

第一次写这样的文章,有没说明白的地方可以email我.
thx