图片框架的选择与使用

图片加载是Android开发中基础的功能,同时图片加载OOM也一直困扰着很多开发者,
因此为了降低开发周期和难度,我们经常会选用一些图片加载的开源库。
老牌的有ImageLoader,UIL,Volley,主流的有,Picasso,
Glide,Fresco等等,选择一款好的图片加载裤就成了我们的首要问题。
接下来我们对比一下主流的两款 Glide,Fresco框架的优缺点

简介

首先来简单的介绍一下这俩个图片加载框架。

Glide

Glide是Google的一位大佬的杰作,
基于Picasso,沿袭了Picasso的简洁风格,并且在此做了大量的优化与改进。Glide
默认的Bitmap格式是RGB_565,所以占用的内存比较小。在磁盘缓存时,Glide支持
缓存多种尺寸,这样Glide在加载速度上也具有一定的优势,可以根据View的大小去加载
图片不用加载全尺寸图片。

除此之外,Glide还支持加载Gif动态图,支持很多自定义样式,比如圆角等等。

Fresco

Fresco是Facebook出品,他是新一
代的图片加载库。我们知道,Android的内存资源是十分有限的,加载图片的时候经常会
出现OOM的情况,虽然可以采用各种方法去优化内存的使用,但是却无法再本质上解决内存
资源稀缺的问题。OOM问题在低端机上问题尤为严重,而Facebook另辟蹊径,在更加底层
的Native层做处理,不去占用Java虚拟机层宝贵的内存资源。Fresco将图片当道一个叫
Ashmem的区域,这样由于图片不占用Java虚拟机的资源,所以大大减少了OOM。

虽然此库很强大,不用用起来很麻烦,包也很大,有2M大小,由于涉及Native层,想读源
码比较困难。

Glide与Fresco详细对比

基本信息对比

对比项 Glide Fresco
发布时间 2014.9 2015.5
是否支持webP true true
大小 500K 2M~3M
是否支持Gif true true
视频加载 true true
开发者 Google Facebook

功能对比

Fresco功能列表
  • 加载进度提示
  • 圆角图片
  • 渐进式加载
  • 显示动图GIF
  • 多图请求
  • 监听下载
  • 缩放 旋转

可以看出,Fresco不仅可以实现一些基本的图片加载图片缓存,而且还提供一些变化功能,比如圆角等,这为我们的开发提供了极大的方便,提高开发效率。

当然还有一个问题,那就是实现的便捷性怎么样,是不是能够让我们很轻松的切换过来。对于Fresco来说,这一点稍微有一点麻烦,如果我们相拥Fresco加载图片,就不能使用原生的ImageView,而是必须使用Drawees这个View,所以说,我们可能需要全局替换一下ImageView,自定义的ImageView的父类也需要修改一下,这种修改就可能会给我们带来一些未知的风险。

撇下这个不谈,我们看一下具体功能的实现方式。

圆角的实现
1
2
3
4
5
6
7
8
public void setRoundImageSrc(SimpleDraweeView draweeView, String src, float radius){
RoundingParams roundingParams = RoundingParams.fromCornersRadius(radius);
draweeView.setHierarchy(
new GenericDraweeHierarchyBuilder(draweeView.getResources())
.setRoundingParams(roundingParams)
.build());
draweeView.setImageURI(Uri.parse(src));

通过上面代码就可以实现圆角的功能了,代码还是很简单的,简单的添加一些参数即可。

缓存处理

Fresco的缓存也是一大亮点,它采用了三级缓存的方式,分别是Bitmap缓存,未解码图片缓存,图片文件缓存。

注意:在Android 5.0以下,bitmap缓存位于ashmem区域,而在Android 5.0以上,bitmap和其他框架一样,缓存在Java Heap之上。因为Android 5.0系统之上,内存管理有了很大改进,OOM的情况减少了不少。

1
2
3
4
5
6
7
8
9
10
11
12
public void initFresco(Context context, String diskCacheUniqueName){
DiskCacheConfig diskCacheConfig = DiskCacheConfig.newBuilder(context)
.setMaxCacheSize(DISK_CACHE_SIZE_HIGH)
.setMaxCacheSizeOnLowDiskSpace(DISK_CACHE_SIZE_LOW)
.setMaxCacheSizeOnVeryLowDiskSpace(DISK_CACHE_SIZE_VERY_LOW)
.build();

ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
.setMainDiskCacheConfig(diskCacheConfig)
.build();
Fresco.initialize(context, config);
}

通过上面代码,可以根据应用设置不同的缓存策略。

图片渐进式显示

渐进式图片格式先呈现大致的图片轮廓,然后随着图片的下载的继续,呈现捉奸清晰的图片,这对于移动设备。尤其是慢网络下的体验是极大的提升。
Android本省的图片库是不支持此格式的,但是Fresco支持。使用的时候只要制定一个URI即可,剩下的部分Fresco会帮我们处理好。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 第一步 初始化配置信息
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
.setProgressiveJpegConfig(new SimpleProgressiveJpegConfig())
.build();
Fresco.initialize(this,config);

// 第二步 请求图片
private void requestImage(){
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(img_url))
.setAutoRotateEnabled(true)
.build();

PipelineDraweeController controller = (PipelineDraweeController) Fresco.newDraweeControllerBuilder()
.setImageRequest(request)
.build();

myimageview.setController(controller);
}
监听下载事件

