美文网首页Android Android开发精选Android 学习资源
网络图片加载的封装【从零开始搭建android框架系列(4)】

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

作者: 程序员Anthony | 来源:发表于2016-05-08 23:43 被阅读14015次

    本篇文章项目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

    相关文章

      网友评论

      • 疯狂的小菜鸡:哈喽,有看到加下QQ:360004431,谢谢
      • 繁复至极返璞归简:感觉这么封装少了点灵活性,每次必须先new一个ImageLoader,再通过ImageLoaderUtil加载,而且从缓存加载,从网络加载,从文件路径加载,从Uri加载,需要写很多不通 的load方法,在加载时做类似是否wifi下加载的判断(判断来源是文件路径还是Uri还是文件File),是否能优化下,比如结合泛型?
      • 沙滩上玩耍的孩纸:楼主,不知为什么到这个项目的时候,说我缺少文件
      • 08dc70a40fb1:感谢楼主
      • b7fb6dc23feb:lz 问下 图片封装到 ImageLoader 但是遇到与生命周期相关的 没法封装 比如 Picasso.with(this).cancelRequest 这种怎么破
      • 阿堃堃堃堃:LZ的文章写得很好,感觉获益匪浅!但我有个小小的疑问:在ImageLoader类中引入了一个内部类Builder,觉得这个Builder的存在是否比较多余?为什么不可以new ImageLoader().setURL().setImgView()这样做呢?感觉Builder的引入并没有对扩展性、可读性方面产生正面的作用啊?LZ能否给进一步解释解释:joy:
      • 76fb133613ae::smile: 很不错啊 谢谢LZ分享,帮助很大0.0
      • 逐风的少年:写的真好,以前没考虑过的问题
      • 4f2d5bd3136b:Fresco无法封装吧,布局中引用到
      • b3f565d2994d: :+1: 看完觉得收获良多,楼主加油,期待你之后的文章
      • 7ba019bcdae5:看完,特意注册个账号来给楼主点赞
      • 人失格:如果是Fresco 这种控件级的ImageLoader 其实并不能封装
      • 风语安然:谢谢!
      • Adam289:特别好!
      • 我有梦我不怕痛:第一次在简书回复,以前只看不回,今天确实是被打动,写的非常的棒,文章非常的通俗易懂,实用价值很大,楼主加油,期待更多更好的作品呈现给大家!
      • 234f933911dc:楼主!你好~你这个demo在哪里可以获得!想下载下来学习总结一下
      • 苌蓊芪:>>随着你们业务的需求,发现 Picasso 这个图片加载库已经满足不了你们了,你们需要换成 Fresco ,如果你没有封装一层的话,想要替换这个库那你要崩溃了,要把所有调用 Picasso 的地方都改一遍,而如果你中间封装了一层,那真的非常轻松,三天两头的换一次也没问题。

        对这段有一点疑惑,Fresco和其它图片库不同在它使用了自定义的DraweeView(希望我没拼写错),glide等使用的是ImageView,那么如何封装才能实现快捷的修改?希望能提供一种思路,谢谢!
        程序员Anthony:@苌蓊芪 这边目前也还没有使用Fresco,一直使用的是Glide和Picasso。 所以抱歉暂时无法给你提供思路。
      • 4b5a49c9c63c:太赞了!!!!!!楼主这篇文让我学到了不少东西,讲得很详细易懂,perfect!我想问下:ImageLoader imageLoader =new ImageLoader.Builder().url("img url").imgView(mImgView).build();
        ImageLoaderUtil.getInstance().loadImage(context,imageLoader); 这两句,为什么不把context给放到Builder里面呢?这样不是就可以少了一句代码了吗?
        程序员Anthony:@长跑终点 没问题 ,其实大家都封装了 ,只有很多人不愿意分享出来,哈哈
        4b5a49c9c63c:@CameloeAnthony 哦哦,我还以为是有什么特殊的作用呢。我是准备自己封装一个,不过是抄你的代码一份,哈哈。你这个是我见过最好的图片框架封装 :smile:
        程序员Anthony:@长跑终点 按理说都是可行的哈,这是我的方法,你也可以尝试自己封装一个哟。
      • SeanZ9:感谢楼主
      • wing_zhong:这样封装原来glide的一些特性就没用了,因为现在只封闭了url方式的,其它的比如glide.load(File file)等重载没有用
        姚瑶大坏蛋:@CameloeAnthony 代码里没有GlideImageLoaderProvider,是指GlideImageLoaderStrategy?
        wing_zhong:@CameloeAnthony 嗯嗯,感谢回复,不过加起来也麻烦,不好封装啊。
        程序员Anthony:@wing_zhong 这里是对网络图片加载的封装哈。但是如果要使用load(File file)的话在GlideImageLoaderProvider里面添加相应的方法即可。
      • 67d75c11e8a3:项目总是下载失败= = 难过,~
        程序员Anthony:@meomeoChars_ 哈哈 。那你再试一试 。我这边也可以clone的哦
        67d75c11e8a3:@CameloeAnthony 想看你网络封装是怎么做的呀。最近项目重构 所以要搭框架。整好用到
        程序员Anthony:@meomeoChars_ github偶尔是这样的吧。 最近项目在做大修改 。可以略微看一下就行了 。后面会有很多需要修改的地方。
      • 00a770ee428a:你好, 可以把你封装的基类 还有封装的各种小控件给我看下吗 谢谢啦
        程序员Anthony:@美女能借个火么 本篇文章项目github地址:MVPCommon https://github.com/CameloeAnthony/MVPCommon 有很多工作中在用的。最近在不断做修改,
      • Tang1024:写的真好,很容易上手
      • 娃娃要从孩子抓起:谢谢LZ分享,文章写的很不错。希望有更多好的文章。
        程序员Anthony:@娃娃要从孩子抓起 :sunglasses: 没问题 。也在不断探索怎么把文章写得清晰易懂 。

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

      本文链接:https://www.haomeiwen.com/subject/asejrttx.html