Android Bitmap 全面解析

整理自帖子:http://www.eoeandroid.com/thread-331669-1-1.html


(一) 加载大尺寸图片

压缩原因:

1.imageview大小如果是200*300那么加载个2000*3000的图片到内存中显然是浪费可耻滴行为;
2.最重要的是图片过大时直接加载原图会造成OOM异常(out of memory内存溢出)


所以一般对于大图我们需要进行下压缩处理
权威处理方法参考 安卓开发者中心的大图片处理教程
http://developer.android.com/training/displaying-bitmaps/load-bitmap.html

主要处理思路是:
1.获取图片的像素宽高(不加载图片至内存中,所以不会占用资源)
2.计算需要压缩的比例
3.按将图片用计算出的比例压缩,并加载至内存中使用


官网大图片加载教程(上面网址里的)对应代码就是:
/**
* 获取压缩后的图片
* @param res
* @param resId
* @param reqWidth            所需图片压缩尺寸最小宽度
* @param reqHeight           所需图片压缩尺寸最小高度
* @return
*/
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {
   
    // 首先不加载图片,仅获取图片尺寸
    final BitmapFactory.Options options = new BitmapFactory.Options();
    // 当inJustDecodeBounds设为true时,不会加载图片仅获取图片尺寸信息
    options.inJustDecodeBounds = true;
    // 此时仅会将图片信息会保存至options对象内,decode方法不会返回bitmap对象
    BitmapFactory.decodeResource(res, resId, options);

    // 计算压缩比例,如inSampleSize=4时,图片会压缩成原图的1/4
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // 当inJustDecodeBounds设为false时,BitmapFactory.decode...就会返回图片对象了
    options. inJustDecodeBounds = false;
    // 利用计算的比例值获取压缩后的图片对象
    return BitmapFactory.decodeResource(res, resId, options);
}
代码详解:
核心方法是BitmapFactory.decode...(...., options)
...的意思是此外还有一系列的decodeFile/decodeStream等等方法,都是利用options灵活解析获取图片,只不过解析图片的来源不同罢了,比如网络图片获取,一般就是解析字节流信息然后decode获取图片实例

Options是图片配置信息,参数详细介绍下:

inJustDecodeBounds 是否只解析边界
     设为true时去decode获取图片,只会加载像素宽高信息
     设为false时decode则会完全加载图片

inSampleSize 压缩比例
     比如原图200*300,如果值是2时会压缩成100*150; 是4则图片压缩成50*75
     最好是2的幂数,比如2 4 8 16 .....

outHeight  图片原高度
outWidth  图片原宽度

其他参数自行研究,这里暂时只用到这几个


decodeSampledBitmapFromResource方法内的三段代码对应上面的三步流程。难点在于中间那步,压缩比例的计算,官网同样提供了个calculateInSampleSize方法,其中reqWidth和reqHeight是所需图片限定最小宽高值
/**
* 计算压缩比例值
* @param options       解析图片的配置信息
* @param reqWidth            所需图片压缩尺寸最小宽度
* @param reqHeight           所需图片压缩尺寸最小高度
* @return
*/
public static int calculateInSampleSize(BitmapFactory.Options options,
             int reqWidth, int reqHeight) {
       // 保存图片原宽高值
       final int height = options. outHeight;
       final int width = options. outWidth;
       // 初始化压缩比例为1
       int inSampleSize = 1;

       // 当图片宽高值任何一个大于所需压缩图片宽高值时,进入循环计算系统
       if (height > reqHeight || width > reqWidth) {

             final int halfHeight = height / 2;
             final int halfWidth = width / 2;

             // 压缩比例值每次循环两倍增加,
             // 直到原图宽高值的一半除以压缩值后都~大于所需宽高值为止
             while ((halfHeight / inSampleSize) >= reqHeight
                        && (halfWidth / inSampleSize) >= reqWidth) {
                  inSampleSize *= 2;
            }
      }

       return inSampleSize;
}
利用此方法获取到所需压缩比例值,最终获取到压缩后的图片~

官网的这个方法是: 将图片一半一半的压缩,直到压缩成成 大于所需宽高数的那个最低值。  大于~不是大于等于 ,所以就会出现我上面那种情况,我觉得方法不是太好= = 能满足压缩的需求,但是压缩的比例不够准确, 所以最好改成大于等于,如下(个人意见,仅供参考,在实际压缩中很少遇到恰巧等于的这个情况,所以>和>=差别也不大额~看我这扯扯淡就当对计算比例的逻辑加深个理解吧)

