Android图片的高效加载与缓存策略

一、简介

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没有其他引用时会去回收它的。但在实际开发中时可以根据实际情况去调用该方法。具体情况
 具体分析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值