一、简介
Bitmap在Android中指的是一张图片。
BitmapFactory提供了四种方法来加载图片:
decodeFile----------从文件加载一个Bitmap对象
decodeResource --从资源加载一个Bitmap对象
decodeStream -----从输入流加载一个Bitmap对象
decodeByteArray--从字节数组加载一个Bitmap对象
其中decodeFile和decodeResource有间接调用了decodeStream
二、Bitmap的高效加载
核心思想就是采用BitmapFactory.Options来加载所需尺寸的图片,通过BitmapFactory.Options可以 按照一定的采样率来加载缩小后的图片。主要用到了inSampleSize(采样率)参数。
当inSampleSize <= 1时,采样后的图片大小为图片的原始大小。
当inSampleSize > 1时,比如2。采样后的图片宽高均为图片的原始大小的1/2,像素为原图的1/4,占有的内存也为原图的1/4
使用采样率压缩图片的方法:
public Bitmap decodeSamplerBitmapFromResource(Resource res, int resId, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
//将inJustDecodeBounds设置为true时,BitmapFactory只会解析图片的原始宽高信息,并不会真正的加载图片
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calcImage(options, reqHeight, reqWidth);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public int calcImage(BitmapFactory.Options options, int reqWidth, int reqHeight) {
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
if(height > reqHeight || width > reqWidth){
int halfHeight = height / 2;
int halfWidth = width / 2;
while((halfHeight / inSampleSize) >= reqHeight && (halfWidth / width) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
//测试
mImageView.setImageBitmap(decodeSamplerBitmapFromResource(getResource(),R.id.testImage, 100, 100));
注意:使用decodeStream加载图片的时候会返回null的错误,主要是由于下面几个原因导致的:
1、流已经关闭
出现这个问题的主要原因是解析网络流的代码写在了流关闭后,只需要分析清楚流在什么时候关闭即可。
2、decodeStream调用了两次
这个原因是因为第一次decodeStream时已经操作过inputstream了,这时候流的操作位置已经移动了,如果再次
decodeStream则不是从 流的起始位置解析,所以无法解析出Bitmap对象。
3、decodeStream的BUG(android 2.2 以下的一个bug)
解决办法就是:
<1> 将获取的流写到文件中,然后再从文件中使用decodeFile加载图片(比较繁琐)
<2> 将输入流转化为byte,然后使用decodeByteArray加载图片
public byte[] getBytes(InputStream is) throws IOException {
ByteArrayOutputStream outstream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = is.read(buffer)) != -1) {
outstream.write(buffer, 0, len);
}
outstream.close();
return outstream.toByteArray();
}
<3> 将输入流转为FileDescriptor,然后使用decodeFileDescriptor加载图片
FileDescriptor fileDescriptor = inputstream.getFD();
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
三、图片的缓存策略
目前常用的缓存算法是LRU(Least Recently Used),LRU是最近最少使用算法。
它的核心思想就是当缓存满时,会优先淘汰那些近期最少使用的缓存对象,采用LRU算法的缓存有两种:LruCache和DiskLruCache
LruCache用于实现内存缓存
DiskLruCache 用于存储设备缓存
1、LruCache
LruCache是一个泛型类,内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,提供了get和put方法来完成缓存的获取和添加操作,当put数据时发现缓存满时,会调用trimToSize方法来移除较早使用的缓存对象,然后添加新的缓存对象。 trimToSize内部是一个循环去判断map中的的size是否大于maxSize, 如果不是就跳出循环,如果是就通过map.eldest()方法拿到最近最少使用的缓存对象,然后调用map.remove方法移除该对象,最后将缓存大小size减去移除数据的大小。
LruCache初始化:
int maxMemory = (int)(Runtime.getRunTime().getMaxMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache mMemoryCache = new LruCache<String, Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap bitmap){
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
获取缓存对象:
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
添加一个缓存对象
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。
在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。
一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(8004804)。
因此,这个缓存大小大概可以存储2.5页的图片。当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。
public void loadBitmap(int resId, ImageView imageView) {
String imageKey = String.valueOf(resId);
Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.image_placeholder);
BitmapTask task = new BitmapTask(imageView);
task.execute(resId);
}
}
BitmapTask 还要把新加载的图片的键值对放到缓存中。
class BitmapTask extends AsyncTask<Integer, Void, Bitmap> {
// 在后台加载图片。
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
}
2、DiskLruCache
DiskLruCache是用于存储设备缓存,即磁盘缓存。它通过将缓存对象写入文件系统来实现缓存
DiskLruCache初始化:
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50//50M
File diskCacheDir = getDiskCacheDir(context, "bitmap");
if(!diskCacheDir.exists()){
diskCacheDir.mkdirs();
}
DiskLruCache mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
缓存添加:
DiskLruCache缓存的添加是通过Editor完成的。
private static final long DISK_CACHE_INDEX = 0
String key = hashKeyFromUrl(url);//图片的URL需要使用md5进行转换,是因为URL中可能含有特殊字符
DiskLruCache.Editor editor = mDiskLruCache.editor(key);
if(editor != null){
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
//由上面代码可以看出使用editor获取到了输出流,那么当从网络下载图片时,就可以通过该输出流写到文件中了
public boolean downLoadUrlToStream(String urlString, OutputStream outputStream){
HttpUrlConnection conn = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try{
URL url = new URL(urlString);
conn = (HttpUrlConnection) url.openConnection(url);
in = new BufferedInputStream(conn.getInputStream());
out = new BufferedOutputStream(outputStream);
int b;
while((b = in.read()) != -1){
out.write(b);
}
return true;
}catch(Exception e){
}finally{
if(conn != null){
conn.disConnect();
}
out.close();
in.close();
}
return false;
}
//经过上面的步骤并没有真正的将图片写入本地文件中,还需要Editor的commit()方法来提交写入操作。
//如果图片下载出现异常,可以通过Editor的abort()方法来回退操作
if(downLoadUrlToStream(url,outputStream)){
editor.commit();
}else{
editor.abort();
}
mDiskLruCache.flush();
缓存查找:
DiskLruCache的查找是通过Snapshot对象完成的。
Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if(snapshot != null){
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = //使用采样率压缩图片,这里不使用decodeStream原因上面已经解释了
if(bitmap != null){
//dosomething
}
}
加载大量图片时滑动引起卡顿现象(如listview)
1、在adapter的getView方法中不要做耗时操作
2、可以判断当前列表是否处于滑动状态,仅当列表静止时才加载图片
recycle方法
Bitmap加载到内存里以后,是包含两部分内存区域的,一部分是Java部分的,一部分是C部分的。从recycle的源码解析
可以知道调用recycle()方法会释放C部分的内存,同时会清理数据对象的引用,但不是立即清理数据,只是给垃圾回收器
发送一个消息指令,让它在没有其他对象引用这个bitmap时进行垃圾回收,当调用recycle方法后这个bitmap就会被标记
为“dead”,这个时候再调用bitmap的相关的其他方法时,就会引起异常。同时这个操作是不可逆的,所以在调用recycle
方法时 一定要谨慎,因为在调用完这个方法后这个bitmap就被标位死亡状态。官网是不建议去主动调用recycle方法的,
因为垃圾回收器会再bitmap没有其他引用时会去回收它的。但在实际开发中时可以根据实际情况去调用该方法。具体情况
具体分析