Android Bitmap 全面解析(三) 开源图片框架分析 UIL

Posted by boredream on May 15, 2014

主要介绍这三个框架,都挺有名的,其他的框架估计也差不多了

Android-Universal-Image-Loader

ImageLoade

Volley(综合框架,包含图片部分)

扯淡时间,可以跳过这段 这些开源框架的源码还是挺复杂的,本人技术有限,有部分分析不对的地方或者不足的地方希望大家一起讨论。

由于有大量的源代码分析,所以造成内容较多且比较杂乱,重点部分我会用红字或者加粗字体标出来,如果没有耐心全部看完的话可以挑部分重点看,可以跳过的部分我也会有说明,大家可以选择性阅读。

其实框架的实现和我们教程(一)(二)章的也差不多,只不过在此基础上进行了一些优化,核心部分是几乎没有区别的。由于篇幅有限,暂时只介绍第一个框架(其他其实也都差不多),另外两个在后续教程中详细介绍


首先介绍universal-image-loader(以下简称UIL) 是github社区上star最多的一个项目,可以理解为点赞最多滴,应该是最有名的一个~国内很多知名软件都用它包括淘宝京东聚划算等等框架其实都差不多,这里详细介绍下使用者最多的一个~之后再分析其他框架时就简单说明一些特性了,重复相似部分不再赘述了~

使用比较简单,这个框架的github主页上也有快速使用的步骤。 基本上就是在application类里的oncreate方法(整个程序开始时运行一次)中进行一下简单的基本配置,可以根据需要自行进行设定,懒得设定的话框架也提供了一个默认的配置,调用一个方法即可。 基本上是配置一些类似于:缓存类型啊,缓存上限值啊,加载图片的线程池数量啊等等

此外在页面内显示的时候还要设置一个显示配置这个配置不同于基本配置,一个项目里可以根据需要创建多个配置对象使用。这个配置就比较具体了,可以设置是否使用disk缓存(存到sd卡里一般),加载图片失败时显示的图片,默认图片,图片的色彩样式等。

配置好以后,就是简单的使用了,创建一个图片加载对象,然后一行代码搞定显示图片功能~参数一般是入你需要显示的图片url和imageview对象大部分框架其实都这一个尿性,配置稍微麻烦点,但是使用时一般只需要一行,显示方法一般会提供多个重载方法,支持不同需要~由于不是框架使用教程,所以~下面结合之前两章的内容着重分析下框架对于单张图片的压缩处理,和多图缓存池的处理


单张图片的压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected Options prepareDecodingOptions(ImageSize imageSize, ImageDecodingInfo decodingInfo) {
      ImageScaleType scaleType = decodingInfo.getImageScaleType();
      int scale;
      if (scaleType == ImageScaleType. NONE) {
            scale = ImageSizeUtils. computeMinImageSampleSize(imageSize);
      } else {
            ImageSize targetSize = decodingInfo.getTargetSize();
             boolean powerOf2 = scaleType == ImageScaleType.IN_SAMPLE_POWER_OF_2 ;
            scale = ImageSizeUtils. computeImageSampleSize(imageSize, targetSize, decodingInfo.getViewScaleType(), powerOf2);
      }
      if (scale > 1 && loggingEnabled) {
            L. d(LOG_SUBSAMPLE_IMAGE, imageSize, imageSize.scaleDown(scale), scale, decodingInfo.getImageKey());
      }

      Options decodingOptions = decodingInfo.getDecodingOptions();
      decodingOptions. inSampleSize = scale;
      return decodingOptions;
}

简单扫一眼,ImageSize,ImageDecodingInfo神马的明显是自定义的一个类,不要管,我们先挑重点部分看

1
2
Options decodingOptions = decodingInfo.getDecodingOptions();
decodingOptions.inSampleSize = scale;

方法最后两行可以看出来ImageDecodingInfo类里面保存了一个option对象,通过一个方法对其中的inSampleSize进行了设置~

ImageScaleType.NONE 什么意思,扫了眼注释,是图片无压缩~那我们看else里面的需要压缩的computeImageSampleSize方法方法是具体如何处理的呢~ 我们再继续跟踪computeImageSampleSize方法~