我们常常需要在图片加载完成后执行某些动作,比如使个别View可见,或者显示一些文字。或者是在下载图片失败后做一些事情,比如向用户显示一条失败信息,或者其他一下提示。图片加载都是在后台进程异步加载的,在Fresco中,我们需要使用DraweeController来监听下载事件。

使用方法:

简单的定义一个ControllerListener即可,官方推荐我们继承BaseControllerListener。

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
ControllerListener controllerListener = new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(
String id,
@Nullable ImageInfo imageInfo,
@Nullable Animatable anim) {
if (imageInfo == null) {
return;
}
QualityInfo qualityInfo = imageInfo.getQualityInfo();
FLog.d("Final image received! " +
"Size %d x %d",
"Quality level %d, good enough: %s, full quality: %s",
imageInfo.getWidth(),
imageInfo.getHeight(),
qualityInfo.getQuality(),
qualityInfo.isOfGoodEnoughQuality(),
qualityInfo.isOfFullQuality());
}

@Override
public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) {
FLog.d("Intermediate image received");
}

@Override
public void onFailure(String id, Throwable throwable) {
FLog.e(getClass(), throwable, "Error loading %s", id)
}
};

Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setControllerListener(controllerListener)
.setUri(uri)
// other setters
.build();
mSimpleDraweeView.setController(controller);

我们发现,我们不仅可以通过监听获取到我们下载的进度,还可以看到尺寸信息等,这一点对我们也是很有帮助的。

Glide功能列表
  • 图片样式自定义,不仅仅支持圆角图片
  • 显示动图GIF
  • 加载视频(不能直接加载网络资源)
  • 缩放 旋转
  • 支持加载的生命周期绑定
  • 支持加载各种资源,如字节数组,uri资源,resource资源等等
图片变换

Glide的圆角处理相比Fresco要稍微麻烦一丢丢,但是必须注意的是,Glide为我们提供了一个非常灵活的接口去处理bitmap,所以我们不仅仅局限于实现圆角,你还可以实现各种各样的样式。

要在Glide中实现圆角的功能,需要我们先自定义一个BitmapTransformation:

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
class RoundTransformation extends BitmapTransformation{

        public RoundTransformation(Context context) {

               super(context);

           }

         @Override

         protected Bitmap transform(BitmapPool pool, Bitmap toTransform,

                int outWidth, int outHeight) {

               //根据需要,进行Bitmap转换

               Bitmap roteBmp = BitmapUtils.getRoundCornerBitmap(toTransform, 360);

                if (roteBmp != toTransform) {

                      toTransform.recycle();

                 }

                 return roteBmp;

           }

           @Override

           public String getId() {

                  return "glide";

           }

}

在transform这个我们中,bitmap直接交给我们处理,所以这里就有很多很多的可能性了,比如灰度,黑白图都是可以实现的,这个实现方案必须点赞打call。

缓存

