网络图片加载的封装【从零开始搭建android框架系列(4)】

本篇文章项目github地址:MVPCommon

项目效果

项目正在改版,即时通讯功能暂时删除了。

1 有哪些常用的图片加载库?


当下使用的主要有Piccaso、Fresco、Android-Universal-Image-Loader、Glide、Volley这五个图片加载框架。
关于这些图片加载框架的对比,网上可以找到很多文章。这里不做过多赘述。具体请参考5中的参考链接,肯定会对你有帮助。


2 为什么要封装?

这个段落的答案,摘抄自Stormzhang的文章 如何正确使用开源项目?

计算机史上有个万能的解决方案就是,如果原有层面解决不了问题,那么就请再加一层!

对于开源项目,我们知道有些库设计的确实很棒,使用者调用起来非常方便,一行代码直接搞定,拿图片加载库 Picasso 举个例子:

Picasso.with(context).load(imageUrl).into(imageView);

使用起来是不是特简单?你也许问我,都封装的这么好了还用得着再封装一层么?那你错了,哪怕他已经很完美了,我都会这么做:

public class ImageLoader {
    public static void with(Context context, String imageUrl, ImageView imageView) {
        Picasso.with(context).load(imageUrl).into(imageView); 
    }
}

这样我所有项目调用的方式直接就是 ImageLoader.with() ,这样做的好处是:

入口统一,所有图片加载都在这一个地方管理,一目了然,即使有什么改动我也只需要改这一个类就可以了。

随着你们业务的需求,发现 Picasso 这个图片加载库已经满足不了你们了,你们需要换成 Fresco ,如果你没有封装一层的话,想要替换这个库那你要崩溃了,要把所有调用 Picasso 的地方都改一遍,而如果你中间封装了一层,那真的非常轻松,三天两头的换一次也没问题。

这就是所谓的外部表现一致,内部灵活处理原则。


3 有哪些需求?

这里提供我平常开发用到的两个需求:

3.1 图片封装,提供统一入口。封装成熟的图片框架,也就解决了这些问题:(内存缓存,磁盘缓存,网络加载的结合,利用采样率对图片进行一定的压缩,高效加载bitmap,图片的加载策略,缓存策略(LRU),图片错位 ,优化列表的卡顿)

3.2 提供wifi下加载图片开关,非wifi下显示占位图片。


4 怎么实现图片封装?

4.1 整体目录

在我的mvp搭建的项目中,imageloader放在和activity,fragment同级的widget下面。当然后续也会不断的添加widget(小控件),比如这里的loading(加载),netstatus(网络状态监听),progress(Material 进度条),swipeback(滑动返回)等。

整体目录结构

4.2 ImageUtil类

作为整个ImageLoader的公共访问入口,以后使用的方式,将会是

ImageLoader imageLoader =new ImageLoader.Builder().url("img url").imgView(mImgView).build();
ImageLoaderUtil.getInstance().loadImage(context,imageLoader);

这种形式。可以看到ImageUtil提供的是单例模式,进行了封装(后期引入Dagger2 之后直接会修改使用ImageLoaderUtil实例的方式)。全局应该只提供一个ImageLoader的实例,因为图片加载中又有线程池,缓存系统和网络请求等,很消耗资源,所以不可能让它构造多个实例。

package edu.com.base.ui.widget.imageloader;

import android.content.Context;

/**
 * Created by Anthony on 2016/3/3.
 * Class Note:
 * use this class to load image,single instance
 */
public class ImageLoaderUtil {

    public static final int PIC_LARGE = 0;
    public static final int PIC_MEDIUM = 1;
    public static final int PIC_SMALL = 2;

    public static final int LOAD_STRATEGY_NORMAL = 0;
    public static final int LOAD_STRATEGY_ONLY_WIFI = 1;

    private static ImageLoaderUtil mInstance;
    private BaseImageLoaderStrategy mStrategy;