----------------------------------------------------------


以上,图片的像素大小已经做了缩放。但是图片的大小除了和像素有关。还和色彩样式有关。 不同的样式决定了图片单个像素占的字节数
比如。图片默认的色彩样式为ARGB_8888。每个像素占4byte(字节)大小
参考资料:http://developer.android.com/reference/android/graphics/Bitmap.Config.html

可以看到一共有四种色彩样式
ALPHA_8      每个像素只要1字节~可惜只能代表透明度,没有颜色属性
ARGB_4444    每个像素要2字节~带透明度的颜色~可惜官方不推荐使用了
ARGB_8888  每个像素要4字节~带透明度的颜色, 默认色样
RGB_565    每个像素要2字节~不带透明度的颜色
默认为ARGB_8888,如果想丧心病狂的继续减少图片所占大小。不需要透明度参数的话, 那就可以把色彩样式设为RGB_565

设置方法是在BitmapFactory.decode..获取图片事例时, 修改配置参数的inPreferredConfig 参数
opts. inPreferredConfig = Bitmap.Config. RGB_565 ;


----------------------------------------------------------

想亲自撸一撸试一试压缩图片了吧?
要注意点问题,如果用res包下图片测试的话,你会发现有图片尺寸有点混乱。那是因为在drawable-*dpi文件夹中的图片会根据对应的屏幕密度值不同自动进行一定的缩放,比如放在drawable-hdpi里的图片,直接不经过压缩BitmapFactor.decode..出来,会发现bitmap的宽高值是原图的2/3。测试的时候图片记得放在drawable包下(没有的话自己res下新建一个),否则你会被奇怪的宽高值弄凌乱的。具体变化原因参考源代码处理,或者网上搜搜看。

还有就是BitmapFactory.decodeStream方法会偶尔解析图片失败(好像是安卓低版本的一个bug)。
此时推荐做法是将流转换为字节流处理,然后利用decodeByteArray方法获取图片



(二)加载多张图片的缓存处理

一般少量图片是很少出现OOM异常的,除非单张图片过大。那么就可以用第一节里面的方法了。
通常应用场景是listview列表加载多张图片,为了提高效率一般要缓存一部分图片,这样方便再次查看时能快速显示,不用重新下载图片。但是手机内存是很有限的,当缓存的图片越来越多,即使单张图片不是很大,不过数量太多时仍然会出现OOM的情况了。

本篇则是讨论多张图片的处理问题

-----------------------------------------------------------------------

图片缓存的一般处理

1.建立一个图片缓存池,用于存放图片对应的bitmap对象。
2.在显示的时候,比如listview对应适配器的getView方法里进行加载图片的工作, 先从缓存池通过url的key值取,如果取到图片了直接显示,如果获取不到再建立异步线程去下载图片(下载好后同时保存至图片缓存池并显示)

但是缓存池不能无限大啊,不然就会异常了,所以通常我们要对缓存池进行一定控制。
需要有两个特性: 
     1.总大小有个限制,不然里面存放无限多的图片时会内存溢出OOM异常
     2.当大小达到上限后,再添加图片时,需要线程池能够智能化的回收移除池内一部分图片,这样才能保证新图片的显示保存


异步线程下载图片神马的简单,网上异步下载任务的代码一大堆,下载以后流数据直接decode成bitmap图片即可。
难点在与这个图片缓存池的设计,现在网上的实现主要有两种
1.软引用/弱引用
2.LruCache

-----------------------------------------------------------------------

拓展: java中4种引用分类

官方资料连接:http://developer.android.com/reference/java/lang/ref/Reference.html
强引用
     平常使用的基本都是强引用,除非主动释放(图片的回收,或者==null赋值为空等),否则会一直保存对象到内存溢出为止~
软引用    SoftReference
     在系统内存不够时,会自动释放部分软引用所指对象~
弱引用    WeakReference
     系统偶尔回收扫描时发现弱引用则释放对象,即和内存够不够的情况无关,完全看心情~
虚引用
     不用了解,其实我也不熟悉

框架基本都比较爱用这个软应用保存图片作为缓存池,这样在图片过多不足时,就会自动回收部分图片,防止OOM。但是有缺点,无法控制内存不足时会回收哪些图片,如果我只想回收一些不常用的,不要回收常用的图片呢?


