Android Bitmap 全面解析

原文路径:

http://www.eoeandroid.com/forum.php?mod=viewthread&tid=331669&page=1&authorid=785220&_dsign=af6370f6

 文档下载地址:http://download.csdn.net/detail/tiananma0607/9168333

Android Bitmap全面解析(一) 加载大尺寸图片

 

压缩原因:
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 BitmapdecodeSampledBitmapFromResource(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.Optionsoptions, 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;

}

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

 

以上代码能够看懂的话,下面这段可以跳过

逻辑是将原图宽高一半一半的缩减,一直减到宽高都小于自己设定的限定宽高时为止,测试的时候问题来了

原图400*300,我限定值200*150,if满足进入,while循环第一次,400/2/1=200不满足>的条件~结束循环,最终返回了个inSampleSize=1给我

当我将限定值稍微改一下变成195*145稍微降低一点点时~if满足进入,while循环第一次,400/2/1>195满足~然后压缩比例1*2变成了2,在下一次while循环时不满足条件结束,最后返回比例值2~ 满足压缩预期

官网的这个方法是: 将图片一半一半的压缩,直到压缩成成大于所需宽高数的那个最低值
大于~不是大于等于,所以就会出现我上面那种情况,我觉得方法不是太好,能满足压缩的需求,但是压缩的比例不够准确~
所以最好改成大于等于,如下:

while ((halfHeight / inSampleSize) >=reqHeight

         && (halfWidth / inSampleSize) >= reqWidth) {

    inSampleSize *= 2;

}


优化:
还是上面例子,如果限定了200*150,而原图是390*290会是个啥情况?
还是第一次while循环,390/2/1结果是195不满足>200的情况,结束循环,比例值为1,最后图片压缩成400*300,虽然压缩一次以后没有满足大于所需宽高,但是和所需宽高很接近啊!!!能不能做一个获取压缩成最接近所需宽高数的比例值呢?找了几个有名的图片加载开源框架发现也都没有这种处理- -不知道是这样设计是不需要呢,还是没啥用呢

以上,图片的像素大小已经做了缩放,但是图片的大小除了和像素有关,还和色彩样式有关,不同的样式决定了图片单个像素占的字节数,比如,图片默认的色彩样式为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包下,否则你会被奇怪的宽高值弄凌乱的

还有就是BitmapFactory.decodeStream方法会偶尔解析图片失败(好像是安卓低版本的一个bug)

此时推荐做法是将流转换为字节流处理,然后利用decodeByteArray方法获取图片

 

 

Android Bitmap全面解析(二)加载多张图片的缓存处理

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

图片缓存的一般处理
1.建立一个图片缓存池,用于存放图片对应的bitmap对象
2.在显示的时候,比如listview对应适配器的getView方法里进行加载图片的工作, 先从缓存池通过url的key值取,如果取到图片了直接显示,如果获取不到再建立异步线程去下载图片(下载好后同时保存至图片缓存池并显示)但是缓存池不能无限大啊~不然就会异常了,所以通常我们要对缓存池进行一定控制
需要有两个特性: 
     总大小有个限制,不然里面存放无限多的图片时会内存溢出OOM异常
     当大小达到上限后,再添加图片时,需要线程池能够智能化的回收移除池内一部分图片,这样才能保证新图片的显示保存

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

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

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

于是引入了二级缓存的逻辑
即设置两个缓存池,一个强引用,一个软引用, 强引用保存常用图片,软引用保存其他图片~
强引用因为不会自动释放对象,所以大小要进行一定限定,否则图片过多会异常, 比如控制里面只存放10张图片,然后每次往里面添加图片的时候,检查如果数量超过10张这个阀值,临界点值时,就移除强引用里面最不常用的那个图片,并将其保存至软引用缓存池中~整个缓存既作为一个整体(一级二级缓存都是内存缓存~每次显示图片前都要检查整个缓存池中有没有图片)又有一定的区分(只回收二级缓存软引用中图片,不回收一级缓存中强引用的图片~)
代码实现
软引用缓存池类型作为二级缓存: 
     HashMap<String,SoftReference<Bitmap>> mSecondLevelCache = new HashMap<String,SoftReference<Bitmap>>();
