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

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 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> cache类型集合
然后提供put remove等基本方法




总的来说,最基本的都有,但是不够完善,需要自行实现诸如二级缓存等一些加强功能
现在几乎都用UIL框架了,所以ImageLoader基本仅提供个小参考了,如果想研究源码又觉得UIL比较复杂,倒是可以看看ImageLoader,在项目里使用的话,还是推荐UIL,我~相信群众~


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


Volley框架图片加载部分
不同于之前两个框架都是专注做图片加载的,这个框架是综合型的,且主要优势在于网络数据异步请求部分,而图片加载部分其实功能不是很完整,仅提供最基本的处理,下面分析一下


首先还是单张图片的处理
同样,全局搜一下关键字inSampleSize定位到ImageRequest类里的一个方法
/**
* The real guts of parseNetworkResponse. Broken out for readability.
*/
private Response doParse(NetworkResponse response) {
    byte[] data = response. data;
    BitmapFactory.Options decodeOptions = 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 or is 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限定宽高值就行了,以下这部分是对此方法的简单分析,精力有限的可以跳过~


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


本来想偷懒网上搜下这方法的说明,可惜最多查到一句"按一定规则计算出...", 算了 我还是自己跑项目边研究边死扣吧~其实这段方法学习参考价值更大一点,实际上设置所需压缩宽高值以后,图片就会按照之前那种规则去获取一个适当的值了,即不采用本方法压缩效果也是可以实现的
    /**
     * Scales one side of a rectangle to fit aspect ratio.
     *
     * @param maxPrimary Maximum size of the primary dimension (i.e. width for
     *        max width), or zero to maintain aspect ratio with secondary
     *        dimension
     * @param maxSecondary Maximum size of the secondary dimension, or zero to
     *        maintain aspect ratio with primary dimension
     * @param actualPrimary Actual size of the primary dimension
     * @param actualSecondary Actual size of the secondary dimension
     */
    private static int getResizedDimension (int maxPrimary, int maxSecondary, int actualPrimary,
            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 scaling ratio.
        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的图
分别调用两次方法获取所需值
int desiredWidth = getResizedDimension(180, 200, 720, 617); //获取调整后的所需宽度值
int desiredHeight = 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 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实现,还可以添加上二级缓存的结构都可以


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


简单过了一遍ImageLoader框架和Volley框架图片部分,再次从侧面验证了我们教程中方法滴靠谱性~
也简单对这俩框架有了一些了解, 如果有兴趣可以自己找几个较为有名的框架自己看一下源码,估计差别也只在优化部分,核心逻辑都是相同的
Volley框架相对其他框架而言,功能其实算是很弱了,只有最最基本的处理逻辑,甚至缓存池都需要自定义类实现,但这也是一个优点,因为Volley是一个综合型框架,已经提供了一个很好的地基,网络数据部分无需我们考虑太多,如果想做一个属于自己的框架,又不想完全一点点自己写,那Volley就是你最好的选择了,我们可以根据需要进行框架的二次开发~
这也是我专门拿Volley出来介绍的原因,之后一章会对几个框架的不同图片处理效果做个简单对比,再之后一章就是Volley框架图片缓存的二次开发了,我也是第一次弄,肯定有不足的地方,到时候大家一起探讨~

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


篇幅不多,送上一个小彩蛋,估计也有不少人已经知道的
压缩图片那么常用的功能sdk里难道没有提供吗?
有~在一个ThumbnaiUtils类中,作用貌似是获取视频预览缩略图的,是一个叫computeSampleSize的方法,不过是private的方法,看着貌似很高级的样子,注释里也说是为了避免OOM异常进行的对应处理,大家有兴趣可以研究研究~

你可能感兴趣的:(Android)