Android-Universal-Image-Loader源码

作者: 正规程序员 | 来源:发表于2017-12-28 20:21 被阅读15次

    ImageLoader简单使用

    ImageLoader的二级分包主要由cache、core和utils组成,三者分别负责缓存、核心加载类和工具类。
    在核心加载类core的子分包中,包括显示、下载、进度监听及属性配置等。

    外部调用:

    ImageLoader.getInstance().displayImage(url, imageShow);
    

    这里底层代码由displayImage方法负责:

    public void displayImage(
                String uri,
                ImageAware imageAware, 
                DisplayImageOptions options,
                ImageSize targetSize, 
                ImageLoadingListener listener, 
                ImageLoadingProgressListener progressListener) {
    ...
    }
    

    ImageAware:ImageAware这个类会将ImageView转换成ImageViewAware, ImageViewAware主要是做什么的呢?该类主要是将ImageView进行一个包装,将ImageView的强引用变成弱引用,当内存不足的时候,可以更好的回收ImageView对象,还有就是获取ImageView的宽度和高度。这使得我们可以根据ImageView的宽高去对图片进行一个裁剪,减少内存的使用。

    通过memoryCacheKey判断内存缓存中是否有bitmap。

    设置图片缓存时要做的事

    为了能够选择一个合适的缓存大小给缓存类, 有以下多个因素应该放入考虑范围内,例如:

    • 你的设备可以为每个应用程序分配多大的内存?
    • 设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
    • 你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
    • 图片的尺寸和大小,还有每张图片会占据多少内存空间。
    • 图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
    • 你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。

    摘自:Android高效加载大图、多图解决方案,有效避免程序OOM

    Listview快速滑动不加载图片策略

    在PauseOnScrollListener类中,通过监听状态OnScrollListener.SCROLL_STATE_FLING执行imagleloader.pause方法,
    最后调用ImageLoaderEngine类的原子变量pause。

    LoadAndDisplayImageTask类中:根据原子变量pause的状态对象锁控制阻塞。

    /** @return <b>true</b> - if task should be interrupted; <b>false</b> - otherwise */
        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();
        }
    

    PauseOnScrollListener类中:监听滑动状态,并根据scroll状态改变ImageLoaderEngine类的原子变量pause值。

        @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);
            }
        }
     
    

    AtomicBoolean是java.util.concurrent.atomic包下的原子变量,这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入。

    ReentrantLock锁机制

    典型写法:

    Lock lock = new ReentrantLock();
    lock.lock();
    try {
        // update object state
    }finally {
        lock.unlock();
    }
    

    Lock是基于JDK层面实现的,通常需要在finally中进行锁的释放,如果忘记就会造成死锁。
    ReentrantLock实现Lock接口,所有的同步操作都是依靠AbstractQueuedSynchronizer(队列同步器)实现。

    API 如下:public void java.util.concurrent.locks.ReentrantLock.lock()

    功能:获取锁。
    如果该锁没有被另一个线程保持,则获取该锁并立即返回,并将锁的保持计数器设置为1.
    如果当前线程已经保持该锁,则将保持计数加1,并且该方法立即返回。
    如果该锁被另一个线程保持,则出于线程调度的目的,禁用该线程,并且在获得锁之前,该线程一直处于休眠状态,此时锁保持计数被设置为1.

    公平锁(FairSync)与非公平锁(NonfairSync):如果获取一个锁是按照请求的顺序得到的,那么就是公平锁,否则就是非公平的。

    • 公平锁保证一个阻塞的线程最终能够获得锁,因为是有序的,所以总是可以按照请求的顺序获得锁。
    • 不公平锁意味着后请求锁的线程可能在其前面排列的休眠线程恢复前拿到锁,这样就有可能提高并发的性能。这是因为通常情况下挂起的线程重新开始与它真正开始运行,二者之间会产生严重的延时。因此非公平锁就可以利用这段时间完成操作。这是非公平锁在某些时候比公平锁性能要好的原因之一。

    详细的介绍我们在另一篇博客里阐述。todo

    内存缓存策略

    LruMemoryCache

    这个类是通过LinkedHashMap有序的保存着强引用bitmap,是Android-Universal-Image-Loader框架默认的缓存类。

    特点

    • 采用强引用方式保存bitmap;
    • 如果超出最大缓存字节限制,则会循环遍历并删除map的第一位(即删除最早缓存的,因此要有序);

    在开源库中,DefaultConfigurationFactory类下的createMemoryCache方法配置缓存类的初始化。

        /**
         * Creates default implementation of {@link MemoryCache} - {@link LruMemoryCache}<br />
         * Default cache size = 1/8 of available app memory.
         */
        public static MemoryCache createMemoryCache(Context context, int memoryCacheSize) {
            if (memoryCacheSize == 0) {
                ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
                //获取可使用的最大内存,单位MB
                int memoryClass = am.getMemoryClass();
                if (hasHoneycomb() && isLargeHeap(context)) {
                    memoryClass = getLargeMemoryClass(am);
                }
                //将最大内存的1/8转变成字节单位。
                memoryCacheSize = 1024 * 1024 * memoryClass / 8;
            }
            return new LruMemoryCache(memoryCacheSize);
        }
    

    1MB = 1024KB = 1024* 1024B(字节)。1B = 8bit(8位,即8位二进制数)。

    “删除最早缓存的”代码如下:

    /**
         * Remove the eldest entries until the total of remaining entries is at or below the requested size.
         *
         * @param maxSize the maximum size of the cache before returning. May be -1 to evict even 0-sized elements.
         */
        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;
                    }
    
                    //这是在size > maxSize的情况下,通过Map.entrySet使用iterator遍历key和value,每次都是先获取第一个。remove之后循环下一次。
                    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的情况下,通过Map.entrySet使用iterator遍历key和value,每次都是先获取第一个。remove之后循环下一次。

    UsingFreqLimitedMemoryCache

    这个类是通过对已存储的bitmap的使用率进行比对,从而移除最小使用率的bitmap,达到控制内存的目的。

    特点:

    • 采用强引用和弱引用相结合的方式存储bitmap,并存储使用次数Integer;
    • put时使用次数置零,get时使用次数+1;
    • 在超出内存上限时,遍历map比较出最小使用次数的bitmap,然后移除;

    UsingFreqLimitedMemoryCache继承了父类LimitedMemoryCache,LimitedMemoryCache继承父类BaseMemoryCache。
    UsingFreqLimitedMemoryCache中的数据结构是:

    /**
         * Contains strong references to stored objects (keys) and last object usage date (in milliseconds). If hard cache
         * size will exceed limit then object with the least frequently usage is deleted (but it continue exist at
         * {@link #softMap} and can be collected by GC at any time)
         */
        private final Map<Bitmap, Integer> usingCounts = Collections.synchronizedMap(new HashMap<Bitmap, Integer>());
    

    map中的key是bitmap,表示存储的位图;value是Integer,表示使用的次数。
    Collections.synchronizedMap,解决了线程安全性问题。 通过将基本的功能从线程安全性中分离开来,允许需要同步的用户可以拥有同步,而不需要同步的用户则不必为同步付出代价。
    LimitedMemoryCache中的数据结构是:

    private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());
    

    此类主要实现一些获取可用内存字节和移除使用率最低的抽象方法。(本来我认为此类可舍弃,该类的逻辑代码可在BaseMemoryCache中进行,但是看到LRULimitedMemoryCache类之后,觉得我的思想设计扩展性很不强哦)
    BaseMemoryCache中的数据结构是:

    private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());
    

    这里是将value对应的bitmap放在引用的泛型中,并在UsingFreqLimitedMemoryCache类中实现抽象方法createReference()。

    因为WeakReference< T >等都是以此形式继承Reference< T >,如此便于扩展。

    UsingFreqLimitedMemoryCache中:

    Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
    

    因此,存储的bitmap是以弱引用的形式存储在hashmap中。

    put:

    Override
        public boolean put(String key, Bitmap value) {
            if (super.put(key, value)) {
                usingCounts.put(value, 0);
                return true;
            } else {
                return false;
            }
        }
    

    这里在对map进行put值的时候,会默认将表示使用次数的Integer设置为0;而在get的时候会将其+1;
    在第二行里子类调用了父类的put方法,其中包含了对removeNext()的调用,是为了处理当存储超出上限时移除使用率最小的值。

    get:

        @Override
        public Bitmap get(String key) {
            Bitmap value = super.get(key);
            // Increment usage count for value if value is contained in hardCahe
            if (value != null) {
                Integer usageCount = usingCounts.get(value);
                if (usageCount != null) {
                    usingCounts.put(value, usageCount + 1);
                }
            }
            return value;
        }
    

    removeNext():

        @Override
        protected Bitmap removeNext() {
            Integer minUsageCount = null;
            Bitmap leastUsedValue = null;
            Set<Entry<Bitmap, Integer>> entries = usingCounts.entrySet();
            synchronized (usingCounts) {
                for (Entry<Bitmap, Integer> entry : entries) {
                    if (leastUsedValue == null) {
                        leastUsedValue = entry.getKey();
                        minUsageCount = entry.getValue();
                    } else {
                        Integer lastValueUsage = entry.getValue();
                        if (lastValueUsage < minUsageCount) {
                            minUsageCount = lastValueUsage;
                            leastUsedValue = entry.getKey();
                        }
                    }
                }
            }
            //检索完毕之后,移除使用频率最低的那个值。
            usingCounts.remove(leastUsedValue);
            return leastUsedValue;
        }
    

    这里,lastValueUsage表示本次(最后、最新)循环内的使用次数值,minUsageCount不为null时表示上次循环时赋的值。
    如果检索出更小使用次数时,则将该循环内的entry.getKey()和entry.getValue()赋值给leastUsedValue和minUsageCount。

    LRULimitedMemoryCache

    此类和UsingFreqLimitedMemoryCache都是继承父类LimitedMemoryCache,意味着上层的逻辑处理是一样的。

    特点:

    • 采用强引用和弱引用相结合的方式存储bitmap;
    • 在超出内存上限时,遍历map比较出最近最少使用的的bitmap,然后移除;

    可以理解为只是将LruMemoryCache中的bitmap由强引用转化为弱引用。

    此类的数据结构是:

        /** Cache providing Least-Recently-Used logic */
        private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));
    

    该类没有统计使用率的Integer的value,但有LinkedHashMap组成的有序的结构体,LinkedHashMap是一个双向循环链表。
    后续会有章节详述Java数据结构及原理,此处略。
    removeNext:

        @Override
        protected Bitmap removeNext() {
            Bitmap mostLongUsedValue = null;
            synchronized (lruCache) {
                //移除最早的那个,因为数据结构是LinkedHashMap,存储是有序的。
                Iterator<Entry<String, Bitmap>> it = lruCache.entrySet().iterator();
                if (it.hasNext()) {
                    Entry<String, Bitmap> entry = it.next();
                    mostLongUsedValue = entry.getValue();
                    it.remove();
                }
            }
            return mostLongUsedValue;
        }
    

    既然LRULimitedMemoryCache和UsingFreqLimitedMemoryCache都是继承父类LimitedMemoryCache,我们看看父类的方法吧。

    在LimitedMemoryCache中,

        @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();
                    //硬盘缓存移除使用频率最低的bitmap
                    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;
        }
    

    在当前已缓存的字节+此次要缓存的bitmap的字节之和大于缓存字节上限时,清除子类指定的bitmap。

    FIFOLimitedMemoryCache

    此类和UsingFreqLimitedMemoryCache都是继承父类LimitedMemoryCache,意味着上层的逻辑处理是一样的。

    特点:

    • 采用强引用和弱引用相结合的方式存储bitmap;
    • 在超出内存上限时,遍历map比较出最早存储的bitmap,然后移除;

    此类的数据结构与之前的不同:

    private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());
    

    LinkedList列表先进先出的方式存储bitmap。

    Override
        protected Bitmap removeNext() {
            return queue.remove(0);
        }
    

    超出内存存储上限时的处理也是直接移除先进去(第一个)的bitmap。

    LargestLimitedMemoryCache

    此类和UsingFreqLimitedMemoryCache都是继承父类LimitedMemoryCache,意味着上层的逻辑处理是一样的。

    特点:

    • 采用强引用和弱引用相结合的方式存储bitmap;
    • 在超出内存上限时,遍历map比较出内存占用最大的bitmap,然后移除;

    此类的数据结构是:

        /**
         * Contains strong references to stored objects (keys) and sizes of the objects. If hard cache
         * size will exceed limit then object with the largest size is deleted (but it continue exist at
         * {@link #softMap} and can be collected by GC at any time)
         */
        private final Map<Bitmap, Integer> valueSizes = Collections.synchronizedMap(new HashMap<Bitmap, Integer>());
    

    疑问:既然都是继承父类LimitedMemoryCache,也都采用强引用和弱引用相结合的方式实现存储。那么三者最后是将bitmap以强引用bitmap、弱引用bitmap、LinkedList中bitmap这其中哪种方式存储呢?

    答:是以子类中createReference(Bitmap value)方法指定的引用类型存储的,目前三者都是以WeakReference弱引用的形式存储在基类BaseMemoryCache定义的HashMap中。
    依据:子类中的get和put方法都是调用基类BaseMemoryCache的方法,而子类自己的数据结构以及父类LimitedMemoryCache的是以一种便于操作数据的数据结构中存在的(因为弱引用的生命周期不定)。

    LimitedAgeMemoryCache

    特点:

    • 采用强引用bitmap的key、vaule形式存储bitmap;
    • get获取值的时候检测HashMap内的Long类型的时间戳是否超出最大时限,若超出则删除。

    结构体如下:

    private final Map<String, Long> loadingDates = Collections.synchronizedMap(new HashMap<String, Long>());
    

    get

        @Override
        public Bitmap get(String key) {
            Long loadingDate = loadingDates.get(key);
            Bitmap bitmap = cache.get(key);
            //在获取值的时候检测时间是否超出缓存时限,若超出则移除。
            if (loadingDate != null && System.currentTimeMillis() - loadingDate > maxAge) {
                cache.remove(key);
                loadingDates.remove(key);
            }
            return bitmap;
        }
    

    这里我做了修改,增加了Bitmap bitmap = cache.get(key);

    WeakMemoryCache

    此类直接继承BaseMemoryCache实现弱引用bitmap存储。

    特点:

    • 采用弱引用bitmap方式存储在HashMap中。

    类结构如下:

    /**
     * Memory cache with {@linkplain WeakReference weak references} to {@linkplain Bitmap bitmaps}<br />
     * <br />
     * <b>NOTE:</b> This cache uses only weak references for stored Bitmaps.
     *
     * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
     * @since 1.5.3
     */
    public class WeakMemoryCache extends BaseMemoryCache {
        @Override
        protected Reference<Bitmap> createReference(Bitmap value) {
            return new WeakReference<Bitmap>(value);
        }
    }
    

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

    LruCache

    先看下构造方法:

       /**
         * LruCache的构造方法:需要传入最大缓存个数
         */
        public LruCache(int maxSize) {
            // 最大缓存个数小于0,会抛出IllegalArgumentException
            if (maxSize <= 0) {
                throw new IllegalArgumentException("maxSize <= 0");
            }
            this.maxSize = maxSize;
            /*
             * 初始化LinkedHashMap
             * 第一个参数:initialCapacity,初始大小
             * 第二个参数:loadFactor,负载因子=0.75f
             * 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
             */
            this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
        }
    

    这里设置了最大缓存的字节数,一般是16M10241024B,然后初始化了LinkedHashMap的参数,其中accessOrder=true,表示基于访问顺序进行排序的。

    new LinkedHashMap<K, V> 中的键值对是通过类的泛型传入的。比如图片加载,可以用new LinkedHashMap<String, Bitmap>作为键值对。

    在put方法里

      /**
         * 给对应key缓存value,该value将被移动到队头。
         */
        public final V put(K key, V value) {
            if (key == null || value == null) {
                throw new NullPointerException("key == null || value == null");
            }
    
            V previous;
            synchronized (this) {
                // 记录 put 的次数
                putCount++;
                // 拿到键值对,计算出在容量中的相对长度,然后加上
                size += safeSizeOf(key, value);
                /*
                 * 放入 key value
                 * 如果 之前存在key 则返回 之前key 的value
                 * 记录在 previous
                 */
                previous = map.put(key, value);
                // 如果存在冲突
                if (previous != null) {
                    // 计算出 冲突键值 在容量中的相对长度,然后减去
                    size -= safeSizeOf(key, previous);
                }
            }
    
            // 如果上面发生冲突
            if (previous != null) {
                /*
                 * previous值被剔除了,此次添加的 value 已经作为key的 新值
                 * 告诉 自定义 的 entryRemoved 方法
                 */
                entryRemoved(false, key, previous, value);
            }
            trimToSize(maxSize);
            return previous;
        }
    

    和以往的缓存类相似。在同步方法块内,先计算要put的value的大小(字节),size累加,然后map.put(),该方法的返回值若不为null,表明替换掉同一个key的旧的value值了。并且size减去旧值的大小(字节)。

    同步块下,有个entryRemoved方法,这个方法默认没有实现,暂做剔除的自定义处理。

    每次put 新值的时候就要检测是否超出最大缓存空间。这个步骤是在trimToSize方法内进行。

    trimToSize如下:

     /**
         * 删除最旧的数据直到剩余的数据的总数以下要求的大小。
         */
        public void trimToSize(int maxSize) {
            /*
             * 这是一个死循环,
             * 1.只有 扩容 的情况下能立即跳出
             * 2.非扩容的情况下,map的数据会一个一个删除,直到map里没有值了,就会跳出
             */
            while (true) {
                K key;
                V value;
                synchronized (this) {
                    // 在重新调整容量大小前,本身容量就为空的话,会出异常的。
                    if (size < 0 || (map.isEmpty() && size != 0)) {
                        throw new IllegalStateException(
                                getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                    }
                    // 如果是 扩容 或者 map为空了,就会中断,因为扩容不会涉及到丢弃数据的情况
                    if (size <= maxSize || map.isEmpty()) {
                        break;
                    }
                    //剩下的情况就是 if (size > maxSize && !map.isEmpty())即存储已超出最大缓存空间
    
                    Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                    key = toEvict.getKey();
                    value = toEvict.getValue();
                    map.remove(key);
                    // 拿到键值对,计算出在容量中的相对长度,然后减去。
                    size -= safeSizeOf(key, value);
                    // 添加一次收回次数
                    evictionCount++;
                }
                /*
                 * 将最后一次删除的最少访问数据回调出去
                 */
                entryRemoved(true, key, value, null);
            }
        }
    

    这里检测插入的值是否超出最大缓存空间,如果超出了就会执行移除操作。

    while循环中遍历进行,符合条件(超出最大缓存空间时)就会执行 map.remove(key)。由于LinkedHashMap是按照访问顺序排序的,所以此缓存策略是按照“最近最少使用”算法(在链表的尾部是最近刚刚使用的结点,在链表的头部是是最近最少使用的结点)进行剔除的。

    为了实现多线程中的同步,对缓存的操作是放在同步块中进行的。

    我们再看下get方法:

     /**
         * 根据key查询缓存,如果存在于缓存或者被create方法创建了。
         * 如果值返回了,那么它将被移动到双向循环链表的的尾部。
         * 如果如果没有缓存的值,则返回null。
         */
        public final V get(K key) {
            if (key == null) {
                throw new NullPointerException("key == null");
            }
    
            V mapValue;
            synchronized (this) {
                // LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
                mapValue = map.get(key);
                // 计算 命中次数
                if (mapValue != null) {
                    hitCount++;
                    return mapValue;
                }
                // 计算 丢失次数
                missCount++;
            }
    
            /*
             * 官方解释:
             * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
             * 候,一个冲突的值被添加到Map,我们在Map中删除这个值,释放被创造的值。
             */
            V createdValue = create(key);
            if (createdValue == null) {
                return null;
            }
    
            /***************************
             * 不覆写create方法走不到下面 *
             ***************************/
    
            /*
             * 正常情况走不到这里
             * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
             * 因为默认的 create(K key) 逻辑为null
             */
            synchronized (this) {
                // 记录 create 的次数
                createCount++;
                // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
                mapValue = map.put(key, createdValue);
    
                // 如果之前存在相同key的value,即有冲突。
                if (mapValue != null) {
                    /*
                     * 有冲突
                     * 所以 撤销 刚才的 操作
                     * 将 之前相同key 的值 重新放回去
                     */
                    map.put(key, mapValue);
                } else {
                    // 拿到键值对,计算出在容量中的相对长度,然后加上
                    size += safeSizeOf(key, createdValue);
                }
            }
    
            // 如果上面 判断出了 将要放入的值发生冲突
            if (mapValue != null) {
                /*
                 * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
                 * 告诉 自定义 的 entryRemoved 方法
                 */
                entryRemoved(false, key, createdValue, mapValue);
                return mapValue;
            } else {
                // 上面 进行了 size += 操作 所以这里要重整长度
                trimToSize(maxSize);
                return createdValue;
            }
        }
    

    通篇的代码逻辑很复杂,简单说就是如果get不到值,可以在create(key)里自定义一个value,然后put到map中。如果此时检测到map中有该key的旧值,就撤销put之前create的值。同样的,对于所有要剔除的操作都要走entryRemoved方法。

    注意:网上下载的源码里的汉语注释,不敢恭维。

    DiskLruCache

    DiskLruCache构造方法:
    DiskLruCache类是因为是常量类,不能通过构造方法实例化,这里通过open方法获取DiskLruCache实例。

        /**
         * Opens the cache in {@code directory}, creating a cache if none exists
         * there.
         *
         * @param directory a writable directory,for example," /sdcard/Android/data/<application package>/cache"
         * @param valueCount the number of values per cache entry. Must be positive.
         * @param maxSize the maximum number of bytes this cache should use to store
         * @param maxFileCount the maximum file count this cache should store
         * @throws IOException if reading or writing the cache directory fails
         */
        public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)
                throws IOException {
                ...
        }
    

    open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。

    这里缓存的位置通常存放在 /sdcard/Android/data/<application package>/cache 这个路径下面,如果没有SD卡或被移除了,可以采用如下代码设置缓存地址。

    /**
         * 获取本地文件
         * @param uniqueName 子目录
         * @return
         */
        public File getDiskCacheDir(String uniqueName) {
            String cachePath;
            if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                    || !Environment.isExternalStorageRemovable()) {
                cachePath = App.getAppContext().getExternalCacheDir().getPath();
            } else {
                cachePath = App.getAppContext().getCacheDir().getPath();
            }
            return new File(cachePath + File.separator + uniqueName);
        }
    
    

    对于版本号appVersion这个参数,每当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。

    外部实例化DiskLruCache如下:

    DiskLruCache mDiskLruCache = null;
    try {
        File cacheDir = getDiskCacheDir(context, "bitmap");
        if (!cacheDir.exists()) {
            cacheDir.mkdirs();
        }
        mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
    } catch (IOException e) {
        e.printStackTrace();
    }
    

    DiskLruCache写入:
    DiskLruCache写入的操作是借助DiskLruCache.Editor这个类完成的,需要调用DiskLruCache的edit()方法来获取实例。

    DiskLruCache.Editor editor = mDiskLruCache.edit(key);  
    

    由于缓存的key要和缓存的value一一对应,这里通常是将URL通过MD5编码后作key。

    MD5编码如下:

      /**
         * MD5 算法
         * @param key
         * @return
         */
        public String hashKeyForDisk(String key) {
            String cacheKey;
            try {
                final MessageDigest mDigest = MessageDigest.getInstance("MD5");
                mDigest.update(key.getBytes());
                cacheKey = bytesToHexString(mDigest.digest());
            } catch (NoSuchAlgorithmException e) {
                cacheKey = String.valueOf(key.hashCode());
            }
            return cacheKey;
        }
    
        private String bytesToHexString(byte[] bytes) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < bytes.length; i++) {
                String hex = Integer.toHexString(0xFF & bytes[i]);
                if (hex.length() == 1) {
                    sb.append('0');
                }
                sb.append(hex);
            }
            return sb.toString();
        }
    

    具体编码算法,后续再研究。

    获取editor实例后,通过newOutputStream()方法可获得一个输出流OutputStream。然后将此输出流传入网络下载中,即可实现在文件下载的过程中,将资源缓存到本地。

    完整的缓存过程如下:

    new Thread(new Runnable() {  
        @Override  
        public void run() {  
            try {  
                String imageUrl = "http://a3.topitme.com/8/2d/b7/1128528404720b72d8o.jpg";  
                String key = hashKeyForDisk(imageUrl);  
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);  
                if (editor != null) {  
                    OutputStream outputStream = editor.newOutputStream(0);  
                    if (downloadImg(imageUrl, outputStream)) {  
                        editor.commit();  
                    } else {  
                        editor.abort();  
                    }  
                }  
                mDiskLruCache.flush();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }).start();
    

    newOutputStream()方法接收一个index参数,由于前面在设置valueCount的时候指定的是1,所以这里index传0就可以了(即索引为0)。
    这里调用一下commit()方法进行提交才能使写入生效,调用abort()方法的话则表示放弃此次写入。

    mDiskLruCache.flush()方法是用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候我有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。

    DiskLruCache读取:

    因为写入缓存的时候是通过MD5编码之后的key作为唯一性标识的,读取的时候也是首先要将URL转码成Key。

    String imageUrl = "http://a3.topitme.com/8/2d/b7/1128528404720b72d8o.jpg";  
    String key = hashKeyForDisk(imageUrl);  
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); 
    

    这里通过获得DiskLruCache.Snapshot实例,然后调用其getInputStream()就可获得输入流,进而转发成对应的文件,如bitmap。这里getInputStream(0)的参数和newOutputStream(0)的参数是一一对应的。

    完整的读取代码如下:

    try {  
        String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
        String key = hashKeyForDisk(imageUrl);  
        DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
        if (snapShot != null) {  
            InputStream is = snapShot.getInputStream(0);  
            Bitmap bitmap = BitmapFactory.decodeStream(is);  
            mImage.setImageBitmap(bitmap);  
        }  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
    

    我们在DiskLruCache源码中,会看到其也用到了LinkedHashMap这个结构体

    private final LinkedHashMap<String, Entry> lruEntries =new LinkedHashMap<String, Entry>(0, 0.75f, true);
    

    这也是基于访问顺序的“最近最少使用算法”,其中的value是Entry类,表示文件的实体类。因此DiskLruCache和LruCache底层思想都是相同的,只是存储的对象有所不同。

    LRU算法(Least Recently Used )

    缓存主要由LruCache这个类实现,LRU是Least Recently Used 近期最少使用算法,多用于清理长时间未使用的资源。

    通过查看LinkedHashMap源码可知:

     /**
         * Constructs an empty <tt>LinkedHashMap</tt> instance with the
         * specified initial capacity, load factor and ordering mode.
         *
         * @param  initialCapacity the initial capacity
         * @param  loadFactor      the load factor
         * @param  accessOrder     the ordering mode - <tt>true</tt> for
         *         access-order, <tt>false</tt> for insertion-order
         * @throws IllegalArgumentException if the initial capacity is negative
         *         or the load factor is nonpositive
         */
        public LinkedHashMap(int initialCapacity,
                             float loadFactor,
                             boolean accessOrder) {
            super(initialCapacity, loadFactor);
            this.accessOrder = accessOrder;
        }
    

    boolean值 accessOrder参数为true表示按照使用顺序排列,为false表示按照插入顺序排列。

    当accessOrder 设置为 true时,每当我们更新(即调用put方法)或访问(即调用get方法)map中的结点时,LinkedHashMap内部都会将这个结点移动到链表的尾部,因此,在链表的尾部是最近刚刚使用的结点,在链表的头部是是最近最少使用的结点,当我们的缓存空间不足时,就应该持续把链表头部结点移除掉,直到有剩余空间放置新结点。

    引用

    Android-Universal-Image-Loader
    LruCache 源码解析
    DiskLruCache

    相关文章

      网友评论

        本文标题:Android-Universal-Image-Loader源码

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