加载网络图片但没URL?不要紧,通过ModelLoader,让Glide直接加载任何奇葩数据源

原文出处:http://www.licheedev.com/2015/09/19/custom-glide-modelloader/ 

什么?加载网路图片没有url?只给我文件id?

最近公司项目换了七牛来存储用户文件。
于是获取图片方面,服务器那边不再给我完整的url,只给我个fid(file id),
我要拿着这个fid去做个http请求,才能获取到个带token的url,只有这个带token的url才能正确获取到图片。

我现在用的图片加载框架是Glide,很赞,Google都用它。Glide基础用法参考->这里

那就是说,我现在要加载一个网络图片,要做两次http请求:
先用http请求获取url,再用Glide加载这个url(第二次请求);
由于Android不允许UI线程访问网络,所以我必须开一个线程来获取那个url,而Glide加载和处理图片时,也会开线程。
于是多线程异步问题来了,如果用在ListView等地方,那么十有八九出现图片加载错乱的情况(真出现了)。

233

当然,可以通过加一堆判断来规避这个问题,但总感觉不怎么爽。
如果可以修改Glide加载图片的过程,把第一次请求url的操作塞进去,
然后像加载普通url那样,直接用我们的fid加载图片,就爽了。

初战铩羽而归

于是果断翻翻Glide项目的wiki,看看能不能干些什么。
然后我翻到这页:

https://github.com/bumptech/glide/wiki/Downloading-custom-sizes-with-Glide 

public class MyUrlLoader extends BaseGlideUrlLoader<MyDataModel> {
    @Override
    protected String getUrl(MyDataModel model, int width, int height) {
        // Construct the url for the correct size here.
        return model.buildUrl(width, height);
    }
}

蛤蛤,貌似有戏,我在这个getUrl()回调里面做次请求,返回获取到的url不就行了么,剩下就交给Glide处理,赞。

当然,凡事没这么简单了。谁知道这getUrl()回调运行在UI线程中,而在Android 3.0以上,UI线程中不允许进行网络操作。
这方案没戏。

高人指点

好吧,这下好了,刚燃起的希望,一下子又熄灭了。
想起了刚开始用Glide的时候,发过issue,这次也发个issue好了。

这次@TWiStErRob大神依旧给力,很快就给了解决方案,虽然只有文字描述。

You have to write a model loader. Create a wrapper class for integer and maybe add in the token as well.
https://github.com/bumptech/glide/wiki/Downloading-custom-sizes-with-Glide
Your model loader can create a fetcher that makes one request for the token and one request for the image, and return the second’s stream.
Or you can make the token request separately inside BaseGlideUrlLoader.getUrl (see wiki), then return the URL and let Glide take care of the rest “as usual”.

简单来说,大神说了两个方案:

  1. 全完实现ModelLoader和DataFetcher。

  2. 我上面说的那个失败了的getUrl()请求url的方案。

其实第二种方案属于第一种的特殊情况,BaseGlideUrlLoader就是ModelLoader的子类,已经实现好从url拉数据的细节。

这样看的话,其实我一开始的方向就是对的,遗憾的是没钻进去研究更底端层次的,懒啊。

于是参考了Glide中,部分已经实现好的ModelLoader和DataFetcher的子类,自己试着写了下,真成功了。下面说说细节。

搞掂

模拟案例

这里模拟了个DEMO,放在github上:

https://github.com/licheedev/Custom-Glide-ModelLoader-Demo

当然,我不可能把公司的接口公布出去,于是用一种很坑爹的方式,去模拟用fid获取url的接口。
因为我这个案例的关键的地方是两次请求,
于是我放了个静态json在七牛上,

{
  "prefix": "http://7xlwmc.com1.z0.glb.clouddn.com/"
}