    private ImageLoaderUtil(){
        mStrategy =new GlideImageLoaderStrategy();
    }

//single instance
    public static ImageLoaderUtil getInstance(){
        if(mInstance ==null){
            synchronized (ImageLoaderUtil.class){
                if(mInstance == null){
                    mInstance = new ImageLoaderUtil();
                    return mInstance;
                }
            }
        }
        return mInstance;
    }


    public void loadImage(Context context,ImageLoader img){
        mStrategy.loadImage(context,img);
    }

    public void setLoadImgStrategy(BaseImageLoaderStrategy strategy){
        mStrategy =strategy;
    }
}

4.3 BaseImageLoaderProvider类

可以看到我们ImageUtil中是采用这个类的loadImage方法去加载图片的。这里是一个接口。由具体的子类(GlideImageLoaderProvider)去实现loadImage方法。(这里我也添加了PicassoImageLoaderStrategy方法,但其中的loadImage方法是空的实现)。

package edu.com.base.ui.widget.imageloader;

import android.content.Context;

/**
 * Created by Anthony on 2016/3/3.
 * Class Note:
 * abstract class/interface defined to load image
 * (Strategy Pattern used here)
 */
public interface BaseImageLoaderStrategy {
   void loadImage(Context ctx, ImageLoader img);
}

4.4 GlideImageLoaderProvider类

是BaseImageLoaderProvider的实现类,完成具体的加载图片操作。这里面会有wifi下加载图片的判断。具体判断将放在util工具类中进行实现。这里也是利用图片加载库Glide进行实现。后期如果工程项目决定使用其他的图片加载框架,当然可以采用其他类继承BaseImageLoaderProvider。

package edu.com.mvplibrary.ui.widget.imageloader;

import android.content.Context;

import com.bumptech.glide.Glide;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.model.stream.StreamModelLoader;


import java.io.IOException;
import java.io.InputStream;

import edu.com.mvplibrary.AbsApplication;
import edu.com.mvplibrary.util.AppUtils;
import edu.com.mvplibrary.util.SettingUtils;

/**
 * Created by Anthony on 2016/3/3.
 * Class Note:
 * provide way to load image
 */
public class GlideImageLoaderProvider implements BaseImageLoaderProvider {
    @Override
    public void loadImage(Context ctx, ImageLoader img) {

        boolean flag= SettingUtils.getOnlyWifiLoadImg(ctx);
        //如果不是在wifi下加载图片,直接加载
        if(!flag){
            loadNormal(ctx,img);
            return;
        }

        int strategy =img.getStrategy();
        if(strategy == ImageLoaderUtil.LOAD_STRATEGY_ONLY_WIFI){
            int netType = AppUtils.getNetWorkType(AbsApplication.app());
            //如果是在wifi下才加载图片,并且当前网络是wifi,直接加载
            if(netType == AppUtils.NETWORKTYPE_WIFI) {
                loadNormal(ctx, img);
            } else {
                //如果是在wifi下才加载图片,并且当前网络不是wifi,加载缓存
                loadCache(ctx, img);
            }
        }else{
            //如果不是在wifi下才加载图片
            loadNormal(ctx,img);
        }

    }


    /**
     * load image with Glide
     */
    private void loadNormal(Context ctx, ImageLoader img) {
        Glide.with(ctx).load(img.getUrl()).placeholder(img.getPlaceHolder()).into(img.getImgView());
    }


    /**
     *load cache image with Glide
     */
    private void loadCache(Context ctx, ImageLoader img) {
        Glide.with(ctx).using(new StreamModelLoader<String>() {
            @Override
            public DataFetcher<InputStream> getResourceFetcher(final String model, int i, int i1) {
                return new DataFetcher<InputStream>() {
                    @Override
                    public InputStream loadData(Priority priority) throws Exception {
                        throw new IOException();
                    }

                    @Override
                    public void cleanup() {

                    }

                    @Override
                    public String getId() {
                        return model;
                    }

                    @Override
                    public void cancel() {

                    }
                };
            }
        }).load(img.getUrl()).placeholder(img.getPlaceHolder()).diskCacheStrategy(DiskCacheStrategy.ALL).into(img.getImgView());
    }
}