Glide为我们提供了很完善的缓存管理方法。前面我们提到过,Glide可以根据View大小自动决定加载图片的大小。比如一张10801920大小的图片,我们仅仅把他显示在了108192大小的View上面,那么Glide会很只能的将图片尺寸变小成和View一样大小的尺度,减少内存的使用。显示在View上面的图片同样是108192的,比原始尺寸整整小了100倍。对于缓存来说,Glide会将这个108192大小的图片缓存到磁盘中,要是我们下一次将他显示在了216*364大小的View上面的话,那么就必须重新从网络上获取一张图了。当然Glide为我们提供了设置接口:

1
Glide.with(this).load(imageUrl).diskCacheStrategy(DiskCacheStrategy.ALL).into(imageView);
  • DiskCacheStrategy.ALL 缓存源资源和转换后的资源
  • DiskCacheStrategy.NONE 不作任何磁盘缓存
  • DiskCacheStrategy.SOURCE 缓存源资源
  • DiskCacheStrategy.RESULT 缓存转换后的资源

如果我们不想使用缓存,每次都要从网络上面加载图片资源的话可以这样:

1
Glide.with(this).load(imageUrl).skipMemoryCache(true).into(imageView);

清理图片缓存:

1
2
3
4
// 清理缓存
Glide.get(this).clearDiskCache();
// 清理内存
Glide.get(this).clearMemory();

ps:需要注意的是,清理磁盘缓存一定要在子线程中调用,清理内存的时候可以在UI线程。

其他的缓存设置还可以通过自定义GlideModule来实现:

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
public class MyGlideModule implements GlideModule {

      @Override

      public void applyOptions(Context context, GlideBuilder builder) {

               // Apply options to the builder here.

        }

      @Override

      public void registerComponents(Context context, Glide glide) {

                // register ModelLoaders here.

        }

}

// 要注意在AndroidManifest.xml文件中注册一下
<meta-data

android:name="com.bodhixu.glide.CustomGlideModule"

android:value="GlideModule"/>

通过builder,我们可以设置内存缓存的大小等内容。可以见得,Glide的定制化做的很棒,提供了各种自定义功能。

监听下载

Glide提供了RequestListener<T, T>来监听下载,不过他只有加载成功和加载失败的回调,没办法获取当前下载的进度。

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
public abstract class LoadCallback {

private RequestListener<String, GlideBitmapDrawable> mListener;

public LoadCallback() {
mListener = new RequestListener<String, GlideBitmapDrawable>() {
@Override
public boolean onException(Exception e, String model, Target<GlideBitmapDrawable> target, boolean isFirstResource) {
onLoadException();
return false;
}

@Override
public boolean onResourceReady(GlideBitmapDrawable resource, String model, Target<GlideBitmapDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
onLoadSuccess();
return false;
}
};
}

public RequestListener<String, GlideBitmapDrawable> getListener() {
return mListener;
}

public abstract void onLoadSuccess();
public abstract void onLoadException();

}

不过,还是有其他方法来实现这一功能的,Glide的图片下载是基于OKHTTP实现的,所以我们可以通过拦截器获取到当前的下下载进度。

我们可以创建一个ProgressModelLoader类,实现StreamModelLoader接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ProgressModelLoader implements StreamModelLoader<String> {   

private Handler handler;

public ProgressModelLoader(Handler handler) {
this.handler = handler;
}

@Override
public DataFetcher<InputStream> getResourceFetcher(String model, int width, int height) {
return new ProgressDataFetcher(model, handler);
}
}

重写getResourceFetcher方法,这个方法返回一个DataFetcher类,这个类是个数据提取类,是个接口,重写他的loadData方法来下载图片,我们老看下ProgressDataFetcher对loadData方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public InputStream loadData(Priority priority) throws Exception {
Request request = new Request.Builder().url(url).build();
OkHttpClient client = new OkHttpClient();
client.interceptors().add(new ProgressInterceptor(getProgressListener()));

try {
progressCall = client.newCall(request);
Response response = progressCall.execute();
if (isCancelled) {
return null;
}

if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
stream = response.body().byteStream();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return stream;
}

为OKHTTP添加一个拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ProgressInterceptor implements Interceptor {    

private ProgressListener progressListener;

public ProgressInterceptor(ProgressListener progressListener) {
this.progressListener = progressListener;
}

@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder().body(new ProgressResponseBody(originalResponse.body(), progressListener)).build();
}
}

