Bitmap压缩、缓存、复用 (inpreferredconfig,inSampleSize,inTargetDensity,LruCache,DiskLruCache,inBitmap)

本文详细介绍了Android中处理Bitmap时的优化技巧,包括Bitmap的压缩方法(采样率压缩、缩放压缩和质量压缩)、内存缓存(LruCache)和磁盘缓存(DiskLruCache)的使用,以及Bitmap的复用技术。通过理解这些知识,可以有效避免内存问题,提高应用性能。
摘要由CSDN通过智能技术生成

Bitmap压缩、缓存、复用

我们平时在android的开发中,总是会和图片打交道;提到图片最先想到的就是被内存问题支配的恐惧,尤其是手机相机的分辨率越来越高时,这种恐惧尤为明显;当然我们的前辈们创造了非常优秀的轮子(如Glide等),但是我们在自定义View和直接处理bitmap的时候还是会碰到内存相关的困扰,下面就来直面恐惧学习一下bitmap相关的知识吧

Bitmap压缩

相关知识

  1. 通过bitmap可以获取图片的信息
  2. 可以对bitmap进行缩放、裁剪等操作
  3. bitmap加载方式:
BitmapFactory.decodeByteArray();
BitmapFactory.decodeFile();
BitmapFactory.decodeResource();
BitmapFactory.decodeStream();
  1. 2.3.3(api10)之前Bitmap解码之后的数据储存在Native Memory中,垃圾回收无法回收占用的内存,所以需要手动调用Recycle进行回收
  2. 3.0(api 11)之后Bitmap解码之后的数据储存在Dalvik heap中,内存回收可以交给GC,不用手动调用Recycle进行回收内存
  3. 大量加载bitmap会占用内存,导致内存抖动,容易进一步导致OOM的发生

图片压缩

0. 解码模式和图片格式
图片格式

Android目前常用的图片格式有png,jpeg和webp,

png:无损压缩图片格式,支持Alpha通道,Android切图素材多采用此格式

jpeg:有损压缩图片格式,不支持背景透明,适用于照片等色彩丰富的大图压缩,不适合logo

webp:是一种同时提供了有损压缩和无损压缩的图片格式,派生自视频编码格式VP8,从 谷歌官网 来看,无损webp平均比png小26%,有损的webp平均比jpeg小25%~34%,无损webp支持Alpha通道,有损webp在一定的条件下同样支持,有损webp在Android4.0(API 14)之后支持,无损和透明在Android4.3(API18)之后支持

解码模式

参数inpreferredconfig的可选值有四个,分别为ALPHA_8,RGB_565,ARGB_4444,ARGB_8888。它们的含义列举如下。

参数取值含义
ALPHA_8图片中每个像素用一个字节(8位)存储,该字节存储的是图片8位的透明度值
RGB_565图片中每个像素用两个字节(16位)存储,两个字节中高5位表示红色通道,中间6位表示绿色通道,低5位表示蓝色通道
ARGB_4444图片中每个像素用两个字节(16位)存储,Alpha,R,G,B四个通道每个通道用4位表示
ARGB_8888图片中每个像素用四个字节(32位)存储,Alpha,R,G,B四个通道每个通道用8位表示
1. 采样率压缩(推荐使用)
  1. 采样率压缩是推荐使用的压缩方式,我们可以通过设置BitmapFactory.Options的inSampleSize值调整生成bitmap的时候的采样率;
  2. inSampleSize的原理就是在该值个像素中选择一个像素点;假设inSampleSize为4,生成的bitmap的大小就是原图的四分之一大小
  3. 设置的inSampleSize在代码底层将变成一个最近的 2的n次方,例如inSampleSize为9,最终的bitmap大小将是原图的八分之一
  4. 想要突破以上限制可以设置Options的inScaled、inDensity、inTargetDensity来达到任意分之一大小
  5. 设置inScaled之后采用的算法会更加复杂,时间会更久,所以我们可以先使用inSampleSize压缩到一定大小后在使用inScaled进一步压缩
public static Bitmap ratio(Activity context, String filename, int pixeW, int pixeH) {
        Bitmap bitmap = null;
        try {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(filename, options);

            int targetDensity = context.getResources().getDisplayMetrics().densityDpi;

            DisplayMetrics dm = new DisplayMetrics();
            context.getWindowManager().getDefaultDisplay().getMetrics(dm);

            int x = dm.widthPixels;
            int y = dm.heightPixels;
            options.inSampleSize = calculateInSampleSize(options, x, y);

            double xSScale = ((double)options.outWidth) / ((double)x);
            double ySScale = ((double)options.outHeight) / ((double)y);

            double startScale = xSScale > ySScale ? xSScale : ySScale;

            options.inScaled = true;
            options.inDensity = (int) (targetDensity*startScale);
            options.inTargetDensity = targetDensity;
            options.inJustDecodeBounds = false;
            bitmap = BitmapFactory.decodeFile(filename, options);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return bitmap;
    }

    public static int calculateInSampleSize(BitmapFactory.Options options,
                                     int reqWidth, int reqHeight) {
        // Raw height and width of image  
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and  
            // keeps both  
            // height and width larger than the requested height and width.  
            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }
2. 缩放法压缩(martix)
        Matrix matrix = new Matrix();
        matrix.setScale(0.5f, 0.5f);
        bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),
        bit.getHeight(), matrix, true);
