Bitmap的加载与Cache(一)
2016-04-22 00:23
351 查看
如何有效的加载一个bitmap,由于Bitmap的特殊性以及Android对单个应用所施加的内存限制,比如16MB,这就导致加载Bitmap的时候很容易出现内存溢出。
因此,如何高效的加载bitmap是一个很重要也很容易被开发者忽略的问题。
高效加载Bitmap的核心就是采用BitmapFactory.Options来加载所需尺寸的图片。这里加色通过ImageView来显示图片,很多时候ImageView并没有图片的原始尺寸那么大,这个时候把整个图片加载进来后再射给ImageView,这显然是没有必要的,因为ImageView并没有办法显示原始的图片。通过Options参数就可以按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高加载时的性能。其缩放图片主要是用到了Options的inSampleSize参数,即采样率,当inSampleSize为1时,采样后的图片大小为图片的原始大小,当inSampleSize大于1时,比如2,那么采样后的图片其宽/高均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4
拿一张1024*1024像素的图片来说,嘉定采用ARGB8888格式存储,那么他占有的内存为1024*1024*4,即4MB,如果inSampleSize为2,那么采样后的图片其内存占用只有512*512*4即1MB。可以发现采样率inSampleSize必须是大于1的整数,图片才会有缩小的效果,并且采样率同时作用于宽/高,这将导致缩放后的图片大小以采样率的2次方形式递减。即inSampleSize=4是,那么缩放比例就是1/16。有一种 特殊情况,就是当inSampleSize小雨1时,其作用相当于1,即无缩放效果。
通过采样率即可有效的加载图片,那么到底如何获取采样率呢?
(1)将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片(只会解析 图片的原始宽/高信息,并不会去真正加载图片)
(2)从BitmapFactory.Options中取出图片的原始宽高信息。他们对英语outWitdh和outHeight参数
(3) 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
(4)将itmapFactory.Options的inJustDecodeBounds参数谁为false,然后重新加载图片。
除了BitmapFactory的decodedResource方法,其他三个decode系列的方法也是支持采样加载的,并且处理方式也是类似的,但是decodeStream方法稍微特殊,后面会讲解。
目前常用的一种缓存算法是LRU,LRU是近期最少使用算法,他的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存由两种:LruCache和DiskLruCache,前者用于实现内存缓存,而后者则充当了存储设备缓存,通过二者的结合就可以很方便的实现一个很高使用价值的ImageLoader。
LruCache是一个泛型类,他内部采用一个LinkedHashMap以强引用的方法存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作。接下来简答说一个几个引用:
强引用: 直接的对象引用
软引用: 当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收;
弱引用: 当一个对象只有弱引用存在时,此对象会随时被gc回收。
另外LruCache是线程安全的,因为他使用了LinkedHashMap:
功欲善其实,必先利器,接下来我们就看看LruCache的使用,首先典型的初始化:
只需要提供缓存的总容量大小并重写sizeOf方法即可。sizeOf方法的作用是计算缓存对象的大小,这里的大小的单位需要和总容量的单位保持一致。对于上面代码,总容量的大小为当前进程可用内存的1/8,单位为KB,sizeOf方法则完成了Bitmap对象的大小计算。
一些特殊情况,还需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用这个方法,因此可以在这个方法中完成一些资源回收(如果需要的话)
除了创建以外还有缓存的获取和添加。
下载地址
DiskLruCache的创建:
DiskLruCache并不能通过构造方法来创建,他提供了open方法用于创建自身,如下所示:
第一个参数表示磁盘缓存在文件系统中的存储路径,缓存路径可以选择SD卡上的缓存目录,具体是指/sdcard/Android/data/package_name/cache目录,其中package_name表示当前应用包名,当应用被卸载后,此目录会一并被删除。当然也可以选择SD卡上的其他指定目录,还可以选择data下的当前应用目录,具体可根据需要灵活设定。如果应用卸载后就希望删除缓存目录,那么就选择SD卡上的缓存目录,否则就选择SD卡上的其他目录。
第二个参数表示应用的版本号,一般设为1即可。当版本号发生变化时DiskLruCache会清空之前所有的缓存文件,而这个特性在实际开发中作用并不大,很多情况下既是应用版本号发生了变化缓存文件仍然是有效的,因此这个参数设为1比较好。
第三个参数表示单个节点所对应的数据的个数,一般设为1即可,
第四个参数表示缓存的总大小,比如50MB,当缓存大小超出这个设定值后,DiskLruCache会清楚一些缓存从而保证总大小不大于这个设定值:
DiskLruCache的缓存添加:
DiskLruCache的缓存添加的操作是通过Editor完成的。Editor表示一个缓存对象的编辑对象,这里仍以图片缓存举例子,首先需要获取图片url所对应的key,然后根据key就可以通过edit()来获取Editor对象,如果这个缓存正在被编辑,那么会返回null,即DiskLruCache不允许同时编辑一个缓冲对象,之所以要把url转换为key,是因为图片的url中很可能有特殊字符,这将影响url在Android中直接使用,一般采用url的md5值作为key:
将图片的url转化为key以后,就可以获取Editor对象了,对于这个key来说,如果当前不存在其他Editor对象,那么edit()就会返回一个新的Editor对象,通过他就可以得到一个文件输出流,需要注意的是,由于前面在DiskLruCache的open方法中设置了一个子节点只能有一个数据,因此下面的DISK_CACHE_INDEX常量直接设为0即可:
有了文件输出流,接下来就是,从网络下载图片时,图片就可以通过这个文件输出流写入到文件系统上:
经过上面的步骤,其实并没有真正的将图片写入文件系统,还必须通过Editor的commit()来提交写入操作,如果图片下载 过程发生了异常,那么还可以通过Editor的abort()来回退整个操作:
DiskLruCache的缓存查找:
和缓存添加过程类似,缓存找找过程也需要将url转换为key,然后通过DiskLruCache的get方法得到一个Snapshot对象,接着再通过Snapshot对象即可得到缓存的文件输入流,有了文件输入流,自然就可以得到Bitmap对象了,为了避免加载图片过程中导致的OOM问题,一般不建议直接加载原始图片。但是BitmapFactory.Options的方式压缩图片对FileInputStream的缩放存在问题,原因是,FileInputStream是一种 有序的文件流,而两次decodeStream调用印象了文件流的位置属性,导致了第二次decodeStream时得到的是null。为了解决这个问题,可以通过文件流来得到他所对应的文件描述符,然后再通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片,代码如下:
好了,就说到这里,主要介绍了一些缓存策略和高效加载bitmap的方式,后续会继续对于ImageLoader进行学习并解析。
因此,如何高效的加载bitmap是一个很重要也很容易被开发者忽略的问题。
Bitmap的高效加载:
如何加载一张图片呢?BitmapFactory类提供了四类方法:decodedFile,decodedResource,decodedStream和decodedByteArray,分别用于支持文件系统,资源,输入流以及字节数组中加载出一个Bitmap对象,其中decodedFile和decodedResource又间接调用了decodedStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个方法。高效加载Bitmap的核心就是采用BitmapFactory.Options来加载所需尺寸的图片。这里加色通过ImageView来显示图片,很多时候ImageView并没有图片的原始尺寸那么大,这个时候把整个图片加载进来后再射给ImageView,这显然是没有必要的,因为ImageView并没有办法显示原始的图片。通过Options参数就可以按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高加载时的性能。其缩放图片主要是用到了Options的inSampleSize参数,即采样率,当inSampleSize为1时,采样后的图片大小为图片的原始大小,当inSampleSize大于1时,比如2,那么采样后的图片其宽/高均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4
拿一张1024*1024像素的图片来说,嘉定采用ARGB8888格式存储,那么他占有的内存为1024*1024*4,即4MB,如果inSampleSize为2,那么采样后的图片其内存占用只有512*512*4即1MB。可以发现采样率inSampleSize必须是大于1的整数,图片才会有缩小的效果,并且采样率同时作用于宽/高,这将导致缩放后的图片大小以采样率的2次方形式递减。即inSampleSize=4是,那么缩放比例就是1/16。有一种 特殊情况,就是当inSampleSize小雨1时,其作用相当于1,即无缩放效果。
通过采样率即可有效的加载图片,那么到底如何获取采样率呢?
(1)将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片(只会解析 图片的原始宽/高信息,并不会去真正加载图片)
(2)从BitmapFactory.Options中取出图片的原始宽高信息。他们对英语outWitdh和outHeight参数
(3) 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
(4)将itmapFactory.Options的inJustDecodeBounds参数谁为false,然后重新加载图片。
public static Bitmap decodeSanpledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); } public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHieght = height / 2; final int halfWidth = width / 2; while ((halfHieght / inSampleSize) >= reqHeight && (halfWidth / inSampleSize >= reqWidth)) { inSampleSize *= 2; } } return inSampleSize; }
除了BitmapFactory的decodedResource方法,其他三个decode系列的方法也是支持采样加载的,并且处理方式也是类似的,但是decodeStream方法稍微特殊,后面会讲解。
Android中的缓存策略:
缓存策略在Android中有着广泛的使用场景,尤其在图片加载这个场景下,缓存策略变得非常重要。目前常用的一种缓存算法是LRU,LRU是近期最少使用算法,他的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存由两种:LruCache和DiskLruCache,前者用于实现内存缓存,而后者则充当了存储设备缓存,通过二者的结合就可以很方便的实现一个很高使用价值的ImageLoader。
LruCache:
LruCache是3.1所提供的一个缓存类,通过v4兼容包可以兼容到早起的版本,为了能够兼容至2.2版本,在使用LruCache的时候建议采用v4兼容包中提供的LruCache,而不要直接使用3.1提供的LruCacheLruCache是一个泛型类,他内部采用一个LinkedHashMap以强引用的方法存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作。接下来简答说一个几个引用:
强引用: 直接的对象引用
软引用: 当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收;
弱引用: 当一个对象只有弱引用存在时,此对象会随时被gc回收。
另外LruCache是线程安全的,因为他使用了LinkedHashMap:
public class LruCache<K, V> { private final LinkedHashMap<K, V> map;
功欲善其实,必先利器,接下来我们就看看LruCache的使用,首先典型的初始化:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // TODO Auto-generated method stub return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } };
只需要提供缓存的总容量大小并重写sizeOf方法即可。sizeOf方法的作用是计算缓存对象的大小,这里的大小的单位需要和总容量的单位保持一致。对于上面代码,总容量的大小为当前进程可用内存的1/8,单位为KB,sizeOf方法则完成了Bitmap对象的大小计算。
一些特殊情况,还需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用这个方法,因此可以在这个方法中完成一些资源回收(如果需要的话)
除了创建以外还有缓存的获取和添加。
mMemoryCache.get(key); mMemoryCache.put(key, value);
DiskLruCache:
DiskLruCache用于实现存储设备缓存,即磁盘缓存。他通过对缓存对象写入文件系统从而实现缓存效果。其源码地址:下载地址
DiskLruCache的创建:
DiskLruCache并不能通过构造方法来创建,他提供了open方法用于创建自身,如下所示:
public static DiskLruCache open(File directory,int appVersion,int valueCount,long manSize)
第一个参数表示磁盘缓存在文件系统中的存储路径,缓存路径可以选择SD卡上的缓存目录,具体是指/sdcard/Android/data/package_name/cache目录,其中package_name表示当前应用包名,当应用被卸载后,此目录会一并被删除。当然也可以选择SD卡上的其他指定目录,还可以选择data下的当前应用目录,具体可根据需要灵活设定。如果应用卸载后就希望删除缓存目录,那么就选择SD卡上的缓存目录,否则就选择SD卡上的其他目录。
第二个参数表示应用的版本号,一般设为1即可。当版本号发生变化时DiskLruCache会清空之前所有的缓存文件,而这个特性在实际开发中作用并不大,很多情况下既是应用版本号发生了变化缓存文件仍然是有效的,因此这个参数设为1比较好。
第三个参数表示单个节点所对应的数据的个数,一般设为1即可,
第四个参数表示缓存的总大小,比如50MB,当缓存大小超出这个设定值后,DiskLruCache会清楚一些缓存从而保证总大小不大于这个设定值:
private static final long DISK_CACHE_SIZE=1024*1024*50; File diskCacheDir=getDiskCacheDir(mContext,"bitmap"); if(!diskCacheDir.exists()){ diskCacheDir.mkdir(); } mDiskLruCache=DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
DiskLruCache的缓存添加:
DiskLruCache的缓存添加的操作是通过Editor完成的。Editor表示一个缓存对象的编辑对象,这里仍以图片缓存举例子,首先需要获取图片url所对应的key,然后根据key就可以通过edit()来获取Editor对象,如果这个缓存正在被编辑,那么会返回null,即DiskLruCache不允许同时编辑一个缓冲对象,之所以要把url转换为key,是因为图片的url中很可能有特殊字符,这将影响url在Android中直接使用,一般采用url的md5值作为key:
private String hashKeyFormUrl(String url) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(url.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (Exception e) { // TODO: handle exception cacheKey = String.valueOf(url.hashCode()); } return cacheKey; } private String bytesToHexString(byte[] digest) { // TODO Auto-generated method stub StringBuilder sb = new StringBuilder(); for (int i = 0; i < digest.length; i++) { String hex = Integer.toHexString(0xFF & digest[i]); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); }
将图片的url转化为key以后,就可以获取Editor对象了,对于这个key来说,如果当前不存在其他Editor对象,那么edit()就会返回一个新的Editor对象,通过他就可以得到一个文件输出流,需要注意的是,由于前面在DiskLruCache的open方法中设置了一个子节点只能有一个数据,因此下面的DISK_CACHE_INDEX常量直接设为0即可:
String key=hashKeyFormUrl(url); DiskLruCache.Editor editor=mDiskLruCache.edit(key); if(editor!=null){ OutputStream outputStream=editor.newOutputStream(DISK_CACHE_INDEX); }
有了文件输出流,接下来就是,从网络下载图片时,图片就可以通过这个文件输出流写入到文件系统上:
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(outputStream, IO_BUFFER_SIZE); int b; while ((b = in.read()) != -1) { out.write(b); } } catch (Exception e) { // TODO: handle exception } finally { if (urlConnection != null) { urlConnection.disconnect(); } MyUtils.close(out); MyUtils.close(in); } return false; }
经过上面的步骤,其实并没有真正的将图片写入文件系统,还必须通过Editor的commit()来提交写入操作,如果图片下载 过程发生了异常,那么还可以通过Editor的abort()来回退整个操作:
OutputStream outputStream=editor.newOutputStream(DISK_CACHE_INDEX); if(downloadUrlToStream(url,outputStream)){ editor.commit(); }else{ editor.abort(); } mDiskLruCache.flush();经过上面的步骤,图片就已经被正确的写入到文件系统中了,接下来图片的获取操作就不需要请求网络了。
DiskLruCache的缓存查找:
和缓存添加过程类似,缓存找找过程也需要将url转换为key,然后通过DiskLruCache的get方法得到一个Snapshot对象,接着再通过Snapshot对象即可得到缓存的文件输入流,有了文件输入流,自然就可以得到Bitmap对象了,为了避免加载图片过程中导致的OOM问题,一般不建议直接加载原始图片。但是BitmapFactory.Options的方式压缩图片对FileInputStream的缩放存在问题,原因是,FileInputStream是一种 有序的文件流,而两次decodeStream调用印象了文件流的位置属性,导致了第二次decodeStream时得到的是null。为了解决这个问题,可以通过文件流来得到他所对应的文件描述符,然后再通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片,代码如下:
Bitmap bitmap = null; String key = hashKeyFormUrl(url); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { FileInputStream fileInputStream = (FileInputStream) snapShot .getInputStream(DISK_CACHE_INDEX); FileDescriptor fileDescriptor = fileInputStream.getFD(); bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor( fileDescriptor, reqWidth, reqHeight); if(bitmap!=null){ addBitmapToMemoryCache(key,bitmap); } }
好了,就说到这里,主要介绍了一些缓存策略和高效加载bitmap的方式,后续会继续对于ImageLoader进行学习并解析。
相关文章推荐
- Gson.toJson()时内存溢出StackOverflowError
- 选定虚拟主机 性能凸显优势
- 修改一行代码提升 Postgres 性能 100 倍
- redis的hGetAll函数的性能问题(记Redis那坑人的HGETALL)
- 推荐Sql server一些常见性能问题的解决方法
- SQL Server误区30日谈 第9天 数据库文件收缩不会影响性能
- 和表值函数连接引发的性能问题分析
- SQLServer 2000 升级到 SQLServer 2008 性能之需要注意的地方之一
- 浅析SQL Server中的执行计划缓存(上)
- Enterprise Library for .NET Framework 2.0缓存使用实例
- 数据库性能优化三:程序操作优化提升性能
- PowerShell中编程清空IE缓存方法
- PowerShell中使用.NET将程序集加入全局程序集缓存
- VBS中的字符串连接的性能问题
- C#实现位图转换成图标的方法
- C#中缓存的基本用法总结
- ASP在ACCESS中模糊查询"内存溢出"的解决方法
- mysql 性能的检查和调优方法
- 数据库性能优化二:数据库表优化提升性能
- SQL语句性能优化(续)