方法的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
 * Computes sample size for downscaling image size (<b> srcSize</b> ) to view size (<b>targetSize</b> ). This sample
 * size is used during
 * {@linkplain BitmapFactory#decodeStream(java.io.InputStream, android.graphics.Rect, android.graphics.BitmapFactory.Options)
 * decoding image} to bitmap.<br />
 * <br />
 * <b>Examples: </b><br />
 * <p/>
 * <pre>
 * srcSize(100x100), targetSize(10x10), powerOf2Scale = true -> sampleSize = 8
 * srcSize(100x100), targetSize(10x10), powerOf2Scale = false -> sampleSize = 10
 *
 * srcSize(100x100), targetSize(20x40), viewScaleType = FIT_INSIDE -> sampleSize = 5
 * srcSize(100x100), targetSize(20x40), viewScaleType = CROP       -> sampleSize = 2
 * </pre>
 * <p/>
 * <br />
 * The sample size is the number of pixels in either dimension that correspond to a single pixel in the decoded
 * bitmap. For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, and 1/16
 * the number of pixels. Any value <= 1 is treated the same as 1.
 *
 * @param srcSize       Original (image) size
 * @param targetSize    Target (view) size
 * @param viewScaleType {@linkplain ViewScaleType Scale type} for placing image in view
 * @param powerOf2Scale <i>true </i> - if sample size be a power of 2 (1, 2, 4, 8, ...)
 * @return Computed sample size
 */
public static int computeImageSampleSize(ImageSize srcSize, ImageSize targetSize, ViewScaleType viewScaleType,
             boolean powerOf2Scale) {
      int srcWidth = srcSize.getWidth();
      int srcHeight = srcSize.getHeight();
      int targetWidth = targetSize .getWidth();
      int targetHeight = targetSize .getHeight();

      int scale = 1;

      int widthScale = srcWidth / targetWidth;
      int heightScale = srcHeight / targetHeight;

      switch (viewScaleType) {
             case FIT_INSIDE:
                   if (powerOf2Scale) {
                         while (srcWidth / 2 >= targetWidth || srcHeight / 2 >= targetHeight) { // ||
                              srcWidth /= 2;
                              srcHeight /= 2;
                              scale *= 2;
                        }
                  } else {
                        scale = Math. max(widthScale, heightScale); // max
                  }
                   break;
             case CROP:
                   if (powerOf2Scale) {
                         while (srcWidth / 2 >= targetWidth && srcHeight / 2 >= targetHeight) { // &&
                              srcWidth /= 2;
                              srcHeight /= 2;
                              scale *= 2;
                        }
                  } else {
                        scale = Math. min(widthScale, heightScale); // min
                  }
                   break;
      }

      if (scale < 1) {
            scale = 1;
      }

      return scale;
}

我连注释一起复制过来了,里面对参数有比较详细的介绍,还有对应的例子~很良心的注释啊~ 可以看到核心计算方法,其实和我们教程(一)里面是一样的(区别在于这里有两个情况的处理,一个是||一个是&&,后面会介绍原因),对源图的宽高和目标图片的宽高进行一些判断,满足时一直循环下去进行处理,直到取得一个合适的比例值为止,最终保证「使压缩后显示的图片像素密度是大于等于设定的像素密度的那个最低值」

(这里换了个更合适的说法,因为框架对图片不同scaleType造成的不同显示效果进行的区别处理,使得压缩比例值计算的更加精确,其实不用这个优化处理已经能够满足图片缩放显示需求了,这里是为了做到更好,当然也会更麻烦了,所以作为我们学习者来说要自行取舍要研究到哪个深度更合适,后面有单独一部分进行介绍,可以选择性看)