3. 质量压缩(bitmap占用内存不会减少)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int quality = 100;
bit.compress(CompressFormat.JPEG, quality, baos);
byte[] bytes = baos.toByteArray();
bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
Log.i("wechat", "压缩后图片的大小" + (bm.getByteCount() / 1024 / 1024)
    + "M宽度为" + bm.getWidth() + "高度为" + bm.getHeight()
    + "bytes.length= " + (bytes.length / 1024) + "KB"
    + "quality=" + quality);
  1. 质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,这也是为什么该方法叫质量压缩方法。那么,图片的长,宽,像素都不变,那么bitmap所占内存大小是不会变的。
  2. 如果是bit.compress(CompressFormat.PNG, quality, baos);这样的png格式,quality就没有作用了,bytes.length不会变化,因为png图片是无损的,不能进行压缩。
总结
  1. 采样率压缩通过降低分辨率的方式,降低了内存占用
  2. 质量压缩只能压缩在磁盘中占用空间,不能降低内存占用
  3. 缩放压缩也是通过降低分辨率进行压缩,但是缩放压缩的前提是已经存在bitmap,bitmap已经占用内存,不适合加载前的压缩方式
  4. 日常开发中可以多种方式搭配使用,以达到最佳效果(例如,对于上传图片需求,可以先进行采样率压缩,在进行质量压缩;对于加载图片可以进行采样率压缩配合占用字节较小的解码模式)

图片缓存

  1. 对于不经常改变的数据可以采用缓存策略
  2. 缓存可以提升响应速度
  3. 缓存可以减轻服务器的压力
1. LruCache(内存缓存)
  1. 近期最少使用算法
  2. 内部采用的是LinkHashMap
private LruCache<String, Bitmap> mMemoryCache;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
	// 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
	// LruCache通过构造函数传入缓存值,以KB为单位。
	int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
	// 使用最大可用内存值的1/8作为缓存的大小。
	int cacheSize = maxMemory / 8;
	mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
		@Override
		protected int sizeOf(String key, Bitmap bitmap) {
			// 重写此方法来衡量每张图片的大小,默认返回图片数量。
			return bitmap.getByteCount() / 1024;
		}
	};
}
 
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
	if (getBitmapFromMemCache(key) == null) {
		mMemoryCache.put(key, bitmap);
	}
}
 
public Bitmap getBitmapFromMemCache(String key) {
	return mMemoryCache.get(key);
}
2. DiskLruCache(磁盘缓存)
  1. 由square团队开发
  2. 使用
  • 通过DiskLruCache.open去初始化缓存对象
  • 通过DiskLruCache.get(String key)去获取到对应key下的缓存数据
  • 通过DiskLruCache.Editor对象将数据保存到本地
  1. 根据外置储存设置合适的缓存路径
  • 有外置: /sdcard/Android/data//cache
  • 无外置: /data/data/Android/data//cache
  1. 只能使用英文字母和数字作为key
  2. 由于DiskLruCache并不是由Google官方编写的,所以这个类并没有被包含在Android API当中,我们需要将这个类从网上下载下来,然后手动添加到项目当中。DiskLruCache的源码在Google Source上,地址如下:

android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java

  1. 郭霖很多年前写过一篇详细的教程,地址如下
    https://blog.csdn.net/guolin_blog/article/details/28863651

下面是一些摘抄的代码

  1. 使用size()方法可以获取缓存的大小
  2. flush()
    这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候我有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了
  3. close()方法和open对应,通常在activity的onDestroy()方法中调用
  4. delete()方法会删除所有的缓存数据,可以在手动清理缓存的功能处调用
//写一个方法来获取缓存地址,如下所示:
public File getDiskCacheDir(Context context, String uniqueName) {
	String cachePath;
	if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
			|| !Environment.isExternalStorageRemovable()) {
		cachePath = context.getExternalCacheDir().getPath();
	} else {
		cachePath = context.getCacheDir().getPath();
	}
	return new File(cachePath + File.separator + uniqueName);
}
//接着是应用程序版本号,我们可以使用如下代码简单地获取到当前应用程序的版本号:
public int getAppVersion(Context context) {
		try {
			PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
			return info.versionCode;
		} catch (NameNotFoundException e) {
			e.printStackTrace();
		}
		return 1;
	}