于是引入了二级缓存的逻辑,即设置两个缓存池,一个强引用,一个软引用, 强引用保存常用图片,软应用保存其他图片。强引用因为不会自动释放对象,所以大小要进行一定限定,否则图片过多会异常, 比如控制里面只存放10张图片,然后每次往里面添加图片的时候,检查如果数量超过10张这个阀值,临界点值时,就移除强引用里面最不常用的那个图片,并将其保存至软应用缓存池中。


整个缓存既作为一个整体(一级二级缓存都是内存缓存~每次显示图片前都要检查整个缓存池中有没有图片)。又有一定的区分(只回收二级缓存软引用中图片,不回收一级缓存中强引用的图片)

 

代码实现
软应用缓存池类型作为二级缓存: 

<span style="font-family:Microsoft YaHei;">HashMap<String, SoftReference<Bitmap>> mSecondLevelCache = new HashMap<String, SoftReference<Bitmap>>();</span>


强引用作为一级缓存,为了实现删除最不常用对象,可以用LinkedHashMap<String,Bitmap>类。LinkedHashMap对象可以复写一个removeEldestEntry,这个方法就是用来处理删除最不常用对象逻辑的,按照之前的设计就可以这么写:

<span style="font-family:Microsoft YaHei;">    final int MAX_CAPACITY = 10; // 一级缓存阈值

    // 第一个参数是初始化大小
    // 第二个参数0.75是加载因子为经验值
    // 第三个参数true则表示按照最近访问量的高低排序,false则表示按照插入顺序排序
    HashMap<String, Bitmap> mFirstLevelCache = new LinkedHashMap<String, Bitmap>(
            MAX_CAPACITY / 2, 0.75f, true) {
        // eldest 最老的对象,即移除的最不常用图片对象
        // 返回值 true即移除该对象,false则是不移除
        protected boolean removeEldestEntry(Entry<String, Bitmap> eldest) {
            if (size() > MAX_CAPACITY) {// 当缓存池总大小超过阈值的时候,将老的值从一级缓存搬到二级缓存
                mSecondLevelCache.put(eldest.getKey(),
                        new SoftReference<Bitmap>(eldest.getValue()));
                return true;
            }
            return false;
        }
    };</span>

每次图片显示时即使用时,如果存在与缓存中,则先将对象从缓存中删除,然后重新添加到一级缓存中的最前端。
会有三种情况
1.如果图片是从一级缓存中取出来的,则相当于把对象移到了一级缓存池的最前端(相当于最近使用的一张图片)
2.如果图片是从二级缓存中取出来的,则会存到一级缓存池最前端并检测,如果超过阀值,则将最不常用的一个对象移动到二级缓存中
3.如果缓存中没有,那就网上下载图片,下载好以后保存至一级缓存中,同样再进行检测是否要移除一个对象至二级缓存中

-----------------------------------------------------------------------

Disk缓存

可以简单的理解为将图片缓存到sd卡中。

由于内存缓存在程序关闭第二次进入时就清空了,对于一个十分常用的图片比如头像一类的,我们希望不要每次进入应用都重新下载一遍,那就要用到disk缓存了,直接图片存到了本地,打开应用时直接获取显示。

网上获取图片的大部分逻辑顺序是:
内存缓存中获取显示(强引用缓存池->弱引用缓存池)  -> 内存中找不到时从sd卡缓存中获取显示 -> 缓存中都没有再建立异步线程下载图片,下载完成后保存至缓存中。

按照获取图片获取效率的速度,由快到慢的依次尝试几个方法

以文件的形式缓存到SD卡中,优点是SD卡容量较大,所以可以缓存很多图片,且多次打开应用都可以使用缓存,缺点是文件读写操作会耗费一点时间。虽然速度没有从内存缓存中获取速度快,但是肯定比重新下载一张图片的速度快,而且还不用每次都下载图片浪费流量。所以使用优先级就介于内存缓存和下载图片之间了。

注意:
sd卡缓存一般要提前进行一下是否装载sd卡的检测, 还要检测sd卡剩余容量是否够用的情况,程序里也要添加注明相应的权限

-----------------------------------------------------------------------

使用LruCache处理图片缓存


以上基本完全掌握了,每一张图最好再进行一下第一节里面介绍的单张缩放处理,那基本整个图片缓存技术就差不多了。但随着android sdk的更新,新版本其实提供了更好的解决方案,下面介绍一下