强引用作为一级缓存,为了实现删除最不常用对象,可以用 LinkedHashMap<String,Bitmap> 类
LinkedHashMap对象可以复写一个removeEldestEntry,这个方法就是用来处理删除最不常用对象逻辑的
按照之前的设计就可以这么写:
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; 
    } 
};  

每次图片显示时即使用时,如果存在于缓存中,则先将对象从缓存中删除,然后重新添加到一级缓存中的最前端
会有三种情况
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 are inefficient for caching. The runtime doesn'thave enough information on which references to clear and which to keep. Mostfatally, it doesn't know what to do when given the choice between clearing asoft reference and growing the 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 softreferences. LruCache has an effective eviction policy and lets the user tunehow much memory is allotted.简单翻译一下

我们要避免用软引用去处理缓存 

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

简而言之,直接使用软引用缓存的话效果不咋滴~推荐使用LruCache
level12以后开始引入的,为了兼容更早版本,android-support-v4包内也添加了一个LruCache类,所以在12版本以上用的话发现有两个包内都有这个类,其实都是一样的~那么这里是官方文档http://developer.android.com/reference/android/util/LruCache.html
LRU的意思是Least Recently 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, Bitmap value) {
     return value.getRowBytes() * value.getHeight();
}
这样的话,相当于缓存池里每一个对象的大小都是计算它的字节数,则在新建LruCache的时候传入一个总size值就行了,一般传入应用可用内存的1/8大小
-----------------------------------------------------------------------

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

 

AndroidBitmap全面解析(三)开源图片框架分析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, ImageDecodingInfodecodingInfo) {
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 correspondto a single pixel in the decoded
* bitmap. For example, inSampleSize == 4 returns an image that is 1/4 thewidth/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 imagein view
* @param powerOf2Scale <i>true </i> - if sample size be a power of2 (1, 2, 4, 8, ...)
* @return Computed sample size
*/
public static int computeImageSampleSize(ImageSize srcSize, ImageSizetargetSize, 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(ImageViewimageView) {
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的处理分析,可以跳过,有兴趣的话就接着看看



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

根据他的两种不同算法(||和&&),我们举个具体例子说明下
比如我们限定图片要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个图片像素点~200200的图片,图片像素密度则是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阀值,超过自定义的内存总大小值时则移除最老的图片


代码如下
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. Eachtime a Bitmap is accessed, it is moved to
* the head of a queue. When a Bitmap is added to a full cache, the Bitmap atthe 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 storedBitmaps.
*
* @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 wasreturned, 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 ofthe 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 belowthe requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1 toevict 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() isreporting 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);
}
}[/mw_shl_code]



代码分析
核心方法是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(...)方法设置其他提供的类型缓存

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