重写intercept方法,创建一个ProgressResponseBody得到图片下载进度:

1
2
3
4
5
6
7
8
9
10
11
12
13
private Source source(Source source) {    
return new ForwardingSource(source) {
long totalBytesRead = 0;
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
if(progressListener != null)
progressListener.progress(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
return bytesRead;
}
};
}

把读到的bytesRead和responseBody.contentLength()传给回调方法
progressListener.progress来计算进度。这样就可以实现下载进度的监听了。

Glide与Fresco的使用小结

相比于Glide,Fresco可以说是更专业的一款图片加载框架,它在内存管理方面有着不可比拟的优势,加载速度也很不错,特别是对Android 5.0的内存优化方面,大大减少了OOM的情况发生。但是另一个方面,它的包有2M之大,而且对于一般的App而言,使用Fresco有点大材小用的意思,Glide已经满足我们绝大部分需求了,除非你是Instagram之类的图片社交应用,否则的话Glide就已经足够使用了。Fresco更加专业,使用起来也更加有技巧,需要开发认真研究一下官方文档才能用的更好,而Glide则是为大部分应用而生,上手简单,满足大部分需求,足以。

思考:哪一天Glide被淘汰了我们怎么办?

UniversalImageLoader可以算是老牌的图片加载库,在GitHub上面有13000+的star,火爆程度令人发指。但是。很遗憾的是,这个项目已经不在维护了。这就意味着以后任何bug都不会修复,新特性也不能开发了,所以继续使用就可能会遇到很多麻烦了。同样的,万一哪一天Glide也不维护了呢?或者说我们应用有新的需求,需求更换新的图片加载库,我们怎么办呢?总不能一处一处的替换吧。

所以呢,想了一下,我们可以自己动手编写一个代理,让代理帮我们下载图片,某一天,我们想换图片加载框架了,只需要换掉代理即可,我们实现的那些下载进度监听,下载失败监听等等代码都不要更改,仅仅是将代码修改一下即可,这样也方便我们调试代码,岂不快哉!

这样,我们程序调用就成了这样:

实现步骤

每一个图片加载可能都需要一个特殊的设置吗,比如圆角,下载回调等等,所以我们最后先同一一个下载的Config:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ImageLoadConfig {

private Context context = null;
private Fragment fragment = null;
private ImageView imageView = null;
private String url = null;
private int drawable = -1;
private int defaultImageResId = -1;
private int loadErrorImageResId = -1;
private int foregroundColor;
private boolean isCircle = false;
private boolean isGray = false;
private boolean isCenterCrop = false;
private boolean isGif = false;
private float borderWidth = 0;
private @ColorInt int borderColor;

private @FloatRange(from = 0.01f, to = 1.0f) float thumbnail = 0.0f;
private @IntRange(from = 1) int radius = -1;
private LoadCallback callback;
}

注意:这些属性都不要和具体的某一个加载框架耦合在一起,这些信息都是我们自定义的

接着我们先定一个图片加载的接口,他接受config作为参数:

1
2
3
public interface IImageLoad {
void load(ImageLoadConfig configuration);
}