先看老版本用的软引用官方文档
http://developer.android.com/reference/java/lang/ref/SoftReference.html


摘取段对软引用的介绍

Avoid Soft References for Caching

In practice, soft references areinefficient for caching. The runtime doesn't have enough information on whichreferences to clear and which to keep. Most fatally, it doesn't knowwhat to do when given the choice between clearing a soft reference and growingthe heap.
The lack of information on the value to your application of each referencelimits the usefulness of soft references. References that are cleared too earlycause unnecessary work; those that are cleared too late waste memory.

Most applications should use an android.util.LruCache instead of soft references.LruCache has an effective eviction policy and lets the user tune how muchmemory is allotted.



简单翻译一下
我们要避免用软引用去处理缓存 。

在实践中,软引用在缓存的处理上是没有效率的。运行时没有足够的信息用于判断哪些引用要清理回收还有哪些要保存。最致命的,当同时面对清理软引用和增加堆内存两种选择时它不知道做什么。对于你应用的每一个引用都缺乏有价值的信息,这一点限制了软引用让它的可用性十分有限。过早清理回收的引用导致了无必要的工作; 而那些过晚清理掉的引用又浪费了内存。

大多数应用程序应该使用一个android.util.LruCache代替软引用。LruCache有一个有效的回收机制,让用户能够调整有多少内存分配。

简而言之,直接使用软引用缓存的话效果不咋滴,推荐使用LruCache。level12以后开始引入的,为了兼容更早版本,android-support-v4包内也添加了一个LruCache类,所以在12版本以上用的话发现有两个包内都有这个类,其实都是一样的。

那么这个类是做啥的呢~这里是官方文档
http://developer.android.com/reference/android/util/LruCache.html

LRU的意思是LeastRecently Used 即近期最少使用算法。眼熟吧,其实之前的二级缓存中的那个强引用LinkedHashMap的处理逻辑其实就是一个LRU算法,而我们查看LruCache这个类的源代码时发现里面其实也有一个LinkedHashMap,大概扫了眼,逻辑和我们之前自己写的差不多。核心功能基本都是: 当添加进去新数据且达到限制的阀值时,则移除一个最少使用的数据。

根据这个新的类做图片加载的话,网上大部分的做法还是二级缓存处理。只不过将LinkedHashMap+软引用,替换成了LruCache+软引用。都是二级缓存,强引用+软引用的结构。因为LruCache和LinkedHashMap都是差不多的处理逻辑,没有移除软引用的使用,而是将两者结合了起来。

根据官网的介绍来看其实软引用效果不大,二级缓存的处理的话,虽然能提高一点效果,但是会浪费对内存的消耗,所以要不要加个软引用的二级缓存,具体选择就看自己理解和实际应用场景了吧。

LruCache我理解是牺牲一小部分效率,换取部分内存。我个人也是倾向于只使用LruCache的实现不用软引用了,也比较简单。

-----------------------------------------------------------------------

LruCache的具体用法


之前对LinkedHashMap有了一定了解了,其实LruCache也差不多,类似于removeEldestEntry方法的回收逻辑,在这个类里面已经处理好了,一般我们只需要处理对阀值的控制就行了。

阀值控制的核心方法是sizeOf()方法, 该方法的意思是返回每一个value对象的大小size,默认返回的是1。即当maxSize(通过构造方法传入)设为10的时候就相当于限制缓存池只保留10个对象了,和上面LinkedHashMap的例子一个意思。

但是由于图片的大小不一,一般限定所有图片的总大小更加合适,那我们就可以对这个sizeOf方法进行复写
@Override
protected int sizeOf(String key, Bitmapvalue) {
     return value.getRowBytes() * value.getHeight();
}

这样的话,相当于缓存池里每一个对象的大小都是计算它的字节数,则在新建LruCache的时候传入一个总size值就行了,一般传入应用可用内存的1/8大小

-----------------------------------------------------------------------

本篇是讨论对于图片数量的控制问题, 再结合第一节中的方法对每一张图片进行相应处理~OOM的情况基本就可以避免了~


-----------------------------------------------------------------------

(三)开源图片框架分析1-UIL

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

Android-Universal-Image-Loader
https://github.com/nostra13/Android-Universal-Image-Loader

ImageLoader
https://github.com/novoda/ImageLoader

Volley(综合框架,包含图片部分)
https://github.com/mcxiaoke/android-volley