AndroidBitmap全面解析(三)开源图片框架分析1-UIL(下)

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框架的默认处理
/* Createsdefault 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效果等等
(此外还有一些扩展的功能,由于是非核心部分不做详细介绍了)

源码结构
主要分三大包~下面挨个介绍
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功能强大是毋庸置疑的,代码框架也很清晰,文档也算齐全~ 但是对开发者尤其是我这样的初学者来说一点点啃下来还是很艰难的,最好先看教程一二,完全懂了以后再看本篇,当然一二看懂基本上图片处理也差不多了

Android Bitmap全面解析(三)开源图片框架分析2-ImageLoader和Volley

ImageLoader和Volley图片部分还包括其他大部分图片框架,基本上图片处理都差不多,区别仅在于部分优化了,而优化方面UIL即Universal-Image-Loader框架做的最好,所以这部分章节算是温习一下图片处理以及寻找下其他框架里面一些不一样的图片处理方式(只关注图片方面)首先是ImageLoader
https://github.com/novoda/ImageLoader
主要还是分析图片加载的核心代码部分,其他地方简略介绍

单张图片的缩放问题
核心方法如下~

int calculateScale(final int requiredSize, int widthTmp, int heightTmp) {

    int scale = 1;

    while (true) {

        if ((widthTmp / 2) < requiredSize ||(heightTmp / 2) < requiredSize) {

           break;

        }

       widthTmp /= 2;

       heightTmp /= 2;

       scale *= 2;

    }

    return scale;

}

也是写法换了一下,其实意义等同于UIL框架中的CROP类型时的缩放,也等同于官方推荐的基本处理(参见教程一)如下

while (srcWidth / 2 >= targetWidth&& srcHeight / 2 >= targetHeight) { // &&

     srcWidth /= 2;

     srcHeight /= 2;

     scale *= 2;

}

一模一样~!~!~!~! 两个条件完全反过来,而一个是while继续的条件,一个是break中断的条件,所以综合起来实际意义是完全一样滴

ImageLoader框架相当于我们教程一里面的,只有对一种情况的解析,没有UIL那种对不同缩放方式的区别处理

此外是色彩样式修改

框架源代码全局搜索了下关键字,也找了主页文档介绍,貌似没有发现ImageLoader对色样有设置,需要修改色样的话需要自己实现了

缓存池部分

ImageLoader框架分三种缓存池

LruBitmapCache    LRU算法的强引用缓存

NoCache                 无缓存情况(其实就不能算缓存池了,没啥意义一般不会使用这个类)

SoftMapCache       软引用缓存

 

支持的类型比较少,缓存池只能算有两种,单独使用强引用和单独使用软引用这两种,没有二级缓存的处理

同样也是提供三个类型,由使用者自行设定缓存类型,设置方法为

SettingsBuilder.withCacheManager(...)

 

LruBitmapCache强引用缓存的分析
框架里面自定义了一个LruCache类,不是官方的LruCache类,但是实现逻辑都相似(基本算是一样),我对比了下,就相当于官方的LruCache类+UIL框架强引用类的综合体了~
内部实现也是一个LinkedHashMap,关键方法名字是put和trimToSize(本来还想吐槽为啥和UIL框架名字一样- -, 最后发现都是模仿官网LruCache类里方法起的名字), 此外还有一个entryRemoved的方法,也是直接模仿官方LruCache写的一方法,意思是超过强引用缓存池阀值时,移除其中最老的对象,可以子类复写此方法,对这个移除的对象做所需处理(比如将其移至软引用缓存中,自己实现一个二级缓存)

public final V put(K key, V value) {

    if (key == null || value == null) {

        throw new NullPointerException( "key == null || value == null" );

    }

    V previous;

    synchronized ( this) {

        putCount++;

        size += safeSizeOf(key, value);

       previous = map.put(key, value);

        if (previous != null) {

           size -= safeSizeOf(key, previous);

        }

    }

 

    if (previous != null) {

       entryRemoved( false, key, previous, value);

    }

 

    trimToSize(maxSize);

    return previous;

}

 

再简单的唠叨下吧

putCount记录添加数量的,这里暂时用不上不介绍

synchronized是为了保证多线程访问时size计算包括putCount的计算不要出现混乱

首先size += 将put的对象的大小加至当前缓存池大小size值上

map.put的返回值是已有对于key时返回的被替换value值,如果非空则代表上一个被移除了,自然要-=减去其size值

然后就是关键的entryRemoved方法了,方法内部是无内容的,只是相当于将移除的bitmap对象作为参数传入方法中

最后调用trimToSize方法检测当前缓存大小是否大于阀值,做对应处理

 

private void trimToSize(int maxSize) {

    while (true) {

       K key = null;

       V value = null;

       synchronized ( this) {

           if ( size < 0 || ( map.isEmpty() && size != 0)) {

               throw new IllegalStateException(getClass().getName()+ ".sizeOf() is reporting inconsistent results!" );

           }

 

           if ( size <= maxSize) {

               break;

           }

           // Change

 

           if ( map.entrySet().iterator().hasNext()) {

               Map.Entry<K, V> toEvict = map.entrySet().iterator().next();

               if (toEvict == null) {

                   break;

               }

 

               key = toEvict.getKey();

               value = toEvict.getValue();

               map.remove(key);

               size -= safeSizeOf(key, value);

               evictionCount++;

           }

       }

 

       entryRemoved( true, key, value, null);

    }

}

和UIL框架也差不多(其实都是跟官方LruCache类里对于方法改的,所以名字逻辑几乎都没区别)

检测size是否超过maxSize,是的话移除之~ 然后对于size处理下

 

entryRemoved方法
内部是空的,参数做个简单介绍,主要是第一个boolean参数,可以直接参考LinkedHashMap的源码里相似方法
意思就是 true是为了移除对象保证空间才移除的对象,而false是平常put替换还有remove时移除的对象,
那么如果想在这个框架里实现二级缓存,就可以自定义子类复写Imageloader框架的LruCache类,然后复写这个类,
在里面判断,如果是第一个参数为true,则将其保存至一个软引用/弱引用的二级缓存池中
SoftMapCache软引用缓存
只有一个软引用,无数量控制所以没啥好分析的,就一个简单的Map<String, SoftReference<Bitmap>> cache类型集合
然后提供put remove等基本方法

总的来说,最基本的都有,但是不够完善,需要自行实现诸如二级缓存等一些加强功能
现在几乎都用UIL框架了,所以ImageLoader基本仅提供个小参考了,如果想研究源码又觉得UIL比较复杂,倒是可以看看ImageLoader,在项目里使用的话,还是推荐UIL,我~相信群众~
---------------------------------------------------------------------------
Volley框架图片加载部分
不同于之前两个框架都是专注做图片加载的,这个框架是综合型的,且主要优势在于网络数据异步请求部分,而图片加载部分其实功能不是很完整,仅提供最基本的处理,下面分析一下

首先还是单张图片的处理
同样,全局搜一下关键字inSampleSize定位到ImageRequest类里的一个方法

/**

* The real guts ofparseNetworkResponse. Broken out for readability.

*/

