在实际的开发过程中,经常会对图片进行获取,并为用户展示在界面。由于android系统对每个应用有一定的内存限制,如果不合理的利用就会造成内存泄漏的情况。因此,在加载bitmap时进行恰当的优化,可以节省系统资源。另外,在进行网络、文件系统获取图片资源时,采用缓存机制,减少网络获取次数,会让应用更流畅,用户体验更友好。下面就从bitmap的高效加载、LruCache、DiskLruCache这三方面进行阐述:
Bitmap的加载
首先,先了解如何加载bitmap。bitmap是一张png、jpg等多种格式的图片,通过BitmapFactory的decodeFile、decodeResource、decodeStream、decodeByteArray四个方法分别从文件系统,资源,输入流以及字节数组中加载一个bitmap对象。这四类方法最终都在android的底层实现,对应着BitmapFactory的类的几个native方法。
那么如何高效的加载bitmap呢?其核心思想是通过BitmapFactory.Options来加载所需尺寸的图片。例如,当我们通过ImageView来加载图片时,若ImageView的尺寸要小于图片的尺寸,此时将图片按原比例放入imageView中,并不会完全显示。通过BitmapFactory.Options按一定的采样率来缩小图片,将缩小后的图片放入ImageView中显示,这样减少了内存的开销,一定程度上避免了OOM,提高了bitmap加载性能。
通过BitmapFactory.Options来缩放图片,主要用到了inSampleSize参数。当参数的值为1时,采样的大小为原始图片大小;当inSampleSize值大于1时,图片将被缩小,假设inSampleSize = 2,图片的长宽均被缩小为原始比例的1/2,像素并为原来的1/4,因此所在内存同样变为原来的1/4。值得注意的是,只有当inSampleSize的值大于1时,图片才会被缩小,由于长、宽被同时作用,图片总是被缩小为inSampleSize的2次方倍,即inSampleSize = 4,则图片将会被缩小为原比例的1/16。当inSampleSize的值小于1时,无效。实际情况中,假如ImageView的大小为100100像素,图片大小为200200,此时只要将inSampleSize值设为2即可。如果图片为200*300呢?此时inSampleSize的值还是设置为2比较合适,如果设置成3,那么图片尺寸将远小于ImageView,导致图片将被拉升,从而变得模糊。
通过采样率可以高效的加载bitmap,那么如何获取图片的采样率呢?
- 首先将BitmapFactory.Options的inJustDecodeBounds参数设置为true;
- 从BitmapFactory.Options中取出原始图片的宽高,对应于onWidth、onHeight;
- 根据目标view所需大小结合采样率规则,计算出inSampleSize 值;
- 将BitmapFactory.Options的inJustDecodeBounds参数设置为false,重新加载图片。
private Bitmap setBitmapImage(Resources res, int id, int width, int height){
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res,id,options);
//获取inSampleSize值
options.inSampleSize = getinSampleSize(options,width,height);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res,id,options);
}
private int getinSampleSize(BitmapFactory.Options options, int width, int height) {
int w = options.outWidth;
int h = options.outHeight;
int inSampleSize = 1;
if (w > width || h > height){
int halfw = w / 2;
int halfh = h / 2;
while ((halfw / inSampleSize) >= width && (halfh / inSampleSize) >= height){
inSampleSize *= 2;
}
}
return inSampleSize;
}
经过上面的步骤,加载出来的图片就是缩放后的图片,当然也有可能不需要缩放。值得一提的是,当inJustDecodeBounds参数为true时,BitmapFactory只会去解析原始图片的宽高,并不会真正的去加载图片,并且该操作是轻量级的。在使用的时候,假如ImageView的大小为100*100,则:
mImageView.setImageBitmap(setBitmapImage(getResources(),R.id.image,100,100));
上面介绍的BitmapFactory.decodeResource方法,其他三种加载Bitmap方法同样支持采样率的使用。在类似于网络获取图片时,一般会处理InputStream流,但是流只能应用一次,如果二次使用将返回null,处理办法是重写其mark()、reSet()方法,具体实现这里就不展开讲了;另一种方法是现将InputStream通过ByteArrayOutputStream缓存,
public static byte[] readStream(InputStream inStream) throws Exception {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
outStream.close();
inStream.close();
return outStream.toByteArray();
}
这样,再通过BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options)返回bitmap对象。
另外,
public void setImageResource(int resId){
InputStream is = getContext().getResources().openRawResource(resId);
Bitmap bmp = BitmapFactory.decodeStream(is);
//图片重新裁剪,原图从中心点按显示大小裁剪
int bw = bmp.getWidth(), bh = bmp.getHeight();
float scaleX = (float) WIDTH / bw;
float scaleY = (float) WIDTH / bh;
Matrix matrix = new Matrix();
matrix.setScale(scaleX,scaleY);
mBitmap = Bitmap.createBitmap(bmp,0,0,bw,bh,matrix,true);//width 裁剪bitmap的宽度;height 裁剪bitmap的高度
invalidate();
}
还可以通过上面的代码,采用Matrix 类来对图片进行缩放。但值得注意的是,这样做并没有改变图片的内存大小,而是三维空间的视觉缩放。
Android缓存策略
在应用的开发过程中,经常会涉及到网络请求,以获取图片、视频等资源。但是对于移动设备来说,数据流量的消耗对用户来说是很关心的,如何为用户减少网络请求,节省流量消耗,显得很有必要。因此,就引入了缓存的概念。以图片为例,当用户第一次网络获取图片后,通过缓存机制将图片存储到存储设备上,下次需要再次用到该图片时,直接从存储设备获取,不需要网络获取。多数情况下,为了提高应用的交互性,通常会将部分图片存储到内存中,再次使用时直接从内存获取,这样速度快于从存储设备、网络中获取。
上面引入了缓存机制,那么具体怎么实现呢?针对缓存机制,有不同的缓存策略,各种策略间并没有统一的标准,但基本包括添加、获取以及删除操作。添加和获取比较好理解,但是为什么还要删除呢?我们都知道,对于手机等移动设备来说,硬件是有限的,特别是存储能力,不可能达到无穷大。因此,在进行缓存时,当缓存容量满时,需要进行旧缓存的删除,以便进行新缓存。如何进行新旧文件的替换,这里就需要缓存算法的支持。目前最常用的缓存算法为近期最少使用算法LRU,核心思想为当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用Lru缓存算法的缓存机制有两种:LruCache和DiskLruCache,LruCache实现内存缓存,DiskLruCache实现储存设备缓存。
-
LruCache
LruCache是一个泛型类,内部采用LinkedHashMap以强引用的方式存储外部缓存对象,提供get和put方法来实现缓存对象的获取和添加,当缓存满时,LruCache会移除较早使用的缓存对象,加入新的缓存对象。介绍下几个概念:1、强引用:直接的对象引用
2、软引用:当一个对象只有软引用存在时,内存资源不足时,此对象会被gc回收;
3、 弱引用:当一个对象只有弱引用存在时,此对象随时会被gc回收;
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
LruCache是线程安全的,源码也比较简单。下面介绍其基本使用:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int memory = maxMemory / 8;
mImageCache = new LruCache<String, Bitmap>(memory){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight() / 1024;
}
};
上面的代码,先计算出当前进程的可用内存,设置缓存大小为可用内存大小的八分之一,通过sizeOf方法完成对bitmap对象大小的判断。除了LruCache的创建外,LruCache还包括缓存对象的添加,获取,删除等。
mImageCache.put(key,value);
mImageCache.get(key);
mImageCache.remove(key);
- DisLruCache
DisLruCache用于实现磁盘缓存,通过将缓存对象写入文件系统而实现缓存的效果。值得注意的是,虽然DisLruCache得到了Android官方文档的推荐,但其并不包括在SDK中。当我们需要使用DisLruCache是,需要手动下载源文件,并引入项目中,下载地址:
android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java
如果不能访问可以点击这里进行下载。下面分别从DiskLruCache的创建、添加以及查找来描述其使用。
1、DiskLruCache的创建
DiskLruCache的创建并不能通过构造方法实现,
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
四个参数中,directory表示磁盘缓存对象所存储的文件系统路径,通常都会存放在 /sdcard/Android/data/ 包名 /cache 这个路径下面,当然,我们还可以选择支持的其他路径。当应用被卸载后,缓存文件也将被删除。
第二个参数appVersion表示应用版本号,一般设为1即可。当应用版本号发生变化时,系统内关于该应用的缓存将被清除。
第三个参数表示单个节点所对应数据的个数,一般设为1即可。第四个参数表示缓存的总大小,当缓存大小超过这个值后,DiskLruCache会清除一些缓存对象,以不大于这个值。
File dir = new File("/sdcard/Android/data/com.example.huangzheng.bitmaptest/cache");
int maxSize = 1024 * 1024 * 50;
if (!dir.exists()){
dir.mkdir();
}
try {
mDiskCache = DiskLruCache.open(dir,1,1,maxSize);
} catch (IOException e) {
e.printStackTrace();
}
2、DiskLruCache的缓存添加
还是一样的,我们以网络获取一张图片,并写入磁盘缓存为例。首先通过下面的代码获取网络图片,并通过OutputStream写入本地。
//网络获取图片,并写入输出流
private boolean downloadUrlToStream(String urlstring, OutputStream outputstream){
HttpURLConnection httpconnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlstring);
httpconnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(httpconnection.getInputStream(),8*1024);
out = new BufferedOutputStream(outputstream,8*1024);
int b;
if ((b = in.read()) != -1){
out.write(b);
}
return true;
}catch (IOException e) {
e.printStackTrace();
} finally {
if (httpconnection != null){
httpconnection.disconnect();
}
try {
if (out != null){
out.close();
}
if (in != null){
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
DiskLruCache的写入操作是通过其Editor 类实现,Editor 不能new,需要调用DiskLruCache的edit方法来创建。
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
从edit方法可以看到,需要传入key值,通常情况下图片的url中包括一些特殊字符,比较常规的做法是通过MD5进行编码,保证字符串的唯一性