紧接着,我们使用框架实现这个加载过程:

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
public class GlideImageLoad implements IImageLoad {
@Override
public void load(ImageLoadConfig configuration) {

RequestManager requestManager;
// 为了与生命周期联动
if (null != configuration.getFragment()) {
requestManager = Glide.with(configuration.getFragment());
} else if (null != configuration.getContext() && configuration.getContext() instanceof Activity) {
requestManager = Glide.with((Activity) configuration.getContext());
} else if (null != configuration.getContext()) {
requestManager = Glide.with(configuration.getContext());
} else {
return;
}

DrawableTypeRequest drawableTypeRequest;

if (null != configuration.getUrl()) {
drawableTypeRequest = requestManager.load(configuration.getUrl());
} else if (configuration.getDrawable() > 0) {
drawableTypeRequest = requestManager.load(configuration.getDrawable());
} else {
return;
}

if (configuration.getThumbnail() > 0f){
drawableTypeRequest.thumbnail(configuration.getThumbnail());
}

Transformation<Bitmap>[] transformations = getTransformations(configuration);
if (transformations != null && transformations.length > 0) {
drawableTypeRequest.bitmapTransform(transformations);
}

if (configuration.getDefaultImageResId() != -1) {
drawableTypeRequest.placeholder(configuration.getDefaultImageResId());
}

if (configuration.getLoadErrorImageResId() != -1) {
drawableTypeRequest.error(configuration.getLoadErrorImageResId());
}

if (configuration.getCallback() != null) {
drawableTypeRequest.listener(configuration.getCallback().getListener());
}

drawableTypeRequest.dontAnimate();

if (configuration.isGif()) {
drawableTypeRequest.asGif();
drawableTypeRequest.diskCacheStrategy(DiskCacheStrategy.SOURCE);
drawableTypeRequest.into(new GlideDrawableImageViewTarget(configuration.getImageView()));
} else {
if(configuration.getTarget() != null){
drawableTypeRequest.into(configuration.getTarget());
}else {
drawableTypeRequest.into(configuration.getImageView());
}
}

}

// 从ImageConfig中获取transform
@SuppressWarnings({"unchecked"})
private static Transformation<Bitmap>[] getTransformations(ImageLoadConfig imageLoadConfig) {
ArrayList<Transformation<Bitmap>> transformationArrayList = new ArrayList<>();

if (imageLoadConfig.isGray()) {
transformationArrayList.add(new GrayscaleTransformation(imageLoadConfig.getContext()));
} else if (imageLoadConfig.getForegroundColor() > 0) {
// transformationArrayList.add(new GrayscaleTransformation(imageLoadConfig.getContext()));

transformationArrayList.add(new AlphaColorMaskTransformation(
imageLoadConfig.getContext(), imageLoadConfig.getForegroundColor())
);
}

if (imageLoadConfig.isCircle()) {
transformationArrayList.add(new BorderCircleTransformation(imageLoadConfig.getContext(),
imageLoadConfig.getBorderWidth(), imageLoadConfig.getBorderColor()));
} else {
if (imageLoadConfig.getRadius() > 0) {
if (imageLoadConfig.isCenterCrop()) {
transformationArrayList.add(new CenterCrop(imageLoadConfig.getContext()));
transformationArrayList.add(new RoundedCornersTransformation(imageLoadConfig.getContext(),
imageLoadConfig.getRadius(), 0, imageLoadConfig.getCornerType()));
} else {
transformationArrayList.add(new RoundedCornersTransformation(imageLoadConfig.getContext(),
imageLoadConfig.getRadius(), 0, imageLoadConfig.getCornerType()));
}
}
}

Transformation<Bitmap>[] transformationArray = new Transformation[transformationArrayList.size()];
transformationArrayList.toArray(transformationArray);
return transformationArray;
}
}

这个config里面的属性配置可以比较灵活,你们需要什么样的样式定制随时可以添加定制,而代码的改动仅仅是config和某一个具体的下载类而已,很方便,有一种予取予求的感觉~

最后,我们定义一个代码类,通过他连接具体的程序调用和框架下载程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ImageLoadProxy {

private IImageLoad mImageLoad;

private static class ImageLoadProxyFactory {
public static ImageLoadProxy Instance = new ImageLoadProxy();
}

public static ImageLoadProxy getInstance() {
return ImageLoadProxyFactory.Instance;
}

private ImageLoadProxy() {
mImageLoad = new GlideImageLoad();
}

public void load(ImageLoadConfig imageLoadCfg) {
mImageLoad.load(imageLoadCfg);
}
}

使用示例:

1
2
3
4
ImageLoadProxy.getInstance().load(new ImageLoadConfig()
.with(imgHead.getContext())
.imageView(imgHead)
.drawable(res));

这样,我们的图片加载就完全和具体的加载框架解耦了,改换其他的方案也是分分钟就能搞定的了。

总结

Fresco和Glide各有各优势,Fresco更加专业,Glide更加灵活小巧,不同项目可以选择不同的框架就可以了。另外,更重要的一点就是要未雨绸缪,万一哪一天需要更改了,想想那可怕的工作量,所以还是早早的上代理的好呢~