private Response<Bitmap>doParse(NetworkResponse response) {

    byte[] data = response. data;

    BitmapFactory.OptionsdecodeOptions = new BitmapFactory.Options();

    Bitmap bitmap = null;

    if (mMaxWidth == 0 && mMaxHeight == 0) {

       decodeOptions. inPreferredConfig = mDecodeConfig;

       bitmap = BitmapFactory. decodeByteArray(data, 0, data.length, decodeOptions);

    } else {

       // If we have to resize this image, first get the natural bounds.

       decodeOptions. inJustDecodeBounds = true;

       BitmapFactory. decodeByteArray(data, 0, data.length, decodeOptions);

       int actualWidth = decodeOptions. outWidth;

       int actualHeight = decodeOptions. outHeight;

 

       // Then compute the dimensions we would ideally like to decode to.

       int desiredWidth = getResizedDimension(mMaxWidth , mMaxHeight ,

               actualWidth, actualHeight);

       int desiredHeight = getResizedDimension(mMaxHeight , mMaxWidth ,

               actualHeight, actualWidth);

 

       // Decode to the nearest power of two scaling factor.

       decodeOptions. inJustDecodeBounds = false;

       // TODO (ficus): Do we need this oris it okay since API 8 doesn't support it?

       // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;

       decodeOptions. inSampleSize =

           findBestSampleSize(actualWidth, actualHeight,desiredWidth, desiredHeight);

       Bitmap tempBitmap =

           BitmapFactory. decodeByteArray(data, 0, data.length, decodeOptions);

 

       // If necessary, scale down to the maximal acceptable size.

       if (tempBitmap != null && (tempBitmap.getWidth()> desiredWidth ||

               tempBitmap.getHeight() > desiredHeight)) {

           bitmap = Bitmap. createScaledBitmap(tempBitmap,

                   desiredWidth,desiredHeight, true);

           tempBitmap.recycle();

        } else {

           bitmap = tempBitmap;

       }

    }

 

    if (bitmap == null) {

       return Response.error(new ParseError(response));

    } else {

       return Response.success(bitmap,HttpHeaderParser.parseCacheHeaders(response));

    }

}

