Glide 一个专注于平滑滚动的图片加载和缓存库

转载请注明本文出自Cym的博客(http://blog.csdn.net/cym492224103),谢谢支持!


如有不了解Gide,请点击

在图片加载库烂大街的今天,选择一个适合自己使用的图片加载库已经成为了每一个Android开发者的必经之路。现在市面上知名的图片加载库有UIL,Picasso,Volley ImageLoader,Fresco以及我们今天的主角Glide。它们各有千秋,不能评定谁一定比谁好,只能说哪一个更适合你。

我的理解

下面我来谈一下个人对这些图片加载库的理解,如有错误,还望指教。

Universal Image Loader:一个强大的图片加载库,包含各种各样的配置,最老牌,使用也最广泛。

Picasso: Square出品,必属精品。和OkHttp搭配起来更配呦!

Volley ImageLoader:Google官方出品,可惜不能加载本地图片~

Fresco:Facebook出的,天生骄傲!不是一般的强大。

Glide:Google推荐的图片加载库,专注于流畅的滚动。

更多详情请看stackoverflow上这个问题

初试Glide

下面进入今天的主题,相信之前很多同学都看到过这篇介绍Glide的文章。文中从各个方面介绍和比较了Glide与Picasso,总体来说二者极为相似,有着近乎相同的API的使用风格。但Glide在缓存策略和加载GIF方面略胜一筹。最后作者也极力推荐了这个库。

而且据说在Google新出的Photos应用中,到处可见Glide的踪迹。看到这里,你是不是已经迫不及待的想试一试这个库呢?就在你下定决心尝试一记的时候,你又听说Yelp app(据说是美国的大众点评)也在使用这个吊炸天的库。你的心中激动万分,发四一定要使用这个库。说干就干,打开Android Studio,在builde.gradle里面添加上

1
compile  'com.github.bumptech.glide:glide:3.6.1'

然后全局搜索图片加载的地方,全部换成了下面的代码:

1
2
3
4
5
Glide. with (mContext)
         .load(url)
         .placeholder(R.drawable.loading_spinner)
         .crossFade()
         .into(myImageView);

在经过漫长的编译过程之后,再次打开APP,看到有着渐现效果的图片呈现在你的面前,你不禁叫道:“wocao,真TM帅!为什么我以前没有发现呢?”。

不过在你使用了几天之后你会发现一些问题:

为什么 有的图片第一次加载的时候只显示占位图,第二次才显示正常的图片呢?

为什么 我总会得到类似You cannot start a load for a destroyed activity这样的异常呢?

为什么 我不能给加载的图片setTag()呢?

为什么?为什么?这么NB的库竟然会有这么多的问题。没错,这就是我今天要讲的重点。怎么避免上面的问题发生。

一些解决方案

1.如果你刚好使用了这个圆形Imageview库或者其他的一些自定义的圆形Imageview,而你又刚好设置了占位的话,那么,你就会遇到第一个问题。如何解决呢?
方案一: 不设置占位;
方案二:使用Glide的Transformation API自定义圆形Bitmap的转换。这里是一个已有的例子
方案三:使用下面的代码加载图片:

1
2
3
4
5
6
7
8
9
Glide. with (mContext)
     .load(url) 
     .placeholder(R.drawable.loading_spinner)
     .into( new  SimpleTarget<Bitmap>(width, height) {
         @Override 
         public void onResourceReady(Bitmap bitmap, GlideAnimation anim) {
             // setImageBitmap(bitmap) on CircleImageView 
        
     };

2.至于第二个问题,请记住一句话:不要再非主线程里面使用Glide加载图片,如果真的使用了,请把context参数换成getApplicationContext。更多的细节请参考这个issue

3.为什么不能设置Tag,是因为你使用的姿势不对哦。如何为ImageView设置Tag呢?且听我细细道来。
方案一:使用setTag(int,object)方法设置tag,具体用法如下:
Java代码是酱紫的:

1
2
3
4
5
6
7
8
Glide. with (context).load(urls.get(i).getUrl()).fitCenter().into(imageViewHolder.image);
         imageViewHolder.image.setTag(R.id.image_tag, i);
         imageViewHolder.image.setOnClickListener( new  View.OnClickListener() {
             @Override
                 int position = (int) v.getTag(R.id.image_tag);
                 Toast.makeText(context, urls.get(position).getWho(), Toast.LENGTH_SHORT).show();
             }
         });

同时在values文件夹下新建ids.xml,添加

1
<item name= "image_tag"  type= "id" />

大功告成!

方案二:从Glide的3.6.0之后,新添加了全局设置的方法。具体方法如下:
先实现GlideMoudle接口,全局设置ViewTaget的tagId:

1
2
3
4
5
6
7
8
9
10
11
public class MyGlideMoudle implements GlideModule{
     @Override
     public void applyOptions(Context context, GlideBuilder builder) {
         ViewTarget.setTagId(R.id.glide_tag_id);
     }
 
     @Override
     public void registerComponents(Context context, Glide glide) {
 
     }
}

同样,也需要在ids.xml下添加id

1
<item name= "glide_tag_id"  type= "id" />

最后在AndroidManifest.xml文件里面添加

1
2
3
<meta-data
     android:name= "com.yourpackagename.MyGlideMoudle"
     android:value= "GlideModule"  />

又可以愉快的玩耍了,嘻嘻`(∩_∩)′。

方案三:写一个继承自ImageViewTaget的类,复写它的get/setRequest方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Glide. with (context).load(urls.get(i).getUrl()).fitCenter().into( new  ImageViewTarget<GlideDrawable>(imageViewHolder.image) {
             @Override
             protected void setResource(GlideDrawable resource) {
                 imageViewHolder.image.setImageDrawable(resource);
             }
 
             @Override
             public void setRequest(Request request) {
                 imageViewHolder.image.setTag(i);
                 imageViewHolder.image.setTag(R.id.glide_tag_id,request);
             }
 
             @Override
             public Request getRequest() {
                 return  (Request) imageViewHolder.image.getTag(R.id.glide_tag_id);
             }
         });
 
         imageViewHolder.image.setOnClickListener( new  View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 int position = (int) v.getTag();
                 Toast.makeText(context, urls.get(position).getWho(), Toast.LENGTH_SHORT).show();
             }
         });

当你想清除掉所有的图片加载请求时,这个方法可以帮助到你。

3.ListPreloader

如果你想让列表预加载的话,不妨试一下ListPreloader这个类。

一些基于Glide的优秀库

1.glide-transformations

一个基于Glide的transformation库,拥有裁剪,着色,模糊,滤镜等多种转换效果,赞的不行不行的~~

2.GlidePalette

一个可以在Glide加载时很方便使用Palette的库。

=========================深入学习===============================

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

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

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



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

初战铩羽而归

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

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

1
2
3
4
5
6
7
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在七牛上,

1
2
3
{
}

其实就是一个完整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的用法可以看下面的配置引用页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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,一次拿图片数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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大神,这次又帮了我。
还有就是,用好开源项目,有时候还真的要多看点源码,毕竟文档有时候写得不够完整。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值