4.5 ImageLoader类

在ImageUtil的load方法中进行图片加载,第一个参数是Context,那么第二个参数呢?正是这里的ImageLoader,采用Builder建造者模式。Builder模式可以将一个复杂对象的构建和它的表示分离,使得同样的构建过程可以构建不同的对象。

why builder pattern?
因为在图片加载中,会处理到的数据必定有图片的url,必定有ImageView的实例,可能有加载策略(是否wifi下加载),可能有图片加载类型(大图,中图,小图),也会有图片加载没有成功时候的占位符。那么这么多数据操作,所以用到了Builder模式,一步一步的创建一个复杂对象的创建者模式,它允许用户在不知道内部构建细节的情况下,可以更精细的控制对象的构建流程。比如这里的ImageLoader。

package edu.com.base.ui.widget.imageloader;

import android.widget.ImageView;

import edu.com.mvplibrary.R;


/**
 * Created by Anthony on 2016/3/3.
 * Class Note:
 * encapsulation of ImageView,Build Pattern used
 */
public class ImageLoader {
    private int type;  //类型 (大图,中图,小图)
    private String url; //需要解析的url
    private int placeHolder; //当没有成功加载的时候显示的图片
    private ImageView imgView; //ImageView的实例
    private int wifiStrategy;//加载策略,是否在wifi下才加载

    private ImageLoader(Builder builder) {
        this.type = builder.type;
        this.url = builder.url;
        this.placeHolder = builder.placeHolder;
        this.imgView = builder.imgView;
        this.wifiStrategy = builder.wifiStrategy;
    }
    public int getType() {
        return type;
    }

    public String getUrl() {
        return url;
    }

    public int getPlaceHolder() {
        return placeHolder;
    }

    public ImageView getImgView() {
        return imgView;
    }

    public int getWifiStrategy() {
        return wifiStrategy;
    }

    public static class Builder {
        private int type;
        private String url;
        private int placeHolder;
        private ImageView imgView;
        private int wifiStrategy;

        public Builder() {
            this.type = ImageLoaderUtil.PIC_SMALL;
            this.url = "";
            this.placeHolder = R.drawable.default_pic_big;
            this.imgView = null;
            this.wifiStrategy = ImageLoaderUtil.LOAD_STRATEGY_NORMAL;
        }

        public Builder type(int type) {
            this.type = type;
            return this;
        }

        public Builder url(String url) {
            this.url = url;
            return this;
        }

        public Builder placeHolder(int placeHolder) {
            this.placeHolder = placeHolder;
            return this;
        }

        public Builder imgView(ImageView imgView) {
            this.imgView = imgView;
            return this;
        }

        public Builder strategy(int strategy) {
            this.wifiStrategy = strategy;
            return this;
        }

        public ImageLoader build() {
            return new ImageLoader(this);
        }

    }
}


4.6 策略模式的使用
上面的图片加载用到了策略模式。

策略模式是指定义了一系列的算法,并将每一个算法封装起来(比如上面的Picasso和Glide),而且他们还可以互相替换。策略模式让他的算法独立于使用它的客户而独立变化。

一起看看整个图片加载封装的类图。

这里真是利用同样是图片加载,我们可以将不同的加载方式抽象出来,提供一个统一的接口,不同的算法或者策略有不同的实现方式,这样我们的客户端,也就是我们利用ImageLoaderUtil类加载图片的时候,就可以实现不同的加载策略。我们也可以通过

public void setLoadImgStrategy(BaseImageLoaderStrategy strategy){    
mStrategy =strategy;
}

来传入不同的加载策略,实现了策略的动态替换。也就提高了后期的可扩展性和可维护性。


5 参考链接

Android 几个图片缓存原理、特性对比

Introduction to Glide, Image Loader Library for Android, recommended by Google

FaceBook推出的Android图片加载库-Fresco

StackOverflow-->Picasso v/s Imageloader v/s Fresco vs Glide

本篇文章项目github地址:MVPCommon

推荐阅读更多精彩内容