框架进行了优化,添加了一个powerOf2Scale的参数和viewScaleType的参数区别不同情况以进行响应处理,方法注释里面都有介绍参数的作用,我这里也简单说明下

  • powerOf2Scale: 是否为2的幂数,也就是2.4.8.16…..
    • true 即我们之前教程里面举得例子,也是官方推荐的,最好缩放比例是2的幂数(2的N次方),需要通过循环每次*2的计算压缩比例
    • false 即不需要是2的幂数,可以是1.2.3.4…任何一个数,直接用图片宽高除以所需图片宽高即可获得压缩比例(注意要是整数)所以,true的时候和我们教程(一)中的算法一致,而false的时候不做限定,那就可以简单的直接进行除法操作了
  • viewScaleType: 就是android里控件imageView的scaleType属性,这里集合成了两种,对应关系见框架的源码,如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    public static ViewScaleType fromImageView(ImageView imageView) {
        switch (imageView.getScaleType()) {
               case FIT_CENTER :
               case FIT_XY :
               case FIT_START :
               case FIT_END :
               case CENTER_INSIDE :
                     return FIT_INSIDE ;
               case MATRIX :
               case CENTER :
               case CENTER_CROP :
               default:
                     return CROP ;
        }
    }
    

再次重复 第一章的计算方法掌握了就可以了,已经能够满足开发需求,下面这段是对于不同viewScaleType的处理分析,可以跳过,有兴趣的话就接着看看


本段可略过~ 这部分内容还是很绕的,我也是边研究边写,实例demo也一边跑一边debug看研究,基本上算是大概了解了。 这一段反复修改了多次,耗费了大量精力,脑子有点迷乱了都,所以可能有不太准确的地方希望大家提醒下,我再做修改~

根据他的两种不同算法(||和&&),我们举个具体例子说明下 比如我们限定图片要200 200~ 原图是1000 600~ 控件则是100 100的正方形(单位都是像素)

先看默认的FIT_INSIDE效果

  • 第一次循环,1000/2 >= 200   600/2 >= 200 都满足, 源宽高变成一半 500 400,缩放值翻倍,变成2~
  • 第二次循环,500/2 >= 200   300/2 >= 200 满足其中一个, 源宽高变成一半 250 200,缩放值翻倍,变成4~
  • 第三次循环,250/2 >= 200 || 150/2 >= 200 都不满足,结束循环,最后缩放值为 4 以缩放比例4计算,获得的缩放图片实例的宽高即为250 150

CROP效果时的处理

  • 第一次循环,1000/2 >= 200 && 600/2 >= 200 都满足, 源宽高变成一半 500 400,缩放值翻倍,变成2~
  • 第二次循环,500/2 >= 200 && 300/2 >= 200 只满足其中一个,没有满足&&的条件,结束循环,最后缩放值是2~ 以缩放比例2计算,获得的缩放图片实例的宽高即为500 300

这样看的话,250 150的结果没有满足宽高都大于等于限定值200 200的原则啊~ 思考下压缩图片的原则,首先尽可能的缩小,因为越小越节约内存,但是不能太小,因为压缩过大的图不够清楚,效果不好~跟看骑兵电影似得,所以要有个最低值~ 比如我的imageview控件的宽高是100 100大小的一个正方形,那我希望将显示的图片压缩成至少为200 200像素的大小,这样我控件的单位大小上都可以显示至少4个像素的图片点~

所以限定大小,实际上是为了限定控件上图片的像素密度,也就是控件单位大小上图片像素点的数量~ 对于控件正方形,图片几乎也是正方形的情况,那就简单了 比如一个500 500的图片和一个200 200的图片都放在100 100的imageview上~ 外观看上去的大小都是一样的,但是单位大小上的图片像素点数量是不同的,500x500=250000,分布在100x100的控件上,那控件单位大小上就有25个图片像素点~200 200的图片,图片像素密度则是4~ 肯定是单位大小上图片像素点越多就越清楚了

为什么要有区分处理呢,因为对于图片长宽比例和控件长宽比例相差很大时,不同的scaleType造成的显示缩放效果区别是很大的~ 在长宽比相同或者相近时,比如都是正方形,那么两种显示效果毫无区别,但是如果控件是正方形,图片是3:2甚至3:1的矩形,那差别就明显了。FIT_INSIDE的显示不全,组件面积>图片显示。而CROP_INSIDE的组件面积<图片实际显示面积。举个极端例子,如果有一个10:1的长图,还是按普通FIT的处理方法压缩图片,但组件实际是CROP的模式显示,最后就会把图片压缩的过大,显示效果就会很模糊了。

