Android--Bitmap加载&ImageLoader

在Android的初级开发阶段,咱们只关注如何实现咱们的功能————无论黑猫白猫,抓到老鼠就是好猫。但在这个过程当中咱们很容易会忽略一些东西,如性能的损失、内存泄漏、代码耦合度高,逻辑混乱以及所以带来的代码混乱。html

实际上上述问题我反而以为最后一个是最严重的,垃圾代码多了,虽然能运行,但一旦某个环节发生问题就会牵一发动全身,甚至极可能整个项目都要重构。于是咱们要学会解耦,其中最好的办法莫过于将每个功能单独封装成一个模块,尽可能少在别的模块里面调用别的模块。java

虽然话是这么说,可是彻底遵循上述仍是很困难的,由于有些时候已经成了定性思惟,稍不留神就又写了一遍垃圾代码。多读、多写、多看,将良好的编程习惯融入本身的风格里面,总能成长的。android

Android优化是一个很大的话题,Android developer中也给出了许多开发者须要注意的地方,多学习一下开发者文档老是没有错的。git

Part.1 Bitmap 加载

在Android开发中,咱们常常用到的图像文件有两种,一种是Drawable,另外一种就是Bitmap。
文档对Drawable的描述是“something that can be Drawn”,而且它自己是一个抽象类。而Bitmap自己能够从Drawable中加载出来,相比于Drawable,Bitmap会占用更大的内存,而且其加载速度也比Drawable要来的慢。但咱们仍要使用它的缘由是它比Drawable来讲,进行图像的处理要更加方便。github

在Android中有一大半的OOM的缘由都是由于Bitmap的处理不当而形成的。一张色彩丰富分辨率极高的图片直接加载进APP中的时候会消耗巨额的内存,而一个APP通常而言只会被分配16M的内存可使用。web

综上所述,要处理好Bitmap的加载是十分必要的。算法

1.1.BitmapFactory.Options

咱们加载一个Bitmap的时候大多数会选择使用BitmapFactory.decodeFile(),BitmapFactory.decodeResource()、BitmapFactory.Stream()、BitmapFactory.decodeByteArray()这四个方法来对文件、资源、输入流以及字节数组进行加载。编程

对于Bitmap的优化莫过于从大小上进行优化,因为移动设备的屏幕大小有限,咱们能看到的只有imageView大小的图片,最多也不过是占据整个屏幕,所以咱们何不对照片进行裁剪呢。数组

另外,不一样格式的编码方式也对Bitmap的大小有影响,在Android中对一个Bitmap的色彩存储方案有4种,分别是ARGB_8888(默认)、ARGB_444四、ALPHA_8以及RGB_565,关于上述色彩方案的区别能够查看下方的引用: 缓存

Bitmap.Config ARGB_4444:每一个像素占四位,即A=4,R=4,G=4,B=4,那么一个像素点占4+4+4+4=16位

Bitmap.Config ARGB_8888:每一个像素占四位,即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位

Bitmap.Config RGB_565:每一个像素占四位,即R=5,G=6,B=5,没有透明度,那么一个像素点占5+6+5=16位

Bitmap.Config ALPHA_8:每一个像素占四位,只有透明度,没有颜色。

而默认Android的色彩方案采用的是ARGB_8888,每一个像素点占据了32位,在得到最佳的表现效果的时候也占据了大量的内存。但实际上在一些场景咱们或许不须要这么好的色彩表现,例如在缩略图中。

在这些方法中都存在一个BitmapFactory.Options的属性,这个属性可使得bitmap会按照option里面的属性进行decode,而咱们对Bitmap的优化也正是从这里下手。

inSamleSize

在Options中有一项叫inSampleSize的属性,该属性负责将Bitmap进行缩放,缩放比例是赋予数值的平方分之一,默认值为1(不缩放)。

举个例子,一张图片的大小为1024 * 1024 * 4=4M(ARGB_8888一共32位,即4个字节),当咱们设置inSamepleSize为2的时候,其长和宽分别缩小一半,于是其大小变为512 * 512 * 4=1M,故比原来缩小了1/4(1/2 * 1/2)的大小和内存消耗。

须要说明的是,当该值小于1的时候,其做用与1相等,即无缩放做用。官方文档指出inSampleSize的取值应为2的倍数,不然将会取离取值最近的一个2的倍数为其设定值。即当我将inSampleSize设置为3的时候,系统默认会将该值约等于2来处理。

