图片加载是Android开发中基础的功能,同时图片加载OOM也一直困扰着很多开发者, 因此为了降低开发周期和难度,我们经常会选用一些图片加载的开源库。 老牌的有ImageLoader,UIL,Volley,主流的有,Picasso, Glide,Fresco等等,选择一款好的图片加载裤就成了我们的首要问题。 接下来我们对比一下主流的两款 Glide,Fresco框架的优缺点
首先来简单的介绍一下这俩个图片加载框架。
Glide是Google的一位大佬的杰作, 基于Picasso,沿袭了Picasso的简洁风格,并且在此做了大量的优化与改进。Glide 默认的Bitmap格式是RGB_565,所以占用的内存比较小。在磁盘缓存时,Glide支持 缓存多种尺寸,这样Glide在加载速度上也具有一定的优势,可以根据View的大小去加载 图片不用加载全尺寸图片。
除此之外,Glide还支持加载Gif动态图,支持很多自定义样式,比如圆角等等。
Fresco是Facebook出品,他是新一 代的图片加载库。我们知道,Android的内存资源是十分有限的,加载图片的时候经常会 出现OOM的情况,虽然可以采用各种方法去优化内存的使用,但是却无法再本质上解决内存 资源稀缺的问题。OOM问题在低端机上问题尤为严重,而Facebook另辟蹊径,在更加底层 的Native层做处理,不去占用Java虚拟机层宝贵的内存资源。Fresco将图片当道一个叫 Ashmem的区域,这样由于图片不占用Java虚拟机的资源,所以大大减少了OOM。
虽然此库很强大,不用用起来很麻烦,包也很大,有2M大小,由于涉及Native层,想读源 码比较困难。
对比项 | Glide | Fresco |
---|---|---|
发布时间 | 2014.9 | 2015.5 |
是否支持webP | true | true |
大小 | 500K | 2M~3M |
是否支持Gif | true | true |
视频加载 | true | true |
开发者 |
可以看出,Fresco不仅可以实现一些基本的图片加载图片缓存,而且还提供一些变化功能,比如圆角等,这为我们的开发提供了极大的方便,提高开发效率。
当然还有一个问题,那就是实现的便捷性怎么样,是不是能够让我们很轻松的切换过来。对于Fresco来说,这一点稍微有一点麻烦,如果我们相拥Fresco加载图片,就不能使用原生的ImageView,而是必须使用Drawees这个View,所以说,我们可能需要全局替换一下ImageView,自定义的ImageView的父类也需要修改一下,这种修改就可能会给我们带来一些未知的风险。
撇下这个不谈,我们看一下具体功能的实现方式。
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的情况减少了不少。
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会帮我们处理好。
示例代码如下:
// 第一步 初始化配置信息
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。
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的圆角处理相比Fresco要稍微麻烦一丢丢,但是必须注意的是,Glide为我们提供了一个非常灵活的接口去处理bitmap,所以我们不仅仅局限于实现圆角,你还可以实现各种各样的样式。
要在Glide中实现圆角的功能,需要我们先自定义一个BitmapTransformation:
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为我们提供了设置接口:
Glide.with(this).load(imageUrl).diskCacheStrategy(DiskCacheStrategy.ALL).into(imageView);
如果我们不想使用缓存,每次都要从网络上面加载图片资源的话可以这样:
Glide.with(this).load(imageUrl).skipMemoryCache(true).into(imageView);
清理图片缓存:
// 清理缓存
Glide.get(this).clearDiskCache();
// 清理内存
Glide.get(this).clearMemory();
ps:需要注意的是,清理磁盘缓存一定要在子线程中调用,清理内存的时候可以在UI线程。
其他的缓存设置还可以通过自定义GlideModule来实现:
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来监听下载,不过他只有加载成功和加载失败的回调,没办法获取当前下载的进度。
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接口:
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方法的实现:
@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添加一个拦截器:
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得到图片下载进度:
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可以说是更专业的一款图片加载框架,它在内存管理方面有着不可比拟的优势,加载速度也很不错,特别是对Android 5.0的内存优化方面,大大减少了OOM的情况发生。但是另一个方面,它的包有2M之大,而且对于一般的App而言,使用Fresco有点大材小用的意思,Glide已经满足我们绝大部分需求了,除非你是Instagram之类的图片社交应用,否则的话Glide就已经足够使用了。Fresco更加专业,使用起来也更加有技巧,需要开发认真研究一下官方文档才能用的更好,而Glide则是为大部分应用而生,上手简单,满足大部分需求,足以。
UniversalImageLoader可以算是老牌的图片加载库,在GitHub上面有13000+
的star,火爆程度令人发指。但是。很遗憾的是,这个项目已经不在维护了。这就意味着以后任何bug都不会修复,新特性也不能开发了,所以继续使用就可能会遇到很多麻烦了。同样的,万一哪一天Glide也不维护了呢?或者说我们应用有新的需求,需求更换新的图片加载库,我们怎么办呢?总不能一处一处的替换吧。
所以呢,想了一下,我们可以自己动手编写一个代理,让代理帮我们下载图片,某一天,我们想换图片加载框架了,只需要换掉代理即可,我们实现的那些下载进度监听,下载失败监听等等代码都不要更改,仅仅是将代码修改一下即可,这样也方便我们调试代码,岂不快哉!
这样,我们程序调用就成了这样:
每一个图片加载可能都需要一个特殊的设置吗,比如圆角,下载回调等等,所以我们最后先同一一个下载的Config:
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作为参数:
public interface IImageLoad {
void load(ImageLoadConfig configuration);
}
紧接着,我们使用框架实现这个加载过程:
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和某一个具体的下载类而已,很方便,有一种予取予求的感觉~
最后,我们定义一个代码类,通过他连接具体的程序调用和框架下载程序:
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);
}
}
使用示例:
ImageLoadProxy.getInstance().load(new ImageLoadConfig()
.with(imgHead.getContext())
.imageView(imgHead)
.drawable(res));
这样,我们的图片加载就完全和具体的加载框架解耦了,改换其他的方案也是分分钟就能搞定的了。
Fresco和Glide各有各优势,Fresco更加专业,Glide更加灵活小巧,不同项目可以选择不同的框架就可以了。另外,更重要的一点就是要未雨绸缪,万一哪一天需要更改了,想想那可怕的工作量,所以还是早早的上代理的好呢~
本文来自网易实践者社区,经作者钟金宝发布。