【学习】Android中Bitmap的加载和Cache

简介

由于Bitmap的特殊性以及Android对单个应用所施加的内存限制,比如16MB,导致加载Bitmap时很容会议出现内存溢出。如何高效加载Bitmap是一个很重要也容易被开发者忽视的问题。

Android中缓存策略是一个通用的思想,实际开发中经常需要用到Bitmap做缓存。通过缓存策略我么不需要每次都从网络上请求图片或者从存储设备中加载图片。目前比较常用的缓存策略时LruCache和DiskLruCache,其中LruCache常被用作内存缓存,而DiskLruCache常被用作存储缓存。Lru是最近最少使用算法,这种算法的核心思想为:当缓存快满时,会淘汰近期最少使用的缓存目标。

Bitmap的高效加载

如何加载一个Bitmap

Bitmap在Android中指的是一张图片,可以是png,jpg等常见的图片格式。BitmapFactory提供了四类方法加载Bitmap:decodeFile decodeResource decodeStream decodeByteArray,分别用于从文件系统、资源、输入流以及字节数组加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法最终在Android的底层实现,对应着BitmapFactory类的几个native方法。

如何高效加载Bitmap

核心思想:采用BitmapFactory.Options来加载所需尺寸的图片,它可以按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,上面的四类方法都支持Options参数。

通过Options来缩放图片,主要是用到它的inSampleSize参数,即采样率。当该参数为1时,采样后的图片为原始大小,当大于1时,比如2,此时采样后图片宽高为原图大小的一半,而像素数为原来的四分之一,内存大小也为原图的四分之一,小于1时没有效果,相当于1。官方文档指出它的取值应该总是2的指数,比如1,2,4,8,16等等,如果外界传递一个不为2的指数的数字,那么系统会向下取整一个最接近2的指数来替代,但这个结论不在所有Android版本上有效,只充当一个建议。

获取采样率

1.将BitmapFactory.Options的inJustDecodeBounds参数设置为true并加载图片
2.从BitmapFactory.Options中去除图片的原始宽高信息,他么对应于outWidth和outHeight参数
3.根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
4.将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片

当inJustDecodeBounds参数为true时,BitmapFactory只会解析图片的原始宽高信息,并不会真正地加载图片,此时获取的宽高信息和图片位置和程序运行的设备有关,同一张图片放在不同的drawable目录下或者程序运行在不同屏幕密度地设备上都可能导致BitmapFactory获取到不同的结果,这个现象和Android的资源加载机制有关。

实例

image.png

Android中的缓存策略

缓存策略主要包括缓存的添加、获取和删除这三类操作。目前常用的一种缓存算法是LRU,近期最少使用算法。核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,其中LruCache常被用作内存缓存,而DiskLruCache常被用作存储缓存。通过两者结合就可以方便地实现一个具有很高实用价值的ImageLoader。

LruCache

它是一个泛型类,并且线程安全,内部采用LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后添加新的缓存对象

强引用、软引用、弱引用的区别

强引用:直接的对象引用
软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收
弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收

定义

image.png

LruCache的典型初始化过程(演示)

image.png
只需要提供缓存的总容量大小并重写sizeOf方法即可。sizeOf方法的作用时计算缓存对象的大小,这里大小的范围需要和总容量的单位一致。对于上面的示例代码来说,总容量的大小为当前进程的可用内存的1/8,单位为KB,而sizeOf方法则完成了Bitmap对象的大小计算。除以1024也是为了将单位转换成KB。getRowBytes方法的作用是: Return the number of bytes between rows in the bitmap's pixels.某些情况下还需要重写LruCache的entryRemoved方法,它一出就缓存时会调用entryRemoved方法,因此可以在此方法中完成一些资源回收工作(如果有需要)。

缓存的获取和添加

image.png
它还支持删除操作,通过remove方法即可删除一个指定的缓存对象。

DiskLruCache

它用于实现磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存效果

1.DiskLruCache的创建

它并不能通过构造方法来创建,它提供了open方法用于创建自身

image.png
第一个参数表示磁盘缓存在文件系统中的存储路径。路径可以选择SD卡上的其他指定目录,具体是指/sdcard/Android/data/package_name/cache,还可以选择data下的当前应用目录,具体可以根据需要灵活设定。如果应用卸载后就希望删除缓存文件,那么就选择SD卡上的缓存目录,若希望保留缓存数据那就选择SD卡上的其他特定目录。

第二个参数表示应用的版本号,一般为1即可。当版本号发生改变时它会清空之前所有的缓存文件,实际开发中作用并不大,因为一般情况下即使应用版本号发生改变缓存文件依然有效。

第三个参数表示单个结点所对应的数据个数,一般设为1。

第四个参数表示缓存的总大小,比如16MB,当缓存超过这个大小就会清除一些缓存从而保证总大小不大于这个设定值

典型创建过程

image.png getDiskCacheDir疑似已经被移除,当 SD 卡存在或者 SD 卡不可被移除的时候,就调用getExternalCacheDir()方法来获取缓存路径,否则就调用getCacheDir() 方法来获取缓存路径。前者获取到的就是/sdcard/Android/data//cache 这个路径,而后者获取到的是/data/data//cache这个路径。最后将获取到的路径和一个 uniqueName 进行拼接,作为最终的缓存路径返回。uniqueName是对不同类型的数据进行区分而设定的一个唯一值,比如bitmap吗,file等文件夹。

2.DiskLruCache的缓存添加

添加操作通过Editor完成,Editor表示一个缓存对象的编辑对象。以图片为例,先要获取图片url所对应的key,然后根据key就可以通过edit()获取Editor对象,如果这个缓存正在被编辑,那么edit()会返回null,即DiskLruCache不允许同时编辑一个缓存对象。之所以要把url转换成key,是因为图片的url中很可能有特殊字符,会影响url在Android中的使用,一般采用url的md5值作为key

image.png 将图片的url转成key之后就可以获取Editor对象了。对于这个key来说,如果当前不存在其他Editor对象,那么Edit()就会返回一个新的Editor对象,通过它就可以得到一个文件输出流。由于前面的open1方法中设置了一个结点只能有一个数据,因此下面的DISK_CACHE_INDEX常量直接设为0即可

image.png

有了文件输出流,但从网络上下载图片时就可以通过这个文件输出流写入到系统文件上

image.png 经过上面的步骤之后还要通过Editor的commit()来提交写入操作,如果图片的下载过程发生异常,可以使用Editor的abort()来回退整个操作。

image.png

image.png

3.DiskLruCache的缓存查找

缓存查找过程也需要将url转换为key,然后通过DiskLruCache的get方法得到一个Snapshot对象,接着通过Snapshot对象即得到缓存的文件输入流,然后得到Bitmap对象。为了避免OOM一般不建议直接加载原始图片。但是通过BitmapFactory.Options对象来加载一张缩放后的图片对FileInputStream的缩放存在问题,因为它是一种有序的文件流,而两次decodeStream的调用影响了文件流的位置属性,导致第二次decodeStream时得到的是null。为了解决这个问题可以通过文件流来得到它所对应的文件描述符,然后通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片

image.png

猜你喜欢

转载自juejin.im/post/7079442123430297630