• 图片加载框架之图片加载框架选型(一)上篇


    阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
    本篇文章将通过Universal-Image-Loader解析来阐述图片加载框架选型:

    一、[Universal-Image-Loader解析基本介绍与使用]

    基本介绍

    相信大家平时做Android应用的时候,多少会接触到异步加载图片,或者加载大量图片的问题,而加载图片我们常常会遇到许多的问题,比如说图片的错乱,OOM等问题,对于新手来说,这些问题解决起来会比较吃力,所以就有很多的开源图片加载框架应运而生,比较著名的就是Universal-Image-Loader,相信很多朋友都听过或者使用过这个强大的图片加载框架,今天这篇文章就是对这个框架的基本介绍以及使用,主要是帮助那些没有使用过这个框架的朋友们。该项目存在于Github上面Android-Universal-Image-Loader,我们可以先看看这个开源库存在哪些特征

    • 多线程下载图片,图片可以来源于网络,文件系统,项目文件夹assets中以及drawable中等

    • 支持随意的配置ImageLoader,例如线程池,图片下载器,内存缓存策略,硬盘缓存策略,图片显示选项以及其他的一些配置

    • 支持图片的内存缓存,文件系统缓存或者SD卡缓存

    • 支持图片下载过程的监听

    • 根据控件(ImageView)的大小对Bitmap进行裁剪,减少Bitmap占用过多的内存

    • 较好的控制图片的加载过程,例如暂停图片加载,重新开始加载图片,一般使用在 ListView,GridView中,滑动过程中暂停加载图片,停止滑动的时候去加载图片

    • 提供在较慢的网络下对图片进行加载

    当然上面列举的特性可能不全,要想了解一些其他的特性只能通过我们的使用慢慢去发现了

    ImageLoaderConfiguration

    图片加载器ImageLoader的配置参数,使用Builder模式。
    常用的配置属性有

    //通过StorageUtils获取内置的内存目录/data/data/.../cache
    File cacheDir = StorageUtils.getCacheDirectory(context);  
    ImageLoaderConfiguration config = new ImageLoaderConfiguration  
                    .Builder(getApplicationContext())  
                    .memoryCacheExtraOptions(480, 800) //即保存的每个缓存文件的最大长宽  
                    .threadPoolSize(3) //线程池内加载的数量  
                    .threadPriority(Thread.NORM_PRIORITY - 2)  
                    //解释:当同一个Uri获取不同大小的图片,缓存到内存时,只缓存一个。默认会缓存多个不同的大小的相同图片  
                    .denyCacheImageMultipleSizesInMemory()  //拒绝缓存多个图片。
                    .memoryCache(new WeakMemoryCache()) //缓存策略你可以通过自己的内存缓存实现 ,这里用弱引用,缺点是太容易被回收了,不是很好!
                    .memoryCacheSize(2 * 1024 * 1024) //设置内存缓存的大小 
                    .diskCacheSize(50 * 1024 * 1024) //设置磁盘缓存大小 50M    
                    .diskCacheFileNameGenerator(new Md5FileNameGenerator()) //将保存的时候的URI名称用MD5 加密  
                    .tasksProcessingOrder(QueueProcessingType.LIFO) //设置图片下载和显示的工作队列排序  
                    .diskCacheFileCount(100) //缓存的文件数量  
                    .diskCache(new UnlimitedDiskCache(cacheDir)) //自定义缓存路径  
                    .defaultDisplayImageOptions(defaultOptions) //显示图片的参数,默认:DisplayImageOptions.createSimple()
                    .imageDownloader(new BaseImageDownloader(this, 5 * 1000, 30 * 1000)) // connectTimeout (5 s), readTimeout (30 s)超时时间  
                    .writeDebugLogs() //打开调试日志
                    .build();//开始构建  
    //配置使用
    ImageLoader.getInstance().init(configuration);  
    
    

    可以设置内存缓存,硬盘缓存的相关参数等。

    设置完相关的参数后就可进行图片加载显示

    图片加载

    ImageLader提供了几个图片加载的方法,主要是这几个displayImage(), loadImage(),loadImageSync(),loadImageSync()方法是同步的,android4.0有个特性,网络操作不能在主线程,所以loadImageSync()方法我们就不去使用

    loadimage()加载图片

    我们先使用ImageLoader的loadImage()方法来加载网络图片

    final ImageView mImageView = (ImageView) findViewById(R.id.image);  
            String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg";  
    
            ImageLoader.getInstance().loadImage(imageUrl, new ImageLoadingListener() {  
    
                @Override  
                public void onLoadingStarted(String imageUri, View view) {  
    
                }  
    
                @Override  
                public void onLoadingFailed(String imageUri, View view,  
                        FailReason failReason) {  
    
                }  
    
                @Override  
                public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {  
                    mImageView.setImageBitmap(loadedImage);  
                }  
    
                @Override  
                public void onLoadingCancelled(String imageUri, View view) {  
    
                }  
            });  
    
    

    传入图片的url和ImageLoaderListener, 在回调方法onLoadingComplete()中将loadedImage设置到ImageView上面就行了,如果你觉得传入ImageLoaderListener太复杂了,我们可以使用SimpleImageLoadingListener类,该类提供了ImageLoaderListener接口方法的空实现,使用的是缺省适配器模式

    final ImageView mImageView = (ImageView) findViewById(R.id.image);  
            String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg";  
    
            ImageLoader.getInstance().loadImage(imageUrl, new SimpleImageLoadingListener(){  
    
                @Override  
                public void onLoadingComplete(String imageUri, View view,  
                        Bitmap loadedImage) {  
                    super.onLoadingComplete(imageUri, view, loadedImage);  
                    mImageView.setImageBitmap(loadedImage);  
                }  
    
            });  
    
    

    如果我们要指定图片的大小该怎么办呢,这也好办,初始化一个ImageSize对象,指定图片的宽和高,代码如下

    final ImageView mImageView = (ImageView) findViewById(R.id.image);  
            String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg";  
    
            ImageSize mImageSize = new ImageSize(100, 100);  
    
            ImageLoader.getInstance().loadImage(imageUrl, mImageSize, new SimpleImageLoadingListener(){  
    
                @Override  
                public void onLoadingComplete(String imageUri, View view,  
                        Bitmap loadedImage) {  
                    super.onLoadingComplete(imageUri, view, loadedImage);  
                    mImageView.setImageBitmap(loadedImage);  
                }  
    
            });  
    
    

    上面只是很简单的使用ImageLoader来加载网络图片,在实际的开发中,我们并不会这么使用,那我们平常会怎么使用呢?我们会用到DisplayImageOptions,他可以配置一些图片显示的选项,比如图片在加载中ImageView显示的图片,是否需要使用内存缓存,是否需要使用文件缓存等等

    DisplayImageOptions

    可以配置一些图片显示的选项,比如图片在加载中ImageView显示的图片,是否需要使用内存缓存,是否需要使用文件缓存等等,可供我们选择的配置如下

    DisplayImageOptions options = new DisplayImageOptions.Builder()  
            .showImageOnLoading(R.drawable.ic_stub) // resource or drawable  
            .showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable  
            .showImageOnFail(R.drawable.ic_error) // resource or drawable  
            .resetViewBeforeLoading(false)  // default  
            .delayBeforeLoading(1000)  
            .cacheInMemory(false) // default  
            .cacheOnDisk(false) // default  
            .preProcessor(...)  
            .postProcessor(...)  
            .extraForDownloader(...)  
            .considerExifParams(false) // default  
            .imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default  
            .bitmapConfig(Bitmap.Config.ARGB_8888) // default  
            .decodingOptions(...)  
            .displayer(new SimpleBitmapDisplayer()) // default  
            .handler(new Handler()) // default  
            .build();  
    
    

    大家就可以根据实际情况去设置。

    displayImage()加载图片

    接下来我们就来看看网络图片加载的另一个方法displayImage(),代码如下

            String imageUrl = "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg";  
    
            //显示图片的配置  
            DisplayImageOptions options = new DisplayImageOptions.Builder()  
                    .showImageOnLoading(R.drawable.ic_stub)  
                    .showImageOnFail(R.drawable.ic_error)  
                    .cacheInMemory(true)  
                    .cacheOnDisk(true)  
                    .bitmapConfig(Bitmap.Config.RGB_565)  
                    .build();  
    
            ImageLoader.getInstance().displayImage(imageUrl, mImageView, options);  
    
    

    可以看到这里是直接传递了ImageView进行设置显示,并不需要监听后设置,这样更为简便,这也是displayImageloadImage的区别。

    加载其他来源的图片

    使用Universal-Image-Loader框架不仅可以加载网络图片,还可以加载sd卡中的图片,Content provider等,使用也很简单,只是将图片的url稍加的改变下就行了,下面是加载文件系统的图片

    我们只需要给每个图片来源的地方加上Scheme包裹起来(Content provider除外),然后当做图片的url传递到imageLoader中,Universal-Image-Loader框架会根据不同的Scheme获取到输入流

            //图片来源于文件
            String imagePath = "/mnt/sdcard/image.png";  
            String imageUrl = Scheme.FILE.wrap(imagePath);  
            //相当于file:/mnt/sdcard/image.png
    
            //图片来源于Content provider  
            String contentprividerUrl = "content://media/external/audio/albumart/13";  
    
            //图片来源于assets  
            String assetsUrl = Scheme.ASSETS.wrap("image.png");  
    
            //图片来源于  
            String drawableUrl = Scheme.DRAWABLE.wrap("R.drawable.image"); 
    
    

    获取到对应URL后就可以调用display/loadImage方法进行显示。

    GirdView,ListView加载图片

    相信大部分人都是使用GridView,ListView来显示大量的图片,而当我们快速滑动GridView,ListView,我们希望能停止图片的加载,而在GridView,ListView停止滑动的时候加载当前界面的图片,这个框架当然也提供这个功能,使用起来也很简单,它提供了PauseOnScrollListener这个类来控制ListView,GridView滑动过程中停止去加载图片,该类使用的是代理模式

    listView.setOnScrollListener(new PauseOnScrollListener(imageLoader, pauseOnScroll, pauseOnFling));  
    
    gridView.setOnScrollListener(new PauseOnScrollListener(imageLoader, pauseOnScroll, pauseOnFling));  
    
    

    第一个参数就是我们的图片加载对象ImageLoader,
    第二个是控制是否在滑动过程中暂停加载图片,如果需要暂停传true就行了,
    第三个参数控制猛的滑动界面的时候图片是否加载

    OutOfMemoryError

    虽然这个框架有很好的缓存机制,有效的避免了OOM的产生,一般的情况下产生OOM的概率比较小,但是并不能保证OutOfMemoryError永远不发生,这个框架对于OutOfMemoryError做了简单的catch,保证我们的程序遇到OOM而不被crash掉,但是如果我们使用该框架经常发生OOM,我们应该怎么去改善呢?

    • 减少线程池中线程的个数,在ImageLoaderConfiguration中的(.threadPoolSize)中配置,推荐配置1-5

    • 在DisplayImageOptions选项中配置bitmapConfig为Bitmap.Config.RGB_565,因为默认是ARGB_8888, 使用RGB_565会比使用ARGB_8888少消耗2倍的内存

    • 在ImageLoaderConfiguration中配置图片的内存缓存为memoryCache(new WeakMemoryCache()) 或者不使用内存缓存

    • 在DisplayImageOptions选项中设置.imageScaleType(ImageScaleType.IN_SAMPLE_INT)或者imageScaleType(ImageScaleType.EXACTLY)

    二、[Universal-Image-Loader解析内部缓存原理]

    对于我们所知道的缓存,常用的是内存缓存MemoryCache和硬盘缓存DiscCache。一个读取快容量小,一个读取慢容量大。

    对于各自使用哪种缓存,则可以在前面配置ImageLoaderConfiguration进行缓存设置,当然也可以自己自定义适合的缓存。

    ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)  
            .memoryCache(new WeakMemoryCache())  
            .build();  
    
    

    对于Universal-Image-Loader来说它的缓存结构也是分为内存缓存MemoryCache和硬盘缓存DiskCache

    一.MemoryCache内存缓存

    首先先看个结构图,理解UIL里面内存缓存的结构

     
    4356742-84fcfec060707012.png
    image

    由于空间有限就没画成标准的UML类图形式。
    对于基类MemoryCache它则是一个接口,里面定义了put,get图片的方法

    public interface MemoryCache {
        ...
        boolean put(String key, Bitmap value);
    
        Bitmap get(String key);
    
        Bitmap remove(String key);
    
        Collection<String> keys();
    
        void clear();
    }
    
    

    都是大家比较所熟悉的方法,而对于其他的类

    我们一个个看

    LruMemoryCache

    这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用。直接实现了MemoryCache方法

    public class LruMemoryCache implements MemoryCache {
    
        private final LinkedHashMap<String, Bitmap> map;
        //最大容量
        private final int maxSize;
        /** 目前缓存的容量大小 */
        private int size;
        public LruMemoryCache(int maxSize) {
            ...
            this.maxSize = maxSize;
            this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
        }
        @Override
        public final Bitmap get(String key) {
            ...
            synchronized (this) {
                return map.get(key);
            }
        }
        @Override
        public final boolean put(String key, Bitmap value) {
            ...
            synchronized (this) {
                size += sizeOf(key, value);
                Bitmap previous = map.put(key, value);
                if (previous != null) {
                    size -= sizeOf(key, previous);
                }
            }
    
            trimToSize(maxSize);
            return true;
        }
    
        /**
         * Lru算法,当容量超过最大缓存容量,则移除最久的条目
         */
        private void trimToSize(int maxSize) {
            while (true) {
                String key;
                Bitmap value;
                synchronized (this) {
                    if (size < 0 || (map.isEmpty() && size != 0)) {
                        throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                    }
    
                    if (size <= maxSize || map.isEmpty()) {
                        break;
                    }
    
                    Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
                    if (toEvict == null) {
                        break;
                    }
                    key = toEvict.getKey();
                    value = toEvict.getValue();
                    map.remove(key);
                    size -= sizeOf(key, value);
                }
            }
        }
    
        @Override
        public final Bitmap remove(String key) {
            ...
            synchronized (this) {
                Bitmap previous = map.remove(key);
                if (previous != null) {
                    size -= sizeOf(key, previous);
                }
                return previous;
            }
        }
        ...
        //返回图片的字节大小
        private int sizeOf(String key, Bitmap value) {
            return value.getRowBytes() * value.getHeight();
        }
        ...
    }
    
    

    LruMemoryCache的源码也比较简单,内部有个成员变量LinkedHashMap<String, Bitmap> map这里直接进行保存的话则是强引用的形式。
    主要看get,put方法。
    对于get方法来说,比较简单,直接根据指定的key返回对应的图片。
    而对于put方法来说,则需要考虑容量的问题。

    @Override
        public final boolean put(String key, Bitmap value) {
            ...
            synchronized (this) {
                size += sizeOf(key, value);
                Bitmap previous = map.put(key, value);
                if (previous != null) {
                    size -= sizeOf(key, previous);
                }
            }
    
            trimToSize(maxSize);
            return true;
        }
    
    

    put方法首先调用了sizeof方法,该方法则是返回指定Bitmap的字节大小,之后size +=,总缓存量增加,之后调用trimToSize该方法则是进行缓存容量判断的。

    private void trimToSize(int maxSize) {
            while (true) {
                String key;
                Bitmap value;
                synchronized (this) {
                    if (size < 0 || (map.isEmpty() && size != 0)) {
                        throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                    }
    
                    if (size <= maxSize || map.isEmpty()) {
                        break;
                    }
    
                    Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
                    if (toEvict == null) {
                        break;
                    }
                    key = toEvict.getKey();
                    value = toEvict.getValue();
                    map.remove(key);
                    size -= sizeOf(key, value);
                }
            }
        }
    
    

    如果加入后的size 缓存容量 <= maxSize 最大缓存容量,则直接break,不用进行判定处理。
    如果大于的话,则直接移除最久未使用的。

    大家肯定有疑问,它到底怎么判断最久未使用的?没看到相关代码呀?

    相信知道LinkedHashMap的话可能就知道。
    LinkedHashMap自身已经实现了顺序存储,默认情况下是按照元素的添加顺序存储,也可以启用按照访问顺序存储,即最近读取的数据放在最前面,最早读取的数据放在最后面,然后它还有一个判断是否删除最老数据的方法,默认是返回false,即不删除数据。大家常见也就是按顺序存储,很少忘了它还可以根据最近未使用的方法。

    //LinkedHashMap的一个构造函数,当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在最前,最早访问的放在后面
    public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
            super(initialCapacity, loadFactor);
            this.accessOrder = accessOrder;
    }
    
    //LinkedHashMap自带的判断是否删除最老的元素方法,默认返回false,即不删除老数据
    //我们要做的就是重写这个方法,当满足一定条件时删除老数据
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
            return false;
    }
    
    

    回看我们前面LinkedHashMap的创建

      this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
    
    

    再举个使用例子

     
    4356742-a3124e74d4d7095c.png
    image

    就比较明了了。

    BaseMemoryCache

    BaseMemoryCache同样也是实现了MemoryCache方法,不过它还是一个抽象类。
    它是一个内存缓存的基类,实现了内存缓存中常用的方法,只不过它里面提供了一个非强引用的Reference作为扩展,方便GC的回收,避免OOM.

    public abstract class BaseMemoryCache implements MemoryCache {
    
        /** Stores not strong references to objects */
        private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());
    
        @Override
        public Bitmap get(String key) {
            Bitmap result = null;
            Reference<Bitmap> reference = softMap.get(key);
            if (reference != null) {
                result = reference.get();
            }
            return result;
        }
    
        @Override
        public boolean put(String key, Bitmap value) {
            softMap.put(key, createReference(value));
            return true;
        }
    
        @Override
        public Bitmap remove(String key) {
            Reference<Bitmap> bmpRef = softMap.remove(key);
            return bmpRef == null ? null : bmpRef.get();
        }
    
        /** Creates {@linkplain Reference not strong} reference of value */
        protected abstract Reference<Bitmap> createReference(Bitmap value);
    }
    
    

    代码也比较简单,内存持有一个Map<String, Reference<Bitmap>> softMap来保存非强引用对象,具体的引用类型则看它实现的抽象方法createReference

    WeakMemoryCache

    我们看它的一个子类WeakMemoryCache则是继承与BaseMemory,实现createReference

    public class WeakMemoryCache extends BaseMemoryCache {
        @Override
        protected Reference<Bitmap> createReference(Bitmap value) {
            return new WeakReference<Bitmap>(value);
        }
    }
    
    

    很明显是来保存弱引用对象的。

    LimitedMemoryCache

    我们看它的另外一个子类LimitedMemoryCache,但它并没有实现BaseMemoryCache里的createReference方法,它也是一个抽象类,在BaseMemoryCache基础上封装了个抽象方法
    protected abstract Bitmap removeNext();用来处理当缓存容量不足时的情况。

    public abstract class LimitedMemoryCache extends BaseMemoryCache {
        ...
        //当前保存的Bitmap,用来统计缓存数
        private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());
        ...
        @Override
        public boolean put(String key, Bitmap value) {
            boolean putSuccessfully = false;
            // Try to add value to hard cache
            int valueSize = getSize(value);
            int sizeLimit = getSizeLimit();
            int curCacheSize = cacheSize.get();
            if (valueSize < sizeLimit) {
                while (curCacheSize + valueSize > sizeLimit) {
                    Bitmap removedValue = removeNext();
                    if (hardCache.remove(removedValue)) {
                        curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                    }
                }
                hardCache.add(value);
                cacheSize.addAndGet(valueSize);
    
                putSuccessfully = true;
            }
            // Add value to soft cache
            super.put(key, value);
            return putSuccessfully;
        }
    
        @Override
        public Bitmap remove(String key) {
            Bitmap value = super.get(key);
            if (value != null) {
                if (hardCache.remove(value)) {
                    cacheSize.addAndGet(-getSize(value));
                }
            }
            return super.remove(key);
        }
       ...
        protected abstract int getSize(Bitmap value);
    
        protected abstract Bitmap removeNext();
    }
    
    

    可以看到在 LimitedMemoryCache里面又有一个List<Bitmap>保存的是强引用,而在BaseMemoryCache里面也有个Map<String, Reference<Bitmap>> softMap来保存Bitmap,为什么要这样。

    这主要是因为在BaseMemoryCache里面并没有做缓存限制处理,它只是封装实现了基本的Bitmap的put,get。而当面对缓存容量有限的情况下,则需要交给子类去处理。

    我们看下这里的put方法,关键在

    while (curCacheSize + valueSize > sizeLimit) {
                    Bitmap removedValue = removeNext();
                    if (hardCache.remove(removedValue)) {
                        curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                    }
                }
    
    

    当超过容量时,调用抽象方法removeNext由子类自行实现,之后hardCache移除,但此时并没有调用softMap的移除。

    也就是对于List<Bitmap>来说,当它的缓存容量超过的时候,它会移除第一个对象来缓解容量,但是保存在Map<String, Reference<Bitmap>> softMap里面的Bitmap并没有被移除。
    如果这样下去softMap岂不是会无限大?

    这是因为在Map<String, Reference<Bitmap>> softMap里面保存的Bitmap是弱引用的存在,而在List<Bitmap>里面保存的是强引用,当内存不足的时候,GC则会先清除softMap里面的对象。

    FIFOLimitedMemoryCache

    我们看下LimitedMemoryCache的一个子类FIFOLimitedMemoryCache,看到FIFO也就是先进先出了。

    public class FIFOLimitedMemoryCache extends LimitedMemoryCache {
    
        private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());
        ...
        @Override
        public boolean put(String key, Bitmap value) {
            if (super.put(key, value)) {
                queue.add(value);
                return true;
            } else {
                return false;
            }
        }
    
        @Override
        public Bitmap remove(String key) {
            Bitmap value = super.get(key);
            if (value != null) {
                queue.remove(value);
            }
            return super.remove(key);
        }
        ...
        @Override
        protected Bitmap removeNext() {
            return queue.remove(0);
        }
    
        @Override
        protected Reference<Bitmap> createReference(Bitmap value) {
            return new WeakReference<Bitmap>(value);
        }
    }
    
    

    可以看到同样的这里也有个List<Bitmap> queue来保存记录,而在removeNext那里,返回的正是队列的第一个元素,符合FIFO。

    LRULimitedMemoryCache

    再来看一个另外一个子类LRULimitedMemoryCache也就是最近未使用删除。

    public class LRULimitedMemoryCache extends LimitedMemoryCache {
    
        /** Cache providing Least-Recently-Used logic */
        private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));
       ...
        @Override
        protected Bitmap removeNext() {
            Bitmap mostLongUsedValue = null;
            synchronized (lruCache) {
                Iterator<Entry<String, Bitmap>> it = lruCache.entrySet().iterator();
                if (it.hasNext()) {
                    Entry<String, Bitmap> entry = it.next();
                    mostLongUsedValue = entry.getValue();
                    it.remove();
                }
            }
            return mostLongUsedValue;
        }
    
        @Override
        protected Reference<Bitmap> createReference(Bitmap value) {
            return new WeakReference<Bitmap>(value);
        }
    }
    
    

    可以看到,这里的LRU处理则是使用LinkedHashMap,在它的构造方法中第三个参数为true表示使用LRU,之后再removeNext返回那个Bitmap。

    同理其他子类也如下,就不一一列举。

    MemoryCache小结

    1. 只使用的是强引用缓存

    • LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用)

    2.使用强引用和弱引用相结合的缓存有

    • UsingFreqLimitedMemoryCache(如果缓存的图片总量超过限定值,先删除使用频率最小的bitmap)

    • LRULimitedMemoryCache(这个也是使用的lru算法,和LruMemoryCache不同的是,他缓存的是bitmap的弱引用)

    • FIFOLimitedMemoryCache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap)

    • LargestLimitedMemoryCache(当超过缓存限定值,先删除最大的bitmap对象)

    • LimitedAgeMemoryCache(当 bitmap加入缓存中的时间超过我们设定的值,将其删除)

    3.只使用弱引用缓存

    • WeakMemoryCache(这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉)

    二.DiskCache硬盘缓存

    同样先来看个结构

     
    4356742-ad45b09c84a0ca5f.png
    image

    DiskCache的设计其实和MemoryCache一样,对于基类DiskCache,它同样是一个接口

    public interface DiskCache {
        //返回硬盘缓存的根目录
        File getDirectory();
    
        File get(String imageUri);
    
        boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;
    
        boolean save(String imageUri, Bitmap bitmap) throws IOException;
    
        boolean remove(String imageUri);
    
        void close();
    
        void clear();
    }
    
    

    同样一个个看

    LruDiskCache

    LruDiskCache则是直接实现了DiskCache接口,采用LRU算法来进行缓存处理。
    再理解LruDiskCache前,先理解另一个类DiskLruCache

    final class DiskLruCache implements Closeable {
        static final String JOURNAL_FILE = "journal";
        static final String JOURNAL_FILE_TEMP = "journal.tmp";
        static final String JOURNAL_FILE_BACKUP = "journal.bkp";
        static final String MAGIC = "libcore.io.DiskLruCache";
        ...
        private final LinkedHashMap<String, Entry> lruEntries =
                new LinkedHashMap<String, Entry>(0, 0.75f, true);
        ...
        final ThreadPoolExecutor executorService =
                new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        ...
        public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)
                throws IOException {
            ...
        }
        ...
        public synchronized Snapshot get(String key) throws IOException {
            ...
        }
        ...
        private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
            ...
        }
        /** A snapshot of the values for an entry. */
        public final class Snapshot implements Closeable {
            private final String key;
            private final long sequenceNumber;
            private File[] files;
            private final InputStream[] ins;
            private final long[] lengths;
            ...
        }
        ...
        public final class Editor {
            private final Entry entry;
            private final boolean[] written;
            private boolean hasErrors;
            private boolean committed;
            ...
        }
        ...
        private final class Entry {
            private final String key;
    
            private final long[] lengths;
    
            private boolean readable;
    
            private Editor currentEditor;
    
            private long sequenceNumber;
            ...
        }
    
    

    这个DiskLruCache比较长也比较复杂,它是LruDiskCache的一个文件工具类。这里的缓存数据存储在文件系统上的一个目录。
    同时也注意到这里的一个成员变量
    private final LinkedHashMap<String, Entry> lruEntries =new LinkedHashMap<String, Entry>(0, 0.75f, true);
    可以知道这是用来处理LRU的。

    同时这里的value则是EntryEntry则是封装了当前文件的编辑情况Ediotr以及key
    而这里Editor封装了文件的写入情况OutputStreamSnapshot封装了文件的读取情况InputStream

    回头看回LruDiskCache

    public class LruDiskCache implements DiskCache {
        protected DiskLruCache cache;
        private File reserveCacheDir;
    
        protected final FileNameGenerator fileNameGenerator;
        ...
        public LruDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize,
                int cacheMaxFileCount) throws IOException {
            ...
            this.reserveCacheDir = reserveCacheDir;
            this.fileNameGenerator = fileNameGenerator;
            initCache(cacheDir, reserveCacheDir, cacheMaxSize, cacheMaxFileCount);
        }
    
        private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
            ...
                cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
            ...
        }
        @Override
        public File get(String imageUri) {
            DiskLruCache.Snapshot snapshot = null;
            try {
                snapshot = cache.get(getKey(imageUri));
                return snapshot == null ? null : snapshot.getFile(0);
            } 
            ...
        }
        @Override
        public boolean save(String imageUri, Bitmap bitmap) throws IOException {
            DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
            ...
            OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
            boolean savedSuccessfully = false;
            try {
                savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
            }
            ...
            return savedSuccessfully;
        }
    
    

    首先LruDiskCache内部成员变量带有DiskLruCache还有文件的保存目录等,在它的构造方法中调用DiskLruCache.open方法创建了DiskLruCache对象,而在它的open方法里,则根据文件的目录情况创建了对应的文件系统。

    再看它的save方法,先调用getKey方法将uri转换为对应的key,而在cache,edit中

    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
            ...
            Entry entry = lruEntries.get(key);
            if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
                    || entry.sequenceNumber != expectedSequenceNumber)) {
                return null; // Snapshot is stale.
            }
            if (entry == null) {
                entry = new Entry(key);
                lruEntries.put(key, entry);
            } else if (entry.currentEditor != null) {
                return null; // Another edit is in progress.
            }
    
            Editor editor = new Editor(entry);
            entry.currentEditor = editor;
            ...
            return editor;
        }
    
    

    则是根据指定的key先判断缓存文件中有没有相应的key,如果没有则创建一个Entry对象持有它,之后保存在lruEntries之后,创建一个当前Entry的编辑对象Editor,以便之后写入到文件中。

    s之后调用了

            OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
    
    

    editor.newOutputStream则是根据当前目录和key创建出一个文件,之后打开这个文件的一个输出流情况,获取到之后就进行Bitmap的写入。

    同理,看下LruDiskCache的get方法

    @Override
        public File get(String imageUri) {
            DiskLruCache.Snapshot snapshot = null;
            try {
                snapshot = cache.get(getKey(imageUri));
                return snapshot == null ? null : snapshot.getFile(0);
            } 
            ...
        }
    
    

    调用了cache,get

    public synchronized Snapshot get(String key) throws IOException {
            。。。
            Entry entry = lruEntries.get(key);
            ...
            File[] files = new File[valueCount];
            InputStream[] ins = new InputStream[valueCount];
            try {
                File file;
                for (int i = 0; i < valueCount; i++) {
                    file = entry.getCleanFile(i);
                    files[i] = file;
                    ins[i] = new FileInputStream(file);
                }
            } 
            ...
            return new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
        }
    
    

    在get方法中,先根据key拿到对应的Entry,再拿到对应的文件打开输入流,之后传入到Snapshot
    而在snapshot.getFile

    /** Returns file with the value for {@code index}. */
            public File getFile(int index) {
                return files[index];
            }
    
    

    返回的则是对应的文件。

    BaseDiskCache

    BaseDiskCache同样也是直接实现了DiskCache方法,实现的方法也比较简单

    public abstract class BaseDiskCache implements DiskCache {
        ...
        protected final File cacheDir;
        protected final File reserveCacheDir;
    
        protected final FileNameGenerator fileNameGenerator;
    
        public BaseDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
            ...
            this.cacheDir = cacheDir;
            this.reserveCacheDir = reserveCacheDir;
            this.fileNameGenerator = fileNameGenerator;
        }
    
        @Override
        public boolean save(String imageUri, Bitmap bitmap) throws IOException {
            File imageFile = getFile(imageUri);
            File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
            OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
            boolean savedSuccessfully = false;
            try {
                savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
            } finally {
                IoUtils.closeSilently(os);
                if (savedSuccessfully && !tmpFile.renameTo(imageFile)) {
                    savedSuccessfully = false;
                }
                if (!savedSuccessfully) {
                    tmpFile.delete();
                }
            }
            bitmap.recycle();
            return savedSuccessfully;
        }
    
        @Override
        public File get(String imageUri) {
            return getFile(imageUri);
        }
    
        protected File getFile(String imageUri) {
            String fileName = fileNameGenerator.generate(imageUri);
            File dir = cacheDir;
            if (!cacheDir.exists() && !cacheDir.mkdirs()) {
                if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
                    dir = reserveCacheDir;
                }
            }
            return new File(dir, fileName);
        }
    
    

    比较简单,根据对应的文件去打开获取。它的两个子类LimitedAgeDiskCacheUnlimitedDiskCache也都不一一扩展开了。

    三、Universal-Image-Loader解析之源代码解析

    当我们配置好ImageConfigurationImageLoader后,我们就会开始调用

    ImageLoader.getInstance().loadImage(...);   
    ImageLoader.getInstance().displayImage(...);
    
    

    这两个方法其中一个来显示图片。
    先看loadImage

    public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options,
                ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
            checkConfiguration();
            if (targetImageSize == null) {
                targetImageSize = configuration.getMaxImageSize();
            }
            if (options == null) {
                options = configuration.defaultDisplayImageOptions;
            }
    
            NonViewAware imageAware = new NonViewAware(uri, targetImageSize, ViewScaleType.CROP);
            displayImage(uri, imageAware, options, listener, progressListener);
        }
    
    

    首先调用了checkConfiguration用来判断是否有初始化ImageLoaderConfiguration
    如果有设置ImageView的大小,则设置,没则默认Configuration的大小。
    如果没有设置DisplayImageOptions,则设置上一个默认的options
    之后创建了个NonViewAware,再调用displayImage
    也就是说,loadImage最终还是调用到了displayImage

    ImageAware

    这里的NonViewAware实现了ImageAware接口。先来看个结构图

     
    4356742-21353fd2bb1c610d.png
    image

    ImageAware是一个接口,内部提供了一系列操作图片的一些方法。
    对于NonViewAware来说,它内部只是简单的保存图片一些必要的数据,比如图片大小尺寸,URI,ScaleType这些。主要封装成ImageAware来给displayImage调用。

    看下displayImage的使用

    public void displayImage(String uri, ImageView imageView) {
            displayImage(uri, new ImageViewAware(imageView), null, null, null);
        }
    
    

    这里把ImageView封装成ImageViewAware再去调用displayImage这个就跟loadImage一样。
    而这里ImageViewAware继承与ViewAware,ViewAware则实现了ImageAware接口。
    NonViewAware不同的是ViewAware内部持有一个Reference<View> viewRef的成员变量,它是用来保存当前ImageView的一个弱引用,以便之后来直接设置显示图片。
    ViewAware很多方法都是依赖于这个View

    @Override
        public boolean setImageDrawable(Drawable drawable) {
            if (Looper.myLooper() == Looper.getMainLooper()) {
                View view = viewRef.get();
                if (view != null) {
                    setImageDrawableInto(drawable, view);
                    return true;
                }
            } else {
                L.w(WARN_CANT_SET_DRAWABLE);
            }
            return false;
        }
    
    

    之后就可以在ImageViewAware中设置显示。

    好了回过头看他们最终调用的方法。
    这个方法有点长,我们拆分成一部分一部分来看

    public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
                ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
            checkConfiguration();
            if (imageAware == null) {
                throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
            }
            if (listener == null) {
                listener = defaultListener;
            }
            if (options == null) {
                options = configuration.defaultDisplayImageOptions;
            }
    
            if (TextUtils.isEmpty(uri)) {
                engine.cancelDisplayTaskFor(imageAware);
                listener.onLoadingStarted(uri, imageAware.getWrappedView());
                if (options.shouldShowImageForEmptyUri()) {
                    imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
                } else {
                    imageAware.setImageDrawable(null);
                }
                listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
                return;
            }
            ...//下一部分看
        }
    
    

    首先先检查是否有初始化设置ImageLoaderConfiguration没则抛出异常,没设置listener和DisplayImageOptions则设置一个默认值。

    之后调用TextUtils.isEmpty(uri)判断是否当前的uri为空,则调用
    engine.cancelDisplayTaskFor(imageAware);
    之后则用listener通知开始和结束,也比较好理解,主要是这个engine。

    这个engine就是ImageLoaderEngine,主要用来负责显示加载图片的一个类。
    ImageLoaderEngine中存在一个HashMap,用来记录正在加载的任务,加载图片的时候会将ImageView的id和图片的url加上尺寸加入到HashMap中,加载完成之后会将其移除。

    接着看下面

    public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
                ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
            ...//前一部分
            if (targetSize == null) {
                targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
            }
            String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
            engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
    
            listener.onLoadingStarted(uri, imageAware.getWrappedView());
    
            Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
            if (bmp != null && !bmp.isRecycled()) {
                L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
    
                if (options.shouldPostProcess()) {
                    ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                            options, listener, progressListener, engine.getLockForUri(uri));
                    ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                            defineHandler(options));
                    if (options.isSyncLoading()) {
                        displayTask.run();
                    } else {
                        engine.submit(displayTask);
                    }
                } else {
                    options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                    listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
                }
            } 
            ...//下一部分
        }
    
    

    当URI不为空的时候来加载显示。首先根据uri获取对应uri对应唯一的一个Key,之后调用engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);来记录当前加载的任务,开启listener的start回调,接着调用Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);来获取内存缓存中的图片,这里默认的内存缓存是LruMemoryCache,前篇文章有分析到。

    如果缓存中存在相应的Bitmap的话,进入到if里面
    我们如果在DisplayImageOptions中设置了postProcessor就进入true逻辑,不过默认postProcessor是为null的,BitmapProcessor接口主要是对Bitmap进行处理,这个框架并没有给出相对应的实现,如果我们有自己的需求的时候可以自己实现BitmapProcessor接口(比如将图片设置成圆形的).

    然后到了27行
    将Bitmap设置到ImageView上面,这里我们可以在DisplayImageOptions中配置显示需求displayer,默认使用的是SimpleBitmapDisplayer,直接将Bitmap设置到ImageView上面,我们可以配置其他的显示逻辑, 他这里提供了FadeInBitmapDisplayer(透明度从0-1)RoundedBitmapDisplayer(4个角是圆弧)等, 然后回调到ImageLoadingListener接口。

    我们知道loadImagedisplayImage的区别在于loadImage依靠返回的Bitmap进行设置显示,而displayImage则是直接显示。而loadImage最终也是调用了displayImage,原因就在于这个display和imageAware

    public final class SimpleBitmapDisplayer implements BitmapDisplayer {
        @Override
        public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
            imageAware.setImageBitmap(bitmap);
        }
    }
    
    

    loadImageImageAwareNonImageAware并没有处理setImageBitmap的方法,而displayImageImageViewAware则有处理显示。

    好,继续前面,当从内存缓存获取到的Bitmap为空的情况下

    public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
                ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
                ...//前两部分
            //如果Bitmap为空
            } else {
                if (options.shouldShowImageOnLoading()) {
                    imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
                } else if (options.isResetViewBeforeLoading()) {
                    imageAware.setImageDrawable(null);
                }
    
                ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                        options, listener, progressListener, engine.getLockForUri(uri));
                LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                        defineHandler(options));
                if (options.isSyncLoading()) {
                    displayTask.run();
                } else {
                    engine.submit(displayTask);
                }
            }
        }
    
    

    如果需要设置显示加载中的图片,则进行设置显示。
    ImageLoadingInfo则是一个加载显示图片任务信息的一个类。
    之后根据它创建了一个LoadAndDisplayImageTask类,它实现了Runnable
    如果配置了isSyncLoading为true, 直接执行LoadAndDisplayImageTask的run方法,表示同步,默认是false,将LoadAndDisplayImageTask提交给线程池对象

    接下来我们就看LoadAndDisplayImageTask的run(), 这个类还是蛮复杂的,我们还是一段一段的分析。

    @Override
        public void run() {
            if (waitIfPaused()) return;
            if (delayIfNeed()) return;
    
            ...
        }
    
    

    如果waitIfPaused(), delayIfNeed()返回true的话,直接从run()方法中返回了,不执行下面的逻辑, 接下来我们先看看waitIfPaused()

    private boolean waitIfPaused() {
            AtomicBoolean pause = engine.getPause();
            if (pause.get()) {
                synchronized (engine.getPauseLock()) {
                    if (pause.get()) {
                        L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey);
                        try {
                            engine.getPauseLock().wait();
                        } catch (InterruptedException e) {
                            L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
                            return true;
                        }
                        L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);
                    }
                }
            }
            return isTaskNotActual();
        }
    
    

    这个方法是干嘛用呢,主要是我们在使用ListView,GridView去加载图片的时候,有时候为了滑动更加的流畅,我们会选择手指在滑动或者猛地一滑动的时候不去加载图片,所以才提出了这么一个方法,那么要怎么用呢? 这里用到了PauseOnScrollListener这个类,使用很简单ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling )), pauseOnScroll控制我们缓慢滑动ListView,GridView是否停止加载图片,pauseOnFling 控制猛的滑动ListView,GridView是否停止加载图片。

    我们可以看下这个PauseOnScrollListener的处理

    @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            switch (scrollState) {
                case OnScrollListener.SCROLL_STATE_IDLE:
                    imageLoader.resume();
                    break;
                case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
                    if (pauseOnScroll) {
                        imageLoader.pause();
                    }
                    break;
                case OnScrollListener.SCROLL_STATE_FLING:
                    if (pauseOnFling) {
                        imageLoader.pause();
                    }
                    break;
            }
            if (externalListener != null) {
                externalListener.onScrollStateChanged(view, scrollState);
            }
        }
    
    

    滑动停止的话会调用到imageLoader.pause

    public void pause() {
            engine.pause();
        }
    ...
    void pause() {
            paused.set(true);
        }
    这里的pause是
    private final AtomicBoolean paused = new AtomicBoolean(false);
    
    

    所以调用pause.get则会返回true。

    除此之外,这个方法的返回值由isTaskNotActual()决定,我们接着看看isTaskNotActual()的源码

    private boolean isTaskNotActual() {
            return isViewCollected() || isViewReused();
        }
    
    

    isViewCollected()是判断我们ImageView是否被垃圾回收器回收了,如果回收了,LoadAndDisplayImageTask方法的run()就直接返回了,isViewReused()判断该ImageView是否被重用,被重用run()方法也直接返回,为什么要用isViewReused()方法呢?主要是ListView,GridView我们会复用item对象,假如我们先去加载ListView,GridView第一页的图片的时候,第一页图片还没有全部加载完我们就快速的滚动,isViewReused()方法就会避免这些不可见的item去加载图片,而直接加载当前界面的图片。

    回头继续看run方法

    @Override
        public void run() {
            ...
            ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
            L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
            if (loadFromUriLock.isLocked()) {
                L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
            }
    
            loadFromUriLock.lock();
            Bitmap bmp;
            try {
                checkTaskNotActual();
    
                bmp = configuration.memoryCache.get(memoryCacheKey);
                if (bmp == null || bmp.isRecycled()) {
                    bmp = tryLoadBitmap();
                    if (bmp == null) return; // listener callback already was fired
    
                    checkTaskNotActual();
                    checkTaskInterrupted();
    
                    if (options.shouldPreProcess()) {
                        L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
                        bmp = options.getPreProcessor().process(bmp);
                        if (bmp == null) {
                            L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
                        }
                    }
    
                    if (bmp != null && options.isCacheInMemory()) {
                        L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
                        configuration.memoryCache.put(memoryCacheKey, bmp);
                    }
                } else {
                    loadedFrom = LoadedFrom.MEMORY_CACHE;
                    L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
                }
    
                if (bmp != null && options.shouldPostProcess()) {
                    L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
                    bmp = options.getPostProcessor().process(bmp);
                    if (bmp == null) {
                        L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
                    }
                }
                checkTaskNotActual();
                checkTaskInterrupted();
            } catch (TaskCancelledException e) {
                fireCancelEvent();
                return;
            } finally {
                loadFromUriLock.unlock();
            }
    
            DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
            runTask(displayBitmapTask, syncLoading, handler, engine);
        }
    
    

    第4行代码有一个loadFromUriLock,这个是一个锁,获取锁的方法在ImageLoaderEngine类的getLockForUri()方法中

    ReentrantLock getLockForUri(String uri) {  
            ReentrantLock lock = uriLocks.get(uri);  
            if (lock == null) {  
                lock = new ReentrantLock();  
                uriLocks.put(uri, lock);  
            }  
            return lock;  
        }  
    
    

    从上面可以看出,这个锁对象与图片的url是相互对应的,为什么要这么做?也行你还有点不理解,不知道大家有没有考虑过一个场景,假如在一个ListView中,某个item正在获取图片的过程中,而此时我们将这个item滚出界面之后又将其滚进来,滚进来之后如果没有加锁,该item又会去加载一次图片,假设在很短的时间内滚动很频繁,那么就会出现多次去网络上面请求图片,所以这里根据图片的Url去对应一个ReentrantLock对象,让具有相同Url的请求就会在第10行等待,等到这次图片加载完成之后,ReentrantLock就被释放,刚刚那些相同Url的请求就会继续执行第10行下面的代码。

    之后来到第13行,先调用checkTaskNotActual判断当前View是否被GC回收使用,是则抛出异常。
    接着15行,它们会先从内存缓存中获取一遍,如果内存缓存中没有在去执行下面的逻辑,所以ReentrantLock的作用就是避免这种情况下重复的去从网络上面请求图片。

    17行的方法tryLoadBitmap(),这个方法确实也有点长,我先告诉大家,这里面的逻辑是先从文件缓存中获取有没有Bitmap对象,如果没有在去从网络中获取,然后将bitmap保存在文件系统中,我们还是具体分析下

    private Bitmap tryLoadBitmap() throws TaskCancelledException {
            Bitmap bitmap = null;
            try {
                File imageFile = configuration.diskCache.get(uri);
                if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
                    L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
                    loadedFrom = LoadedFrom.DISC_CACHE;
    
                    checkTaskNotActual();
                    bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
                }
                if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
                    L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
                    loadedFrom = LoadedFrom.NETWORK;
    
                    String imageUriForDecoding = uri;
                    if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
                        imageFile = configuration.diskCache.get(uri);
                        if (imageFile != null) {
                            imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                        }
                    }
    
                    checkTaskNotActual();
                    bitmap = decodeImage(imageUriForDecoding);
    
                    if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
                        fireFailEvent(FailType.DECODING_ERROR, null);
                    }
                }
            }
            ...
            return bitmap;
        }
    
    

    首先在第4行会去磁盘缓存中去获取图片,如果图片已经保存在磁盘了,则直接获取对应的File路径,调用bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));进行解析。

    如果在磁盘中没有的话,则到了12行,开始进行网络下载获取。
    在17行会去调用isCacheOnDisk判断是否要保持在磁盘中,如果默认false,如果是则调用tryCacheImageOnDisk来下载图片并且保持在磁盘

    private boolean tryCacheImageOnDisk() throws TaskCancelledException {
            L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
    
            boolean loaded;
            try {
                loaded = downloadImage();
                ...
            } ...
            return loaded;
        }
    
    

    调用了downloadImage进行下载图片

    private boolean downloadImage() throws IOException {
            InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
            if (is == null) {
                L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey);
                return false;
            } else {
                try {
                    return configuration.diskCache.save(uri, is, this);
                } finally {
                    IoUtils.closeSilently(is);
                }
            }
        }
    
    

    可以看到这里调用了getDownloader().getStream来下载,这里先不扩展,在后面会说到
    下载之后则保存在磁盘中。
    回来前面

    String imageUriForDecoding = uri;
                    if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
                        imageFile = configuration.diskCache.get(uri);
                        if (imageFile != null) {
                            imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                        }
                    }
                    checkTaskNotActual();
                    bitmap = decodeImage(imageUriForDecoding);
    
    

    这里有个String变量imageUriForDecoding,初始值是uri,如果有设置磁盘缓存的话,则会调用tryCacheImageOnDisk来下载并且保持图片,此时的imageUriForDecoding则是文件File的路径。

    如果没有设置磁盘缓存的话,则imageUriForDecoding还是uri。
    关键则是在decodeImage,它能根据对应的uri来加载图片。

        private Bitmap decodeImage(String imageUri) throws IOException {
            ViewScaleType viewScaleType = imageAware.getScaleType();
            ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
                    getDownloader(), options);
            return decoder.decode(decodingInfo);
        }
    
    

    把传递进来的imageUri(可能是文件的uri,也可能是图片的uri)封装到ImageDecodingInfo进行解析。
    这里的decoder是ImageDecode,它的默认实现类是BaseImageDecode

    @Override
        public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
            Bitmap decodedBitmap;
            ImageFileInfo imageInfo;
    
            InputStream imageStream = getImageStream(decodingInfo);
            ...
    }
    
    

    通过getImageStream来获取输入流

    protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {
            return decodingInfo.getDownloader().getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader());
        }
    
    

    这里的Downloader默认实现类是BaseImageDownloader

        @Override
        public InputStream getStream(String imageUri, Object extra) throws IOException {
            switch (Scheme.ofUri(imageUri)) {
                case HTTP:
                case HTTPS:
                    return getStreamFromNetwork(imageUri, extra);
                case FILE:
                    return getStreamFromFile(imageUri, extra);
                case CONTENT:
                    return getStreamFromContent(imageUri, extra);
                case ASSETS:
                    return getStreamFromAssets(imageUri, extra);
                case DRAWABLE:
                    return getStreamFromDrawable(imageUri, extra);
                case UNKNOWN:
                default:
                    return getStreamFromOtherSource(imageUri, extra);
            }
        }
    
    

    可以看到,在这里,已经做了多种情况的读取判断。第一篇文章就有介绍到UIL可以根据不同的uri来解析图片,其原理就是在这里。
    而前面通过tryCacheImageOnDisk来下载图片也是根据这个。这里就不一一扩展开。
    这里的网络下载图片内部则是使用HttpUrlConnection来下载的。

    回到最前面LoadAndDisplayImageTask的run方法后面,当我们获取到Bitmap后,到了

        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
            runTask(displayBitmapTask, syncLoading, handler, engine);
    
    

    这两个代码就是一个显示任务
    直接看DisplayBitmapTask类的run()方法

        @Override
        public void run() {
            if (imageAware.isCollected()) {
                L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
                listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
            } else if (isViewWasReused()) {
                L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
                listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
            } else {
                L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
                displayer.display(bitmap, imageAware, loadedFrom);
                engine.cancelDisplayTaskFor(imageAware);
                listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
            }
        }
    
    

    假如ImageView被回收了或者被重用了,回调给ImageLoadingListener接口,否则就调用BitmapDisplayer去显示Bitmap。到这里Bitmap已经显示加载完成,调用engine移除图片显示任务。

    当然在最前面那里

    public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
                ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
                ...
                ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                        options, listener, progressListener, engine.getLockForUri(uri));
                LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                        defineHandler(options));
                if (options.isSyncLoading()) {
                    displayTask.run();
                } else {
                    engine.submit(displayTask);
                }
            }
        }
    
    

    如果此时的显示加载是异步的话,则交由engine的Executor线程池去处理,最终也是调用了LoadAndDisplayImageTask的run方法去加载显示。

    到这里Universal-Image-Loader的分析也算完了,从基本使用到内存模型在加载显示,可以看到UIL这个开源框架十分的灵活,比如建造者模式,装饰模式,代理模式,策略模式等等,这样方便我们去扩展,实现我们想要的功能,当然,也带给我们更多的想象空间。

    原文链接:https://www.jianshu.com/p/cff58eddb4ae
    阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

  • 相关阅读:
    Java基础教程(20)--数字和字符串
    Java基础教程(19)--Object类
    python 选择和循环结构
    购物清单
    第五次安卓作业(计算器和增删改查)
    第四次安卓作业(用户添加)
    第三次安卓作业(用户登录)
    第二次安卓作业(九宫格)
    5.22作业
    5.29作业
  • 原文地址:https://www.cnblogs.com/Android-Alvin/p/11990557.html
Copyright © 2020-2023  润新知