其实就是一个完整url的前缀,当然也没有token,每次加载一个fid的时候,都会"多此一举"地拉这个json回来解析,
然后把prefix拼上fid,就是一个可用的url:
比如这个http://7xlwmc.com1.z0.glb.clouddn.com/SAMPLE_IMG_008.jpg

这就完成了一次请求。

细节

下面代码的写法参考了Glide自带的HttpGlideUrlLoader 和 HttpUrlFetcher

ImageFidLoader

ModelLoader的作用只有一个——实现getResourceFetcher()方法,返回一个DataFetcher对象。
ModelLoaderFactory的用法可以看下面的配置引用页。

public class ImageFidLoader implements ModelLoader<ImageFid,InputStream> {
    private final ModelCache<ImageFid, ImageFid> mModelCache;
    public ImageFidLoader() {
        this(null);
    }
    public ImageFidLoader(ModelCache<ImageFid, ImageFid> modelCache) {
        mModelCache = modelCache;
    }
    @Override
    public DataFetcher<InputStream> getResourceFetcher(ImageFid model, int width, int height) {
        ImageFid imageFid = model;
        // 从缓存中取出ImageFid,ImgeFid已重写equals()和hashCode()方法
        // 缓存中ImgeFid对象的url,有可能还没被初始化
        if (mModelCache != null) {
            imageFid = mModelCache.get(model, 0, 0);
            if (imageFid == null) {
                mModelCache.put(model, 0, 0, model);
                imageFid = model;
            }
        }
        return new ImageFidFetcher(imageFid);
    }
    // ModelLoader工厂,在向Glide注册自定义ModelLoader时使用到
    public static class Factory implements ModelLoaderFactory<ImageFid, InputStream> {
        // 缓存
        private final ModelCache<ImageFid, ImageFid> mModelCache = new ModelCache<>(500);
        
        @Override
        public ModelLoader<ImageFid, InputStream> build(Context context,
            GenericLoaderFactory factories) {
            // 返回ImageFidLoader对象
            return new ImageFidLoader(mModelCache);
        }
        @Override
        public void teardown() {
        }
    }
    
}

ImageFidFetcher

DataFetcher的作用是从数据源(图片网络地址、本地路径、res资源id等)中获取到图片的流数据(InputStream),然后交给Glide做处理(缩放、本地缓存等)。

注意loadData()、cleanup()、getId()和cancel()四个回调方法的作用场景。
在loadData()中,这里进行了两次请求,一次拿url,一次拿图片数据。

public class ImageFidFetcher implements DataFetcher<InputStream> {
    // 检查是否取消任务的标识
    private volatile boolean mIsCanceled;
    
    private final ImageFid mImageFid;
    private Call mFetchUrlCall;
    private Call mFetchStreamCall;
    private InputStream mInputStream;
    public ImageFidFetcher(ImageFid imageFid) {
        mImageFid = imageFid;
    }
    /**
     * 在后台线程中调用,用于获取图片的数据流,给Glide处理
     * @param priority
     * @return
     * @throws Exception
     */
    @Override
    public InputStream loadData(Priority priority) throws Exception {
        // mImageFid有可能是来自缓存的,先从此对象获取url
        String url = mImageFid.getUrl();
        if (url == null) {
            if (mIsCanceled) {
                return null;
            }
            // 建立http请求,从网络上获取fid对应的的url
            url = fetchImageUrl();
            if (url == null) {
                return null;
            }
            // 存储获取到的url,以供缓存使用
            mImageFid.setUrl(url);
        }
        if (mIsCanceled) {
            return null;
        }
        // 再次建立http请求,获取url的流
        mInputStream = fetchStream(url);
        return mInputStream;
    }
    /**
     * 获取图片fid对应的url
     * @return
     */
    private String fetchImageUrl() {
        // 缓存请求,用来及时取消连接
        mFetchUrlCall = syncGet(Config.IMAGE_REQUEST_URL);
        try {
            String json = mFetchUrlCall.execute().body().string();
            JSONObject jsonObject = new JSONObject(json);
            return jsonObject.getString("prefix") + mImageFid.getFid();
        } catch (IOException e) {
            //e.printStackTrace();
        } catch (JSONException e) {
            //e.printStackTrace();
        }
        return null;
    }
    