所以图片限定宽高大小,是为了保证图片的清晰度,实质上是要保证单位大小上有足够的像素点,即对像素密度有个最低值要求根据长边缩放的FIT_INSIDE效果,我们只需要保证长的边即其中一个大于等于限定数值即可而根据短边缩放的CROP效果,要保证短的边大于等于限定数值即只有宽高都满足大于等于限定的数时才可以由于大部分情况下,尤其是实际应用场景中,什么头像啊,logo啊,图片啊大部分其实都是控件和图片长宽比相似的,偶尔有几个奇葩图片也浪费不来多少内存的,所以一般没有做区分处理~UIL框架这种区别计算比例更加的准确,当然,同时也更加难了


图片色彩样式 缩放比例的控制介绍完毕,基本上和我们教程(一)中介绍的方法差不多(没羞没臊啊), 只不过进行了一些优化而对于图片色彩样式的控制,则可以在框架提供的显示配置对象中设置 DisplayImageOptions.bitmapConfig(Bitmap.Config.RGB_565) 传入所需色样即可,默认同样是ARGB_8888


多张图片的缓存池

单张图片的缩放处理其实还是比较简单的(不考虑不同缩放效果区别处理的话)~重点在于多张图片的缓存池控制,下面进行介绍

首先是UIL的内存缓存 一共有八种内存缓存池类型,使用时只要选择其中的一个即可(通过ImageLoaderConfiguration.memoryCache(…)设置) 我们看下UIL在github上的主页中对于各个内存缓存类型的具体介绍(主页地址见文章最开始处)

  • 只用了强引用 是现在官方推荐的方法,相当于我们教程(二)里面说的只用LruCache一个强引用缓存池的方式~也是框架的默认内存缓存方式
    • LruMemoryCache 当超过缓存数量的极限值时删除使用时间离得最远的图片(即LruCache的算法)
  • 同时使用弱引用+强引用 也就是相当于教程(二)里面说的二级缓存技术,区别在于这里对其中的一级缓存即强引用缓存池部分的删除算法进行了细分,采用了不同的规则,看下小括号后面的英文就知道了,我这里简单的翻译下
    • UsingFregLimitedMemoryCache 当超过缓存数量的极限值时删除最不常用的图片
    • LruLimitedMemoryCache 当超过缓存数量的极限值时删除使用时间离得最远的图片(即LruCache的算法)
    • FIFOLimitedMemoryCache 当超过缓存数量的极限值时根据FIFO first in first out 先入先出算法删除
    • LargestLimitedMemoryCache 当超过缓存数量的极限值时删除最大的图片
    • LimitedAgeMemoryCache 当超过缓存数量的极限值时删除超过存在最长时间的那个图片(这个翻译有可能不太准确= = )
  • 只使用弱引用 由于完全没有使用强引用,所以肯定不会出现OOM异常,但是效率上捉鸡~ UIL使用时基本上很少出现OOM异常的,真是人品爆发出现了,那解决办法之一就是将内存缓存设置成这个只用弱引用的类型,因为没有强引用部分~
    • WeakmemoryCache 无限制的内存(系统根据弱引用规则自动回收图片)

估计是框架很早以前就开始开发的问题,强引用部分用的是LinkedHashMap类,没有采用LruCache类,但实际上处理逻辑基本都是一样的~由于LinkedHashMap的功能稍微弱一点~教程二里面可以看到我们是通过size()>阀值判断的,也就是仅根据数量而不是根据所有图片内存总大小,所以UIL框架中自己做了对应处理,在这个自定义个的强引用类型LruMemoryCache中设置了一个总内存size参数,每次put remove等操作时,都计算size的变化,并且在每次put操作时调用一个trimToSize方法,用于判断添加后的缓存池大小,观察size值是否超过了maxSize阀值,超过自定义的内存总大小值时则移除最老的图片 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package com.nostra13.universalimageloader.cache.memory.impl;

import android.graphics.Bitmap;

import com.nostra13.universalimageloader.cache.memory.MemoryCache;