扯淡时间,可以跳过这段。这些开源框架的源码还是挺复杂的,本人技术有限,有部分分析不对的地方或者不足的地方希望大家一起讨论,由于有大量的源代码分析,所以造成内容较多且比较杂乱,重点部分我会用红字或者加粗字体标出来,如果没有耐心全部看完的话可以挑部分重点看,可以跳过的部分我也会有说明,大家可以选择性阅读。其实框架的实现和我们教程(一)(二)节的也差不多,只不过在此基础上进行了一些优化,核心部分是几乎没有区别的。

由于篇幅有限,暂时只介绍第一个框架(其他其实也都差不多),另外两个在后续教程中详细介绍

---------------------------------------------------------------------


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

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

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

配置好以后,就是简单的使用了,创建一个图片加载对象,然后一行代码搞定显示图片功能。参数一般是入你需要显示的图片url和imageview对象。大部分框架其实都这一个尿性,配置稍微麻烦点,但是使用时一般只需要一行,显示方法一般会提供多个重载方法,支持不同需要。

由于不是框架使用教程,所以下面结合之前两章的内容着重分析下框架对于单张图片的压缩处理,和多图缓存池的处理
---------------------------------------------------------------------

单张图片的压缩

(业界良心的小技巧: 框架肯定也是基于android sdk的, 所以获取图片缩放实例的话,option的inSampleSize参数是肯定要使用的, 我们直接crtl+h打开搜索页面,选择file search, 然后file name patterns选择*.java,即搜索所有java文件,最后在containing text上输入想搜索的内容,这里我们要搜inSampleSize,搜索结果里随便扫一扫,发现BaseImageDecoder里面有个靠谱的方法如下)

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神马的明显是自定义的一个类,不要管,我们先挑重点部分看

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


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

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


(业界良心小技巧:按着ctrl不松左键点击方法或者变量或者类,就可以自动跳转到对应的地方了)