两个地方是关键,一个是getResizedDimension一个是findBestSampleSize,分别介绍下

先看后一个findBestSampleSize,我们比较熟的逻辑方法如下

    static int findBestSampleSize (

           int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {

       double wr = (double) actualWidth / desiredWidth;

       double hr = (double) actualHeight / desiredHeight;

       double ratio = Math.min(wr, hr);

       float n = 1.0f;

       while ((n * 2) <= ratio) {

           n *= 2;

       }

 

       return (int) n;

    }

又一种算法~但实际意义还是和教程一里的一样~数学逻辑好的应该一下就能看出来,看不出来的...做个demo对比下几个方法对不同值的计算结果,懒得测试的,那就信我的话吧,基本是木有区别的

 

然后是另一个方法的介绍getResizedDimension,以前没见过,有点参考价值~
获取调整后的大小尺寸,也就是获取所需压缩图片大小值的一个计算方法,处理方法挺特别的,可以略过,其实不通过这个方法进行"调整",直接使用自定义的maxWidth maxHeight限定宽高值就行了

本来想偷懒网上搜下这方法的说明,可惜最多查到一句"按一定规则计算出...", 算了 我还是自己跑项目边研究吧~其实这段方法学习参考价值更大一点,实际上设置所需压缩宽高值以后,图片就会按照之前那种规则去获取一个适当的值了,即不采用本方法压缩效果也是可以实现的

    /**

     * Scalesone side of a rectangle to fit aspect ratio.

     *

     * @param maxPrimary Maximum size of theprimary dimension (i.e. width for

     *       max width), or zero to maintain aspect ratiowith secondary

     *       dimension

     * @param maxSecondary Maximum size of thesecondary dimension, or zero to

     *       maintain aspect ratio with primary dimension

     * @param actualPrimary Actual size of theprimary dimension

     * @param actualSecondary Actual size of thesecondary dimension

     */

    private static int getResizedDimension (int maxPrimary, int maxSecondary, intactualPrimary,

           int actualSecondary) {

       // If no dominant value at all, just return the actual.

       if (maxPrimary == 0 &&maxSecondary == 0) {

           return actualPrimary;

       }

 

       // If primary is unspecified, scale primary to match secondary's scalingratio.

       if (maxPrimary == 0) {

           double ratio = ( double) maxSecondary / ( double) actualSecondary;

           return ( int) (actualPrimary * ratio);

       }

 

       if (maxSecondary == 0) {

           return maxPrimary;

       }

 

       double ratio = ( double) actualSecondary / ( double) actualPrimary;

       int resized = maxPrimary;

       if (resized * ratio > maxSecondary){

           resized = ( int) (maxSecondary / ratio);

       }

       return resized;

    }

方法传入4个数据,即两组
一组是max,即限定值,分成两个 主值和次值,不是宽高的意思,主值有可能是宽则次值是高, 也有可能是高为主值,宽为次值
另一组则是actual实际值,即图片的原宽高数值,也分成主和次, max主为限定高度,次为限定宽度时, 实际值也对于主为高,次为宽,反之亦然
- - 意义不明?我们接着看


使用方法是调用两次,主为高次为宽时,计算的结果是调整后所需高度值,也就是计算为主的那个所需值
同理,主为宽时,计算结果是调整后的所需宽度值
- - 意义还是不明?我们再接着看


举个实际例子吧
如果设置了限定的宽高值,比如maxWidth = 180 maxHeight = 200, 原图我这边是加载的一张图片宽*高是 720*617的图
分别调用两次方法获取所需值
intdesiredWidth = getResizedDimension(180, 200, 720, 617); //获取调整后的所需宽度值
intdesiredHeight = getResizedDimension(200,180, 617,720); //获取调整后的所需高度值

计算结果

desiredWidth = 180;

desiredHeight = 154;

 

直观上理解就是,将所需宽高值调整成了与原图宽高一样的比例

(所需宽高调整后比例180/154≈1.17   原图比例720/617≈1.17)

 

再次测试,同一张图片,maxWidth和maxHeight都设成200,计算后结果是

desiredWidth = 200;

desiredHeight = 171;

宽高比也和原图比例相同(200/171≈1.17)

 

即,在限定宽高都设定过的情况下(都不为0),计算原图比例然后将限定宽高大小调整为与原图一致的比例,且调整后的宽高值都小于等于原限定值宽高

任一方为0或者都为0的情况这里就不分析了,不是太重要,是对限定值为0即未设定数值时进行的一个对应处理

之前UIL框架中针对两种不同缩放类型,将压缩比例值计算区分成了两种,一个是||连接条件,一个是&&连接条件

||的处理结果是保证压缩后图片任意一条边大于限定值对应边即可

&&的处理结果是保证压缩后图片宽高两条边都要大于限定值对应边

 

&&的处理与官方提供的例子一致,也是教程一中介绍的计算方法

那volley的这种算法,简单实验了几个数据,我发现结果和UIL中FIT_INSIDE情况即||连接条件计算的结果差不多,我也写了简单demo对比了更多组不同数据下两者的结果,也都是一致的~

逻辑上理解,如果限定值和原图比例不同,那么处理后的限定值其中一个边会减少~那以这个调整后的限定值压缩图片的话,最终结果就有可能出现: 最后获取的压缩图片样式一条边小于调整前我们设置的限定值对应边

最终造成了只有一条边满足大于等于限定值的情况,即与UIL中FIT_INSIDE情况相同

 

 

总结,官方的方法是保证无论什么缩放显示类型,都能保证压缩后图片宽高值大于等于限定值

UIL比较好,不同情况都有考虑,比例值计算的比较精确

Volley框架则是另一种了,即默认情况下能够保证图片清晰度(像素密度能达到目标),但是CROP等缩放类型下尤其是长宽比较大的情况时,则压缩后图片无法保证清晰度(由于此情况较少,所以大部分情况下的压缩质量还是有保证的)

注:可能有不太准确的地方,我正在整理 1.教程一官方方法 2.UIL框架3.Volley框架图片处理 三者的具体数据对比,因为数据量比较大且杂,所以之后如果能整理顺利且有时间的话,就单开一章对框架做个简单比较~

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

色彩样式部分
框架源码里是将图片色彩样式设为RGB_565,也是直接写死的

多张图片缓存池

一个木有- - 只提供了一个缓存池类型接口ImageCache,需要自定义类实现它

网上主流的做法,包括demo都是自定义一个类继承LruCache然后实现Volley框架中对应接口

类写法也很简单,教程二熟悉的话估计都会写,类代码如下

public class BitmapLruCache extends LruCache<String, Bitmap> implements ImageCache {

    public BitmapLruCache( int maxSize) {

       super(maxSize);

    }

 

    @Override

    protected int sizeOf(String key, Bitmap value) {

       return value.getRowBytes() *value.getHeight();

    }

 

    @Override

    public Bitmap getBitmap(String url) {

       return get(url);

    }

 

    @Override

    public void putBitmap(String url, Bitmap bitmap){

       put(url, bitmap);

    }

}

同样,你也可以利用LinkedHashMap实现,还可以添加上二级缓存的结构都可以

Volley框架相对其他框架而言,功能其实算是很弱了,只有最最基本的处理逻辑,甚至缓存池都需要自定义类实现,但这也是一个优点,因为Volley是一个综合型框架,已经提供了一个很好的地基,网络数据部分无需我们考虑太多,如果想做一个属于自己的框架,又不想完全一点点自己写,那Volley就是你最好的选择了,我们可以根据需要进行框架的二次开发~

 

 

AndroidBitmap全面解析(四)图片处理效果对比

对比对象: 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框架缩放类型的对应关系,

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值