import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* A cache that holds strong references to a limited number of Bitmaps. Each time a Bitmap is accessed, it is moved to
* the head of a queue. When a Bitmap is added to a full cache, the Bitmap at the end of that queue is evicted and may
* become eligible for garbage collection.<br />
* <br />
* <b>NOTE:</b> This cache uses only strong references for stored Bitmaps.
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @since 1.8.1
*/
public class LruMemoryCache implements MemoryCache {

     private final LinkedHashMap<String, Bitmap> map;

     private final int maxSize;
     /** Size of this cache in bytes */
     private int size;

     /** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */
     public LruMemoryCache(int maxSize) {
          if (maxSize <= 0) {
               throw new IllegalArgumentException("maxSize <= 0");
          }
          this.maxSize = maxSize;
          this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
     }

     /**
     * Returns the Bitmap for {@code key} if it exists in the cache. If a Bitmap was returned, it is moved to the head
     * of the queue. This returns null if a Bitmap is not cached.
     */
     @Override
     public final Bitmap get(String key) {
          if (key == null) {
               throw new NullPointerException("key == null");
          }

          synchronized (this) {
               return map.get(key);
          }
     }

     /** Caches {@code Bitmap} for {@code key}. The Bitmap is moved to the head of the queue. */
     @Override
     public final boolean put(String key, Bitmap value) {
          if (key == null || value == null) {
               throw new NullPointerException("key == null || value == null");
          }

          synchronized (this) {
               size += sizeOf(key, value);
               Bitmap previous = map.put(key, value);
               if (previous != null) {
                    size -= sizeOf(key, previous);
               }
          }

          trimToSize(maxSize);
          return true;
     }

     /**
     * 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;
                    }

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

     /** Removes the entry for {@code key} if it exists. */
     @Override
     public final void remove(String key) {
          if (key == null) {
               throw new NullPointerException("key == null");
          }

          synchronized (this) {
               Bitmap previous = map.remove(key);
               if (previous != null) {
                    size -= sizeOf(key, previous);
               }
          }
     }

     @Override
     public Collection<String> keys() {
          synchronized (this) {
               return new HashSet<String>(map.keySet());
          }
     }

     @Override
     public void clear() {
          trimToSize(-1); // -1 will evict 0-sized elements
     }

     /**
     * Returns the size {@code Bitmap} in bytes.
     * <p/>
     * An entry's size must not change while it is in the cache.
     */
     private int sizeOf(String key, Bitmap value) {
          return value.getRowBytes() * value.getHeight();
     }

     @Override
     public synchronized final String toString() {
          return String.format("LruCache[maxSize=%d]", maxSize);
     }
}

代码分析 核心方法是 put 和 trimToSize

put中: synchronized修饰是为了防止多线程访问时造成size计算错误的。 sizeOf是自定义方法,计算图片size大小的,这里首先将添加的图片大小增至缓存池总大小size中。 然后map.put方法添加图片对象,返回值是如果map里对应key已经有值了,则返回put前已有的那个对象~当然,put以后value已经被替换了,所以在+=新的图片对象大小后,还要将前一个被替换掉的图片size从总缓存池内存大小中减去。 最后完成添加过程以后调用trimToSize方法查看添加操作完成后当前缓存池大小是否超过我们自定义的阀值总大小maxSize

trimToSize中: 无限循环直到当前缓存池大小size低于自定义阀值大小maxSize为止~ 如果没有满足size<=maxSize,即当前缓存池过大,则对map添加一个迭代器依次遍历整个map从始至末挨个删除,直到满足size<=maxSize结束循环~ 之所以用循环遍历,是因为可能不止删除一个图片,比如最早添加的图片只有5kb,而新图片有15kb,那只删除最早一张图片是不行的,可能添加进来一张需要挤出去多张~

每次put的时候hashMap都会将新对象添加至末端~key已存在时则替代对应的value对象并移到末端~而当迭代器遍历的时候则是从始端开始遍历的,也就等于个LRU的算法,删除使用时间距今最老的图片则UIL框架自定义的这个强引用LRU缓存类,主要还是对内存阀值size进行了处理和判断。