    private InputStream fetchStream(String url) {
        // 缓存请求,用来及时取消连接
        mFetchStreamCall = syncGet(url);
        try {
            return mFetchStreamCall.execute().body().byteStream();
        } catch (IOException e) {
            //e.printStackTrace();
        }
        return null;
    }
    /**
     * 同步的http get请求
     * @param url 要访问的url
     * @return
     */
    private Call syncGet(String url) {
        Request request = new Request.Builder().url(url).get().build();
        return OkHttpManager.getClient().newCall(request);
    }
    /**
     * 在后台线程中调用,在Glide处理完{@link #loadData(Priority)}返回的数据后,进行清理和回收资源
     */
    @Override
    public void cleanup() {
        if (mInputStream != null) {
            try {
                mInputStream.close();
            } catch (IOException e) {
                //e.printStackTrace();
            } finally {
                mInputStream = null;
            }
        }
    }
    /**
     * 在UI线程中调用,返回用于区别数据的唯一id
     * @return
     */
    @Override
    public String getId() {
        return mImageFid.getFid();
    }
    /**
     * 在UI线程中调用,取消加载任务
     */
    @Override
    public void cancel() {
        mIsCanceled = true;
        // 取消获取url
        if (mFetchUrlCall != null) {
            mFetchUrlCall.cancel();
        }
        // 取消下载文件
        if (mFetchStreamCall != null) {
            mFetchStreamCall.cancel();
        }
    }
}

CustomGlideModule

配置Glide,注册ModelLoader。
注册完之后,我们就可以直接用Glide去直接加载对应类型的数据(这里的是ImageFid)来获取图片了。

具体参照:

https://github.com/bumptech/glide/wiki/Configuration 

https://github.com/bumptech/glide/releases 

https://github.com/bumptech/glide/wiki/Downloading-custom-sizes-with-Glide

public class CustomGlideModule implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        ViewTarget.setTagId(R.id.glide_tag_id); // 设置别的get/set tag id,以免占用View默认的
        builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888); // 设置图片质量为高质量
    }
    @Override
    public void registerComponents(Context context, Glide glide) {
        // 注册我们的ImageFidLoader
        glide.register(ImageFid.class, InputStream.class, new ImageFidLoader.Factory());
    }
}

ItemAdapter

这里展示了怎么直接使用fid来加载图片。跟普通的直接加载URL没什么两样。

如果没有在自定义GlideModule注册ModelLoader,
则每次加载图片,都需要调用using(new MyUrlLoader())注册ModelLoader,
即Glide.with(yourFragment).using(new MyUrlLoader()).load(yourModel).into(yourView);

public class ItemAdapter extends ArrayAdapter<ImageFid> {
    private final DrawableRequestBuilder<ImageFid> mGlideBuilder;
    public ItemAdapter(Context context, ImageFid\[\] images) {
        super(context, 0, images);
        mGlideBuilder = Glide.with(context)
            .from(ImageFid.class) // 设置数据源类型为我们的ImageFid
            .fitCenter().crossFade()
            .diskCacheStrategy(DiskCacheStrategy.ALL) // 设置本地缓存,缓存源文件和目标图像
            .placeholder(R.mipmap.ic_launcher);
    }
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        ...
        ImageFid fid = getItem(position);
        // 直接加载fid
        mGlideBuilder.load(fid).into(holder.ivImage);
        ...
        return convertView;
    }
    private static class ViewHolder {
    ...
    }
}

效果

网络加载

加载缓存

总结

首先再次感谢@TWiStErRob大神,这次又帮了我。
还有就是,用好开源项目,有时候还真的要多看点源码,毕竟文档有时候写得不够完整。