//需要注意的是,每当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。
//一个非常标准的open()方法就可以这样写:	
DiskLruCache mDiskLruCache = null;
try {
	File cacheDir = getDiskCacheDir(context, "bitmap");
	if (!cacheDir.exists()) {
		cacheDir.mkdirs();
	}
	mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
} catch (IOException e) {
	e.printStackTrace();
}
//写一个方法用来将字符串进行MD5编码,代码如下所示:
public String hashKeyForDisk(String key) {
	String cacheKey;
	try {
		final MessageDigest mDigest = MessageDigest.getInstance("MD5");
		mDigest.update(key.getBytes());
		cacheKey = bytesToHexString(mDigest.digest());
	} catch (NoSuchAlgorithmException e) {
		cacheKey = String.valueOf(key.hashCode());
	}
	return cacheKey;
}
 
private String bytesToHexString(byte[] bytes) {
	StringBuilder sb = new StringBuilder();
	for (int i = 0; i < bytes.length; i++) {
		String hex = Integer.toHexString(0xFF & bytes[i]);
		if (hex.length() == 1) {
			sb.append('0');
		}
		sb.append(hex);
	}
	return sb.toString();
}
//一次完整写入操作的代码如下所示:
new Thread(new Runnable() {
	@Override
	public void run() {
		try {
			String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
			String key = hashKeyForDisk(imageUrl);
			DiskLruCache.Editor editor = mDiskLruCache.edit(key);
			if (editor != null) {
				OutputStream outputStream = editor.newOutputStream(0);
				if (downloadUrlToStream(imageUrl, outputStream)) {
					editor.commit();
				} else {
					editor.abort();
				}
			}
			mDiskLruCache.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}).start();
//一段完整的读取缓存,并将图片加载到界面上的代码如下所示:

try {
	String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
	String key = hashKeyForDisk(imageUrl);
	DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
	if (snapShot != null) {
		InputStream is = snapShot.getInputStream(0);
		Bitmap bitmap = BitmapFactory.decodeStream(is);
		mImage.setImageBitmap(bitmap);
	}
} catch (IOException e) {
	e.printStackTrace();
}

复用Bitmap

  1. 使用bitmapOptions.inBitmap 可以实现复用之前的内存空间,只能在3.0以后使用
  2. 4.4之前的版本inBitmap会有一些限制,新的bitmap只能复用大小相等的bitmap的内存;4.4(SDk19)以后只要老的bitmap占用内存比新的bitmap大
  3. 下面是google关于inBitmap的介绍视频

https://www.youtube.com/watch?v=_ioFW3cyRV0&index=17&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE

  1. 使用inBitmap的Demo:

https://developer.android.com/topic/performance/graphics/manage-memory.html#java

  1. 详细了解可以参考这篇文章

https://my.oschina.net/u/3863980/blog/3019921

  1. Google官方文档已经给出了一个非常棒的教程Managing Bitmap Memory

另外非常推荐Google官方文档的这个系列 Displaying Bitmaps Efficiently 绝对帮你精通bitmap的操作。


@TargetApi(Build.VERSION_CODES.HONEYCOMB)
		private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
		 
	    	options.inMutable = true;
	    	if (cache != null) {
	    		Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
	    		
	    		if (inBitmap != null) {
	                options.inBitmap = inBitmap;
	            }
	    	}
	    }

protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
		Bitmap bitmap = null;
		
		if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
			final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();
			Bitmap item;
			
			while (iterator.hasNext()) {
				item = iterator.next().get();
				
				if (null != item && item.isMutable()) {
					if (canUseForInBitmap(item, options)) {
						
						Log.v("TEST", "canUseForInBitmap!!!!");
						
                        bitmap = item;

                        // Remove from reusable set so it can't be used again
                        iterator.remove();
                        break;
                    }
				} else {
					// Remove from the set if the reference has been cleared.
					iterator.remove();
				}
			}
		}
		
		return bitmap;
	}
	
	@TargetApi(VERSION_CODES.KITKAT)
	private static boolean canUseForInBitmap(
			Bitmap candidate, BitmapFactory.Options targetOptions) {
		
		//4.4之前的版本,尺寸必须完全吻合
		if (Build.VERSION.SDK_INT < VERSION_CODES.KITKAT) {
			return candidate.getWidth() == targetOptions.outWidth
                    && candidate.getHeight() == targetOptions.outHeight
                    && targetOptions.inSampleSize == 1;
		}
		//4.4版本,可以使用比自己大的bitmap
		int width = targetOptions.outWidth / targetOptions.inSampleSize;
		int height = targetOptions.outHeight / targetOptions.inSampleSize;
		
		//根据图片格式,计算具体的bitmap大小
		int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
		
		return byteCount <= candidate.getAllocationByteCount();
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值