框架里的处理和LinkedHashMap的removeEldestEntry方法最终实现的效果都是一样的~只不过具体代码实现方式不同而已这里使用自定义方法然后进行处理,个人目测应该是为了支持框架多重缓存池类型而设计的,毕竟LinkedHashMap的removeEldestEntry仅提供删除最早使用对象的功能,而删除最大图片等这样其他的功能就不支持了~

以上是只用强引用的缓存池部分,还算简单,下面是二级缓存


二级缓存(弱+强) 这部分就凌乱了~由于UIL框架中有5个弱+强的具体实现类(详见上面的内存缓存介绍图),所以基本功能实现都是在基类中进行的处理,实现的子类中仅针对算法进行了自定义规则而已~ 由于代码涉及到多层多个类= = 本人的水平又有限,所以…就不仔细研究了,可能后续有时间的时候再补充到帖子里~大概思路是基类提供一个虚拟方法removeNext,返回bitmap值,然后在基类里put方法后判断强引用的size超过阀值时,对这个方法返回的bitmap对象删除操作,然后将其移至弱引用池里~ 具体移除规则则子类中自行编写,你还可以设计个按图片size单双数删除图片一类的特殊规则。

这就属于设计思想了,可以参考LinkedHashMap的removeEldestEntry源码,可以看到源码中只使用,而方法内则是空的,就是留给开发者自行继承重写逻辑的。 自己如若有兴趣开发框架,可以根据自己阶段性的水平来,开始的时候就直接用系统的LruCache写就可以了,二级缓存也很好处理~

注意: 我们教程里使用的是软引用,这里是弱引用~两者其实也差不多,弱引用生命周期比软引用还低点~但是效率其实都一般,官方都不推荐使用了。肯定是要跟着官方权威信息走的,所以UIL框架的默认内存缓存方式用的也是LruMemoryCache类, 但同时又保留了其他类型的缓存类, 框架使用者可以根据实际需要在配置时通过ImageLoaderConfiguration.memoryCache(…)方法设置其他提供的类型缓存。

关于软引用其他那几个类型算法FIFO等的具体实现,我这里就不研究了,算法神马的水平有限,大家有兴趣可以自己看看。

此外还有一个FuzzyKeyMemoryCache的内存缓存类, UIL中缓存里是支持保存同一张图片的多种缩放比例缓存对象的,设置中也可以取消这种设置使得缓存池中一个图片只保留一个size的缓存对象,通过调用 ImageLoader.denyCacheImageMultipleSizesInMemory()方法实现, 此时,UIL框架就会自动使用Fuzzy…去包装一下你之前选择的内存缓存类型~即算法还是按照你选择的类型,但是多了一个只保持一种size缓存对象的功能,这种设计叫做Decorator装饰模式,举个例子帮助理解下,这种设计模式相当于有一个变相怪杰的特殊面具,无论人或狗,谁带上谁就能具有相应的牛逼能力~算是一个特殊的内存缓存类型,不能单独使用


disk缓存 主要难点在于内存缓存,disk缓存其实比较简单,就是图片加载完成后把图片文件存到本地方便下次使用。

和内存缓存差不多,根据算法不同提供了几种类别,可以自行通过ImageLoaderConfiguration.discCache(..)设置

硬盘缓存,保存是以文件的形式。 框架提供了4种类型,具体算法规则不同,看名字我们大概也能知道对应意思

  • UnlimitedDiscCache 不限制缓存池大小,最快的一种disk缓存,默认硬盘缓存方式(比其他disk缓存方式快30%)
  • TotalSizeLimitedDiscCache 限制缓存池总size大小,超出时删除最早保存的图片缓存文件
  • FileCountLimitedDiscCache 限制缓存池总数量,超出时删除最早保存的图片缓存文件,缓存图片是相同大小时使用此disk缓存类型
  • LimitedAgeDiscCache 不限制缓存池的大小,只对文件保存时间做限制,如果图片文件超出定义时间时删除之

同样,具体算法不做讨论~ 图片缓存文件位置是优先保存在内存卡下的Android\data\应用包名\cache文件夹下的。 (无权限,没有装sd等无法保存至sd卡的情况时,则保存在手机内存data\data\应用包名\cache文件夹下) 文件缓存的保存位置是区分应用的,每个应用设置一个缓存文件夹,图片文件的名字也经过了md5编码处理


