Android图片加载框架浅析

图片加载是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的父类也需要修改一下,这种修改就可能会给我们带来一些未知的风险。

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

圆角的实现

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功能列表

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

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);
  • DiskCacheStrategy.ALL 缓存源资源和转换后的资源
  • DiskCacheStrategy.NONE 不作任何磁盘缓存
  • DiskCacheStrategy.SOURCE 缓存源资源
  • DiskCacheStrategy.RESULT 缓存转换后的资源

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

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的使用小结

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

思考:哪一天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更加灵活小巧,不同项目可以选择不同的框架就可以了。另外,更重要的一点就是要未雨绸缪,万一哪一天需要更改了,想想那可怕的工作量,所以还是早早的上代理的好呢~

本文来自网易实践者社区,经作者钟金宝发布。