当一个图片的大小为200 * 300而ImageView的显示大小为100 * 100的时候,应将inSamepleSize设置为2,缩放后Bitmap大小为100 * 150,符合imageView的大小。但若是设置成4的时候,缩放后的大小就会变成50 * 75,虽然仍旧符合imageView的大小,但图片有可能会被拉伸填充ImageView致使失真变形。

inPreferredConfig

对应设置色彩方案的属性是inPreferredConfig,其能够设置的值就是四种色彩方案,其原理与上述相似,不过影响的是1024 * 1024 * 4中4的值。

inJustDecodeBounds

在咱们要修改缩放的大小以前,咱们必需要得到图片的原始宽高与imageView的宽高作对比才能得出inSampleSize的值,而咱们必须先加载图片才能获取原始宽高,但这样在第一次加载的时候咱们仍是占用了巨大的内存,并无进行优化。

面对这个问题,Android提供了inJustDecodeBounds这个属性。

该属性为true的时候,BitmapFactory的解码并不会真的生成Bitmap对象,而只是解析该图片的原始宽高信息并将其置于options对象中,毕竟有了原始的宽高咱们才能决定缩放的大小。

但要记得在设置完毕后将该值重置为false,不然没法加载出真正的图片。

所以,一个完整的优化过程应该为:

BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res,redId,options);

    options.inSampleSize = shapeSize(options,reqWidth,reqHeight);
    options.inJustDecodeBounds = false;
    bitmap =  BitmapFactory.decodeResource(res,redId,options);

通过该过程获取到的Bitmap就是最适合的Bitmap。

Part.2 Bitmap的复用与缓存

通常来讲,Bitmap的来源都是经过网络下载或者读取本地sd卡的文件而获得的,如果经过本地读取的还好,如果经过网络下载的话无疑会消耗大量的流量,在无网/弱网状态下甚至没法观看已经浏览过的照片。

前面曾说过,Bitmap占据着大量的内存,尽管通过上面的优化已经变得很小,可是当有大量Bitmap不断添加进来的时候程序仍旧会由于内存空间不足而卡死。

对于上述的问题,诸如glide picasso fresco等图片加载框架都给咱们很好的解决了问题,可是为了更好的了解它们的缓存原理,本人参照《Android 开发艺术探索》从新写了一个imageLoader,该框架实现了三级缓存、异步加载以及图片压缩的功能,基本可以知足平常使用。

2.1.LruCache

在操做系统中咱们就接触过Lru算法(Least Recently Used),该算法的原理是:在必定大小的空间内缓存对象,当该空间变满但仍要缓存的时候,就选择淘汰掉最久没有使用过的缓存对象。

在Android中咱们经常使用LruCache类来进行内存的缓存,利用DiskLruCache类进行硬盘的缓存,下面以LruCache类为例探索Lru算法的实现。

private final LinkedHashMap<K, V> map;

该类使用了LinkedHashMap来存储数据,它保存了每一个值的插入顺序,使用Iterator遍历的时候获得的记录是按照插入顺序的。

该类是线程同步的,在get和put方法的内部都用synchronized关键字进行同步。

在get方法被调用的时候,因为访问了LinkedHashMap内部的recordAccess方法,会将被访问的对象插入到访问链表的最后。

一样的,在put一个对象的时候首先会去检索是否存在该对象,若存在一样会调用recordAccess方法将该节点取下来挂在到访问链表的最后。

当HashMap满要进行删除的时候,会直接取出LinkedHashMap的eldest节点删除掉。

这样就完成了一个Lru的过程。

Lru的使用方法十分简单:

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    int cacheMemory = maxMemory / 8;
    mMemoryCache = new LruCache<String,Bitmap>(cacheMemory){
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getHeight() * value.getRowBytes() / 1024;
        }
    };

上面的代码首先获取了虚拟机的可用内存,而后取其中的1/8做为lru的缓存大小。

而sizeOf中完成了Lru中每一个Bitmap的大小的计算,每添加或删除一个Bitmap都会使得lru的现存容量发生变化。

2.2.DiskLruCache

该类不存在于Android提供的库中,须要开发者从Android Developer中进行下载

DiskLruCache

尽管没有整合到库中,但它获得了Android官方文档的推荐。

DiskLruCache的实现实际上与LruCache相似,可是因为它是对硬盘进行读写,因此加入了一套管理文件的方法。

其全部文件的管理基础都基于一个叫作journal的文件,该文件内记录了缓存的版本,Aapp的版本,缓存的数量。