图片加载一些具体数值的设置

比较重要的比如限定图片宽高多少合适啊~缓存池大小限定多少啊等等~ UIL框架中是在基本配置对象中进行设置的,前面提到过,有一个默认设置ImageLoaderConfiguration.createDefault(this); 针对绝大部分情况都适用的,要修改一些常用的配置的话, 设置方法可以去github UIL主页下载(文章开头有地址,文章结尾处我也会加上附件的)压缩包,里面包括源代码以及示例代码,可以在示例demo中查看一些常用基本配置设置和显示配置的设置

这里只介绍几个重要的值设定

  • 图像压缩后的限定宽高值 不同的限定值设定,是有一个优先级的~ 我专门实验研究了下(actual measured width and height不是太清楚)优先级 memoryCacheExtraOpstion > width/height > maxWidth/maxHeight > divice screen 即由高到低依次遍历是否能获取到值,获得后作为限定宽高值解析压缩后的图片实例对象

  • 缓存池大小的设置~ 分强引用和弱引用,弱引用我们知道不用限制~主要是针对强引用部分缓存池的限制。教程(二)里面也提到过,主要分两种:限制图片总数量 和 限制全部图片所占内存大小对于强引用大小的限定,我们看下UIL框架的默认处理 ```java /**
  • Creates default implementation of {@link MemoryCache} - {@link LruMemoryCache}
  • Default cache size = 1/8 of available app memory. */ public static MemoryCache createMemoryCache (int memoryCacheSize) { if (memoryCacheSize == 0) { memoryCacheSize = ( int) (Runtime. getRuntime().maxMemory() / 8); } return new LruMemoryCache(memoryCacheSize); } ``` 获取程序可用内存的1/8作为缓存池大小,也是推荐设置,有需要可以调用上面的方法传入>0的自定义size值即可设置限制图片总数量,恕我眼神拙计= = 源码中没找到默认设置,毕竟设置内存大小更合适点。如果自己项目中有特殊需要,可以参考教程(二)中的相关部分进行设置。

  • disk缓存池 由于默认是无限制disk缓存类型,没有一些具体参数数值的设定了~还有图片下载线程数量问题,disk图片取名问题等等,不是核心部分~这里篇幅有限就不介绍了

以上,核心代码分析结束~都弄懂了的话,图片的处理已经算是小有所成了,好处嘛~ 一方面在使用框架的时候你可以更了解内部的机制,适当时候可以根据项目具体需要做相关继承重写进行修改(最好不要直接修改开源框架源码,保证其完整性) 另一方面更可以当作自己的一个优势点亮点,尤其在面试时多发挥发挥~比如介绍自己项目时,大部分网络数据交互的项目一般都是会有列表多图加载的情况的,你就可以就图片加载部分大谈特谈了~不过聊框架部分有风险,对于此方面知识匮乏的面试官来说, 有时候不明也不一定会觉历,会白白浪费口水~最好结合项目一边演示一边谈,具体技巧这里就不班门弄斧了,可以网上找些帖子看


写了大概将近一周,修改时间占了一大半,反反复复的改,也是因为个人技术有限,部分姿势也是边研究边写的,可能有不太详细或者错误的地方再次希望大家提出来,多多指正或者共同探讨一下,也希望大家能收藏一下,我之后也会继续润色或补充文章不足地方的

UIL功能强大是毋庸置疑的,代码框架也很清晰,文档也算齐全~ 但是对开发者尤其是我这样的初学者来说一点点啃下来还是很艰难的,最好先看教程一二,完全懂了以后再看本篇,当然一二看懂基本上图片处理也差不多了

本篇相当于对之前教程做个验证 “你看,最有名的框架基本上也是这么处理嘛~”如此这般从侧面证实一下教程中方法的靠谱性~其他作用呢? 通过钻研肯定是提高我们的技术了,学习别人的类结构设计~ 也能更好的使用图片框架(其他框架差不多都这样逻辑),且对于拓展部分也可以作为自己的亮点在面试中使用