主要介绍这三个框架,都挺有名的,其他的框架估计也差不多了
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里面有个靠谱的方法如下)
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;
}[/mw_shl_code]
简单扫一眼,ImageSize,ImageDecodingInfo神马的明显是自定义的一个类,不要管,我们先挑重点部分看
方法最后两行可以看出来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;
}[/mw_shl_code]
我连注释一起复制过来了,里面对参数有比较详细的介绍,还有对应的例子~很良心的注释啊~
可以看到核心计算方法,其实和我们教程(一)里面是一样的(区别在于这里有两个情况的处理,一个是||一个是&&,后面会介绍原因),对源图的宽高和目标图片的宽高进行一些判断,满足时一直循环下去进行处理,直到取得一个合适的比例值为止,最终保证
使压缩后显示的图片像素密度是大于等于设定的像素密度的那个最低值
(这里换了个更合适的说法,因为框架对图片不同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属性,这里集合成了两种,对应关系见框架的源码,如下
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 ;
}
}[/mw_shl_code]
---------------------------------------------------------------------
根据他的两种不同算法(||和&&),我们举个具体例子说明下
比如我们限定图片要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
<ignore_js_op> <ignore_js_op>
两种显示效果简单的理解为
FIT_INSIDE(左图) 完整的展示图片,但imageView控件可能不占满
CROP(右图) 填充整个imageview,图片可能会被裁去一部分
对于上面的例子1000 600的图, 而图片限定大小是200 200
如果两种效果都是用一种压缩规则算法,比如官方提供的那种规则(也是我们教程一里面的),则会把图片压缩成了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上的主页中对于各个内存缓存类型的具体介绍
(主页地址见文章最开始处)
<ignore_js_op>
灰色框中标注的七种
只用了强引用,是现在官方推荐的方法,相当于我们教程(二)里面说的只用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阀值,超过自定义的内存总大小值时则移除最老的图片
代码如下
[mw_shl_code=java,true]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);
}
}[/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(...)方法设置其他提供的类型缓存
关于软引用其他那几个类型算法FIFO等的具体实现,我这里就不研究了,算法神马的水平有限,大家有兴趣可以自己看看
此外还有一个FuzzyKeyMemoryCache的内存缓存类, UIL中缓存里是支持保存同一张图片的多种缩放比例缓存对象的,设置中也可以取消这种设置使得缓存池中一个图片只保留一个size的缓存对象,通过调用 ImageLoader.denyCacheImageMultipleSizesInMemory()方法实现, 此时,UIL框架就会自动使用Fuzzy...去包装一下你之前选择的内存缓存类型~即算法还是按照你选择的类型,但是多了一个只保持一种size缓存对象的功能,这种设计叫做Decorator装饰模式,举个例子帮助理解下,这种设计模式相当于有一个变相怪杰的特殊面具,无论人或狗,谁带上谁就能具有相应的牛逼能力~
算是一个特殊的内存缓存类型,不能单独使用