Bitmap压缩、缓存、复用
我们平时在android的开发中,总是会和图片打交道;提到图片最先想到的就是被内存问题支配的恐惧,尤其是手机相机的分辨率越来越高时,这种恐惧尤为明显;当然我们的前辈们创造了非常优秀的轮子(如Glide等),但是我们在自定义View和直接处理bitmap的时候还是会碰到内存相关的困扰,下面就来直面恐惧学习一下bitmap相关的知识吧
Bitmap压缩
相关知识
- 通过bitmap可以获取图片的信息
- 可以对bitmap进行缩放、裁剪等操作
- bitmap加载方式:
BitmapFactory.decodeByteArray();
BitmapFactory.decodeFile();
BitmapFactory.decodeResource();
BitmapFactory.decodeStream();
- 2.3.3(api10)之前Bitmap解码之后的数据储存在Native Memory中,垃圾回收无法回收占用的内存,所以需要手动调用Recycle进行回收
- 3.0(api 11)之后Bitmap解码之后的数据储存在Dalvik heap中,内存回收可以交给GC,不用手动调用Recycle进行回收内存
- 大量加载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. 采样率压缩(推荐使用)
- 采样率压缩是推荐使用的压缩方式,我们可以通过设置BitmapFactory.Options的inSampleSize值调整生成bitmap的时候的采样率;
- inSampleSize的原理就是在该值个像素中选择一个像素点;假设inSampleSize为4,生成的bitmap的大小就是原图的四分之一大小
- 设置的inSampleSize在代码底层将变成一个最近的 2的n次方,例如inSampleSize为9,最终的bitmap大小将是原图的八分之一
- 想要突破以上限制可以设置Options的inScaled、inDensity、inTargetDensity来达到任意分之一大小
- 设置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);
- 质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,这也是为什么该方法叫质量压缩方法。那么,图片的长,宽,像素都不变,那么bitmap所占内存大小是不会变的。
- 如果是bit.compress(CompressFormat.PNG, quality, baos);这样的png格式,quality就没有作用了,bytes.length不会变化,因为png图片是无损的,不能进行压缩。
总结
- 采样率压缩通过降低分辨率的方式,降低了内存占用
- 质量压缩只能压缩在磁盘中占用空间,不能降低内存占用
- 缩放压缩也是通过降低分辨率进行压缩,但是缩放压缩的前提是已经存在bitmap,bitmap已经占用内存,不适合加载前的压缩方式
- 日常开发中可以多种方式搭配使用,以达到最佳效果(例如,对于上传图片需求,可以先进行采样率压缩,在进行质量压缩;对于加载图片可以进行采样率压缩配合占用字节较小的解码模式)
图片缓存
- 对于不经常改变的数据可以采用缓存策略
- 缓存可以提升响应速度
- 缓存可以减轻服务器的压力
1. LruCache(内存缓存)
- 近期最少使用算法
- 内部采用的是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(磁盘缓存)
- 由square团队开发
- 使用
- 通过DiskLruCache.open去初始化缓存对象
- 通过DiskLruCache.get(String key)去获取到对应key下的缓存数据
- 通过DiskLruCache.Editor对象将数据保存到本地
- 根据外置储存设置合适的缓存路径
- 有外置: /sdcard/Android/data//cache
- 无外置: /data/data/Android/data//cache
- 只能使用英文字母和数字作为key
- 由于DiskLruCache并不是由Google官方编写的,所以这个类并没有被包含在Android API当中,我们需要将这个类从网上下载下来,然后手动添加到项目当中。DiskLruCache的源码在Google Source上,地址如下:
android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java
- 郭霖很多年前写过一篇详细的教程,地址如下
https://blog.csdn.net/guolin_blog/article/details/28863651
下面是一些摘抄的代码
- 使用size()方法可以获取缓存的大小
- flush()
这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候我有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了 - close()方法和open对应,通常在activity的onDestroy()方法中调用
- 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
- 使用bitmapOptions.inBitmap 可以实现复用之前的内存空间,只能在3.0以后使用
- 4.4之前的版本inBitmap会有一些限制,新的bitmap只能复用大小相等的bitmap的内存;4.4(SDk19)以后只要老的bitmap占用内存比新的bitmap大
- 下面是google关于inBitmap的介绍视频
https://www.youtube.com/watch?v=_ioFW3cyRV0&index=17&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE
- 使用inBitmap的Demo:
https://developer.android.com/topic/performance/graphics/manage-memory.html#java
- 详细了解可以参考这篇文章
https://my.oschina.net/u/3863980/blog/3019921
- 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();
}