Android--Bitmap加载&ImageLoader

来源:互联网 发布:什么牛二手车软件 编辑:程序博客网 时间:2024/06/05 11:40

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

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

虽然话是这么说,但是完全遵循上述还是很困难的,因为有些时候已经成了定性思维,稍不留神就又写了一遍垃圾代码。多读、多写、多看,将良好的编程习惯融入自己的风格里面,总能成长的。

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

Part.1 Bitmap 加载

在Android开发中,我们经常用到的图像文件有两种,一种是Drawable,另一种就是Bitmap。
文档对Drawable的描述是“something that can be Drawn”,并且它本身是一个抽象类。而Bitmap本身可以从Drawable中加载出来,相比于Drawable,Bitmap会占用更大的内存,并且其加载速度也比Drawable要来的慢。但我们仍要使用它的原因是它比Drawable来说,进行图像的处理要更加方便。

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

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

1.1.BitmapFactory.Options

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

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

另外,不同格式的编码方式也对Bitmap的大小有影响,在Android中对一个Bitmap的色彩存储方案有4种,分别是ARGB_8888(默认)、ARGB_4444、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对访问进行拦截,但不知为何没有达到预期的效果,欢迎讨论。

1 0
原创粉丝点击