转载请注明本文出自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的优秀库
一个基于Glide的transformation库,拥有裁剪,着色,模糊,滤镜等多种转换效果,赞的不行不行的~~
一个可以在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”.
简单来说,大神说了两个方案:
全完实现ModelLoader和DataFetcher。
我上面说的那个失败了的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大神,这次又帮了我。
还有就是,用好开源项目,有时候还真的要多看点源码,毕竟文档有时候写得不够完整。