/* libcore.io.DiskLruCache * 1 * 100 * 2 * * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 * DIRTY 335c4c6028171cfddfbaae1a9c313c52 * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 * REMOVE 335c4c6028171cfddfbaae1a9c313c52 * DIRTY 1ab96a171faeeee38496d8b330771a7a * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 * READ 335c4c6028171cfddfbaae1a9c313c52 * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 */

接下来的子行记录了每一个缓存的操做过程和每一个缓存的key,在这些key的前头会有一些标识符。

CLEAN 标识符表明着数据是干净的,当一个脏数据被确认更改的时候就会变成CLEAN。

DIRTY 表明着当前数据是脏数据,这种数据发生过更改,可是尚未正式的写入文件中。

REMOVE 表明着当前数据须要被删除。

READ 表明着当前数据被get()方法读取过。

能够注意到每一个DIRTY数据后面都必然紧跟着一个CLEAN或者是REMOVE,这表明着DIRTY的数据被CLEAN(洗净)或者REMOVE(删除)掉了。

每次调用DiskLruCache.open()的时候就会对该文件进行读取,全部的数据操做会被读入到一个linkedHashMap中,这就恢复了上次操做的顺序了。

接下来对该linkedHashMap的操做跟上述的LruCache的操做是类似的,不一样之处在于每次操做都会去修改journal文件罢了。

当咱们的操做愈来愈多的时候,该文件也会变得愈来愈大,但因为每次对缓存的读写都会使得一个redundantOpCount的变量增大,并在读写末尾会对该变量进行判断。当操做记录大于2000条的时候,该文件会发生重构,并将一些多余的操做记录清除掉以保持它的大小合理。

Part.3 ImageLoader

上面的两Part不过是为了下面这个ImageLoader的铺垫,实际上网络上已经有不少关于该框架的文章,其结构都大同小异,故本人再也不深刻探讨代码了。实际上都是一些逻辑上的操做,懂了过程,天然就懂怎么写了。

该ImageLoader采用了两级缓存,分别是内存和本地。存储方式是以原图存储,色彩方案是ARGB_8888。

支持同步/异步缓存,同步缓存时须要注意不能再主线程中进行操做,而异步缓存的原理则是对缓存的存入/放出操做均使用了一个线程池来管理,该线程池共有CPU+1个核心线程,2倍CPU的最大线程数以及10S的线程闲置时间,具体能够个别配置。

整个访问的流程是MemoryCache -> DiskCache -> Http,在上级访问不到的时候就会向下级缓存发出请求。当在某一级请求成功以后会将Bitmap逆向加载进去。

本ImageLoader以url做为Cache的key值。

举个例子:

① 第一次访问Bitmap的时候因为MemoryCache于DiskCache中必然不存在,于是直接访问网络将图片以流的形式下载下来

② 下载成功后会将图片流加入到DiskCache中,并尝试从DiskCache中将缓存取出,。

③ 在DiskCache将Bitmap取出的过程当中会将Bitmap进行压缩,压缩后会将Bitmap加载到MemoryCache中以方便下次读取。

④ 若此时DiskCache中仍旧没法读取缓存,则有多是DiskCache没法建立,所以此时再次从URL中获取图片并直接将图片返回。

当缓存进行到最后一步的时候,因为采起的是BitmapFactory.decodeSteam()的方法,没有办法再对图片进行调整,所以此时加载出来的图片是原图,有可能致使OOM。

发生上述问题的缘由是虽然BitmapFactory支持对流进行编码构造,可是BufferedInputStream是一种有序的文件流,而咱们的压缩须要两次decode该文件流。这就会致使第二次decode时候没法获取文件正确的位置致使返回null。

以上就是imageLoader的一个粗略过程,详细能够查看我Github上面的代码。

ImageLoader

该项目用到了RxJava+Retrofit+EasyRecycleView和ImageLoader,前面的开源库都是试水阶段,还有不少不明白的东西。

图片来源:gank.io

因为图片来源于上述网站的API,而每张图的URL存于API返回的JSON中。为了达到无网能够访问缓存,构建了一个UrlCache类,当正常访问网络的时候会刷新缓存,当无网的时候直接走缓存取出url再经过imageLoader进行加载。

附上一张APP的运行流程图:

这里写图片描述

起初打算利用okHttp3的interceptor对访问进行拦截,但不知为什么没有达到预期的效果,欢迎讨论。