方法的代码如下

   /**
     * 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属性,这里集合成了两种,对应关系见框架的源码,如下

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上~外观看上去的大小都是一样的,但是单位大小上的图片像素点数量是不同的,500*500=250000,分布在100*100的控件上,那控件单位大小上就有25个图片像素点~
200 200的图片,图片像素密度则是4~ 肯定是单位大小上图片像素点越多就越清楚了




为什么要有区分处理呢,因为对于图片长宽比例和控件长宽比例相差很大时,不同的scaleType造成的显示缩放效果区别是很大的~
在长宽比相同或者相近时,比如都是正方形,那么两种显示效果毫无区别,但是如果控件是正方形,图片是3:2甚至3:1的矩形,那差别就明显了, 我们看下比例值差别较大时的
FIT_INSIDE的显示效果和CROP_INSIDE


两种显示效果简单的理解为
     FIT_INSIDE(左图)   完整的展示图片,但imageView控件可能不占满
     CROP(右图)          填充整个imageview,图片可能会被裁去一部分


对于上面的例子1000 600的图, 而图片限定大小是200200
如果两种效果都是用一种压缩规则算法,比如官方提供的那种规则(也是我们教程一里面的),则会把图片压缩成了500 300~
分析下压缩后图片在两种scaleType下的缩放情况


FIT_INSIDE模式时
显示时候的缩放(不是图片像素的压缩)是根据较长的那个边缩放的(蓝色框框的上下两条长边),
500缩放成了100,缩放比例为5,那像素密度就是25


CROP模式时
显示时候的缩放是根据较短的那个段边缩放的(蓝色框框的左右俩短边)
上例中是短边是300,缩放成了100,比例为3,那单位大小上图片像素点就是9


预期大小是200 200,显示在100 100的控件上其实就相当于希望单位大小上的图片像素数量4以上




那个25貌似是有点多了呢~
那我们按照UIL框架里针对FIT_INSIDE的算法重新计算下,压缩后图片像素为250 150
缩放类型按照长边缩放,比例为2.5~单位大小上图片像素点是12.5~明显满足条件又更加合适~


那CROP缩放类型也按照UIL框架FIT_INSIDE对应的算法处理呢?
算出来是250 150,显示在CROP上,则是按照短边缩放,比例是1.5~最后像素密度是2.25,不到4,明显是不满足的了~




所以图片限定宽高大小,是为了保证图片的清晰度,实质上是要保证单位大小上有足够的像素点,即对像素密度有个最低值要求
根据长边缩放的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阀值,超过自定义的内存总大小值时则移除最老的图片


代码如下

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框架的默认处理

/**
 * Creates default implementation of {@link MemoryCache} -
 * {@link LruMemoryCache}<br />
 * 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框架的一些扩展功能,以及源码的结


--------------------------------------------------------------------------------------------------------


UIL框架在核心的图片缓存技术,图片压缩,异步线程下载图片的传统框架功能以外~
还做了一些其他的
拓展功能
自定义ImageView控件,叫做ImageAware,支持圆角效果等功能
disk保存图片文件名md5编码
图片加载的显示效果,比如淡入的fade效果等等
(此外还有一些扩展的功能,由于是非核心部分不做详细介绍了)
此外还提供Widget~ 这个我就没研究了


源码结构
主要分三大包~下面挨个介绍


UIL的缓存池部分包/类结构
见下图,很清楚的结构,cache缓存包下分成两个部分,disc(disk缓存)包和memory(内存缓存)包
绿色部分是基类父类~ impl是具体实现子类,比如memory的impl包下的那8个,就是我们之前介绍的可以直接使用的不同缓存池类型,项目中有特殊需求的话,也可以自定义一个类继承重写做对应的修改
disk同理,基类+具体实现类,在此基础上多了个naming命名包,是对图片保存文件名称进行md5编码解码处理的

 







核心包
其中着重研究红框部分
    图片解析包decode
    基本配置类ImageLoaderCofiguration
    显示配置类DisplayImageOptions
    和最重要的图片加载器类ImageLoder
其他部分
assist为助手包,里面大部分都是一些Enum枚举类,比如前面提到的图片缩放类型,图片压缩类型等
display图片显示包,框架提供圆角矩形的图片显示效果,以及加载图片时fade效果(从透明到实体慢慢浮现的感觉)等
download图片下载包,篇幅有限此功能这里不做分析
imageaware自定义图片控件包,提供圆角的效果等
listener监听包,图片开始加载,正在加载,加载完毕等监听
process进程包,可以继承里面的接口自行定义图片加载过程中的效果
其他的还有一些线程啊,默认配置文件生成类啊等等的,同样篇幅有限不做分析
 



工具包
提供一些所需功能,比如IO流处理,disk缓存的路径处理等
其中最重要的是红框所示的图片size工具类,比如前面提到的computeImageSampleSize计算缩放比例就是在这个类里
 




--------------------------------------------------------------------------------------------------------





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


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


本篇相当于对之前教程做个验证 "你看,最有名的框架基本上也是这么处理嘛~"如此这般从侧面证实一下教程中方法的靠谱性~

其他作用呢? 通过钻研肯定是提高我们的技术了,学习别人的类结构设计~ 也能更好的使用图片框架(其他框架差不多都这样逻辑),且对于拓展部分也可以作为自己的亮点在面试中使用 ~


(四)开源图片框架分析2-ImageLoader和Volley

懒得复制+调整格式了,地址:http://www.eoeandroid.com/thread-334315-1-1.html


(五)图片处理效果对比

对比对象: UIL Volley 官方教程中的方法(此系列教程一里介绍的,ImageLoader的处理方法和官方的差不多)

------------------------------------------------------------------------

首先 单张图片的压缩处理,也是分析重点
专门撸了一个小demo(结尾会放出下载连接)将对应计算方法copy了出来,然后计算了几十组数据,进行了对比
原图宽高都是一个10000以内的随机整数,限定大小是400 200,然后进行压缩处理,记录了10组数据如下
 

控件缩放类型FIT_INSIDE和CROP在教程三-1的UIL框架分析中有详细说明,这里再简单说下
FIT_INSIDE是默认控件显示类型,图片会完整的显示在控件内,不一定会填满控件
CROP是使图片填充整个控件,图片可能会显示不全
具体效果大家可以在android项目里建一个imageview不设置缩放类型(即FIT_INSIDE效果), 再建一个scaleType为CROP的同大小控件,然后显示同一张图片,就会看出来区别了


官方/volley/imageloader都没有对缩放类型做区别处理,所以不同缩放类型下,最终压缩图片是一个size
结果我们也可以明显看出
官方算法是和UIL的CROP类型时结果一样的,实质上作用是保证压缩后图片宽高 都要大于等于限定宽高,
Volley的那种对限定值的特殊处理方式,则实质上最终的效果是让压缩后的图片宽高 任意一个大于等于限定宽高即可,则对应UIL的FIT_INSIDE效果


哪种好呢?(只针对图片的压缩处理而言)
UIL我们已经长篇详细的分析过了(教程系列三-1),对于缩放类型的处理是十分准确的, 明显是最好的~(这么多人用不是没道理的)
那对比起来,官方的处理和Volley的则各有不足了~

官方=UIL CROP 即官方的处理方法其实更适合于CROP缩放类型的图片显示
然而在处理宽高比差别较大的图片时,如果是FIT_INSIDE显示模式,则会造成压缩图片略大,虽然能保证显示质量,但是浪费小部分内存资源
比如上图中的第一组数据,664 3640的压缩结果,明显和我们所需的400 200差别过大- - 脑补一下大概就是火柴盒跟西瓜刀吧(不是太准确~)

Volley=UIL FIT_INSIDE 则代表Volley更适合默认情况下的图片显示情况了
那么在处理宽高比差别过大的图片时,如果是CROP缩放类型,则压缩大小看起来是差不多了,但实际上显示效果是无法达到预期的,大小我们可以从size数值上看出来,显示效果我还是弄个实际图片大家对比看看吧(可以在文章末尾下载demo项目)


丧心病狂的找了个微博长图实验,项目一共四个ImageView控件,上面俩是Volley,下排是UIL,左边俩是FIT_INSIDE效果,右边则是CROP
而我说的Volley在过大宽高差时,用CROP类型显示无法达到预期质量的效果,就是右上角这个图了,简直就是我不戴眼镜看世界滴样子
对比下,UIL在处理CROP情况时效果十分有保障~
 

上面缺点都有个条件 宽高比差别过大,因为大部分情况下,原图的宽高比是比较稳定的,而我们限定值也都是差不多的,大部分情况都是设为一个正方形的限定大小,原图基本也都是接近正方形的矩形~这就解释了为什么大部分的图片框架包括官方教程中都没有专门对不同缩放类型做区分处理,因为一般情况下,即长宽比稳定差别不大的情况下,官方的那种简单处理都是适用滴
参考上面数据我们也可以得出,当原图长宽比和限定长宽比越相近,则两种不同算法(官方和UIL的CROP是一种,Volley和UIL的FIT_INSIDE是另一种)的区别越小,当原图和限定值的长宽比十分相似甚至相等时,那两种算法最终的结果就一样了(比如数据中红色加粗的部分)
而我们看长宽比差别较大的第一组数据和倒数第二组数据,两种算法的差别直接是2^4=16倍~大家有兴趣可以自行尝试,弄几个更大比例的特殊值试一试

------------------------------------------------------------------------

图片色彩样式对比
Volley写死了是RGB_565
ImageLoader没有设置,那应该就是默认的ARGB_8888
UIL提供设置API,默认则也是ARGB_8888

------------------------------------------------------------------------

缓存池对比
Volley只有接口...实例类都没有,不过写起来也很简单
ImageLoader则主要是单强引用,单软引用两种
UIL提供了8种类型,弱和强,单独以及混合的,包括不同的算法

区别就在于软引用和弱引用的不同,这个直接看教程二里面针对不同引用类型的介绍就行了,也可以自行百度搜,其实两者区别不大
强引用部分也都差不多,即使使用了最新的LruCache类,其实看源码我们也会发现内部还是用LinkedHashMap实现的

------------------------------------------------------------------------


综上,肯定是UIL完全胜出了,还有很多拓展功能部分也都是UIL更加强大没跑了
不过Volley框架我们之前也说过了,虽然功能不够完善,但十分适合我们二次开发
ImageLoader吗,稍微简单点,可以当学习源码的小练手来用


------------------------------------------------------------------------

最后滴最后,demo简单介绍下
drawable包下有一个长宽比很大的微博长图,还有一个正常的接近正方形的图,
代码只有一个类,里面把几种压缩算法都贴出来了,测试方法也已经写好,大家可以自行修改数值计算,也可以拷贝一些图片进去看其他比例图片的效果
布局文件对应也只有一个,里面放了4个imageView,俩普通的,俩center_crop的,也就是对应的CROP缩放类型,控件缩放类型和UIL框架缩放类型的对应关系,参考教程三-1的UIL介绍里


链接: http://pan.baidu.com/s/1eQswfcu 密码:na94



  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值