Android性能优化之Bitmap的内存优化

一、BitmapFactory解析Bitmap的原理

BitmapFactory提供的解析Bitmap的静态工厂方法有如下五种:java

Bitmap decodeFile(...) Bitmap decodeResource(...) Bitmap decodeByteArray(...) Bitmap decodeStream(...) Bitmap decodeFileDescriptor(...)

其中经常使用的三个:decodeFile、decodeResource、decodeStream。 
decodeFile和decodeResource其实最终都是调用decodeStream方法来解析Bitmap,decodeStream的内部则是调用两个native方法解析Bitmap的:android

nativeDecodeAsset() nativeDecodeStream()

这两个native方法只是对应decodeFile和decodeResource、decodeStream来解析的,像decodeByteArray、decodeFileDescriptor也有专门的native方法负责解析Bitmap。api

接下来就是看看这两个方法在解析Bitmap时究竟有什么区别decodeFile、decodeResource,查看后发现它们调用路径以下:bash

decodeFile->decodeStream 
decodeResource->decodeResourceStream->decodeStream工具

decodeResource在解析时多调用了一个decodeResourceStream方法,而这个decodeResourceStream方法代码以下:布局

public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); }

它主要是对Options进行处理了,在获得opts.inDensity属性的前提下,若是咱们没有对该属性设定值,那么将opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;赋定这个默认的Density值,这个默认值为160,为标准的dpi比例,即在Density=160的设备上1dp=1px,这个方法中还有这么一行性能

opts.inTargetDensity = res.getDisplayMetrics().densityDpi;优化

对opts.inTargetDensity进行了赋值,该值为当前设备的densityDpi值,因此说在decodeResourceStream方法中主要作了两件事:ui

一、对opts.inDensity赋值,没有则赋默认值160 
二、对opts.inTargetDensity赋值,没有则赋当前设备的densityDpi值spa

以后重点来了,以后参数将传入decodeStream方法,该方法中在调用native方法进行解析Bitmap后会调用这个方法setDensityFromOptions(bm, opts);:

private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) { if (outputBitmap == null || opts == null) return; final int density = opts.inDensity; if (density != 0) { outputBitmap.setDensity(density); final int targetDensity = opts.inTargetDensity; if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) { return; } byte[] np = outputBitmap.getNinePatchChunk(); final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np); if (opts.inScaled || isNinePatch) { outputBitmap.setDensity(targetDensity); } } else if (opts.inBitmap != null) { // bitmap was reused, ensure density is reset outputBitmap.setDensity(Bitmap.getDefaultDensity()); } }

该方法主要就是把刚刚赋值过的两个属性inDensity和inTargetDensity给Bitmap进行赋值,不过并非直接赋给Bitmap就完了,中间有个判断,当inDensity的值与inTargetDensity或与设备的屏幕Density不相等时,则将应用inTargetDensity的值,若是相等则应用inDensity的值。

因此总结来讲,setDensityFromOptions方法就是把inTargetDensity的值赋给Bitmap,不过前提是opts.inScaled = true;

进过上面的分析,能够得出这样一个结论:

在不配置Options的状况下: 
一、decodeFile、decodeStream在解析时不会对Bitmap进行一系列的屏幕适配,解析出来的将是原始大小的图 
二、decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操做,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,而且Bitmap的大小将比原始的大

1.一、关于Density、分辨率、-hdpi等res目录之间的关系

DensityDpi 分辨率 res Density
160dpi 320x533 mdpi 1
240dpi 480x800 hdpi 1.5
320dpi 720x1280 xhdpi 2
480dpi 1080x1920 xxhdpi 3
560dpi 1440x2560 xxxhdpi 3.5

dp与px的换算公式为:

px = dp * Density

1.二、DisplayMetrics::densityDpi与density的区别

getResources().getDisplayMetrics().densityDpi——表示屏幕的像素密度 
getResources().getDisplayMetrics().density——1dp等于多少个像素(px)

举个栗子:在屏幕密度为160的设备下,1dp=1px。在屏幕密度为320的设备下,1dp=2px。 
因此这就为何在安卓中布局建议使用dp为单位,由于能够根据当前设备的屏幕密度动态的调整进行适配

二、Bitmap的优化策略

2.一、BitmapFactory.Options的属性解析

BitmapFactory.Options中有如下属性:

inBitmap——在解析Bitmap时重用该Bitmap,不过必须等大的Bitmap并且inMutable须为true inMutable——配置Bitmap是否能够更改,好比:在Bitmap上隔几个像素加一条线段 inJustDecodeBounds——为true仅返回Bitmap的宽高等属性 inSampleSize——须>=1,表示Bitmap的压缩比例,如:inSampleSize=4,将返回一个是原始图的1/16大小的Bitmap inPreferredConfig——Bitmap.Config.ARGB_8888等 inDither——是否抖动,默认为false inPremultiplied——默认为true,通常不改变它的值 inDensity——Bitmap的像素密度 inTargetDensity——Bitmap最终的像素密度 inScreenDensity——当前屏幕的像素密度 inScaled——是否支持缩放,默认为true,当设置了这个,Bitmap将会以inTargetDensity的值进行缩放 inPurgeable——当存储Pixel的内存空间在系统内存不足时是否能够被回收 inInputShareable——inPurgeable为true状况下才生效,是否能够共享一个InputStream inPreferQualityOverSpeed——为true则优先保证Bitmap质量其次是解码速度 outWidth——返回的Bitmap的宽 outHeight——返回的Bitmap的高 inTempStorage——解码时的临时空间,建议16*1024

2.二、优化策略

一、BitmapConfig的配置 
二、使用decodeFile、decodeResource、decodeStream进行解析Bitmap时,配置inDensity和inTargetDensity,二者应该相等,值能够等于屏幕像素密度*0.75f 
三、使用inJustDecodeBounds预判断Bitmap的大小及使用inSampleSize进行压缩 
四、对Density>240的设备进行Bitmap的适配(缩放Density) 
五、2.3版本inNativeAlloc的使用 
六、4.4如下版本inPurgeable、inInputShareable的使用 
七、Bitmap的回收

针对上面方案,把Bitmap解码的代码封装成了一个工具类,以下:

public class BitmapDecodeUtil { private static final int DEFAULT_DENSITY = 240; private static final float SCALE_FACTOR = 0.75f; private static final Bitmap.Config DEFAULT_BITMAP_CONFIG = Bitmap.Config.RGB_565; private static BitmapFactory.Options getBitmapOptions(Context context) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = true; options.inPreferredConfig = DEFAULT_BITMAP_CONFIG; options.inPurgeable = true; options.inInputShareable = true; options.inJustDecodeBounds = false; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { Field field = null; try { field = BitmapFactory.Options.class.getDeclaredField("inNativeAlloc"); field.setAccessible(true); field.setBoolean(options, true); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } int displayDensityDpi = context.getResources().getDisplayMetrics().densityDpi; float displayDensity = context.getResources().getDisplayMetrics().density; if (displayDensityDpi > DEFAULT_DENSITY && displayDensity > 1.5f) { int density = (int) (displayDensityDpi * SCALE_FACTOR); options.inDensity = density; options.inTargetDensity = density; } return options; } public static Bitmap decodeBitmap(Context context, int resId) { checkParam(context); return BitmapFactory.decodeResource(context.getResources(), resId, getBitmapOptions(context)); } public static Bitmap decodeBitmap(Context context, String pathName) { checkParam(context); return BitmapFactory.decodeFile(pathName, getBitmapOptions(context)); } public static Bitmap decodeBitmap(Context context, InputStream is) { checkParam(context); checkParam(is); return BitmapFactory.decodeStream(is, null, getBitmapOptions(context)); } public static Bitmap compressBitmap(Context context,int resId, int maxWidth, int maxHeight) { checkParam(context); final TypedValue value = new TypedValue(); InputStream is = null; try { is = context.getResources().openRawResource(resId, value); return compressBitmap(context, is, maxWidth, maxHeight); } catch (Exception e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } public static Bitmap compressBitmap(Context context, String pathName, int maxWidth, int maxHeight) { checkParam(context); InputStream is = null; try { is = new FileInputStream(pathName); return compressBitmap(context, is, maxWidth, maxHeight); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } public static Bitmap compressBitmap(Context context, InputStream is, int maxWidth, int maxHeight) { checkParam(context); checkParam(is); BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, null, opt); int height = opt.outHeight; int width = opt.outWidth; int sampleSize = computeSampleSize(width, height, maxWidth, maxHeight); BitmapFactory.Options options = getBitmapOptions(context); options.inSampleSize = sampleSize; return BitmapFactory.decodeStream(is, null, options); } private static int computeSampleSize(int width, int height, int maxWidth, int maxHeight) { int inSampleSize = 1; if (height > maxHeight || width > maxWidth) { final int heightRate = Math.round((float) height / (float) maxHeight); final int widthRate = Math.round((float) width / (float) maxWidth); inSampleSize = heightRate < widthRate ? heightRate : widthRate; } if (inSampleSize % 2 != 0) { inSampleSize -= 1; } return inSampleSize <= 1 ? 1 : inSampleSize; } private static <T> void checkParam(T param){ if(param == null) throw new NullPointerException(); } }

主要有两类方法: 
1、decodeBitmap:对Bitmap不压缩,可是会根据屏幕的密度合适的进行缩放压缩 
2、compressBimtap:对Bitmap进行超过最大宽高的压缩,同时也会根据屏幕的密度合适的进行缩放压缩。

三、Bitmap优化先后性能对比

针对上面方案,作一下性能对比,图片大小为3.26M,分辨率为2048*2048 
有两台设备:

3.一、density为320的设备

这里写图片描述

3.二、density为560的设备

这里写图片描述

能够看到,都是加载同一图片,在高屏幕像素密度的设备下所须要的内存须要很大、载入内存中的Bitmap的宽高也因设备的屏幕像素密度也改变,正如上面分析的同样,使用decodeResource会自动适配当前设备的分辨率达到一个最佳效果,而只有这个方法会自动适配其它方法将不会,依次思路,咱们在封装的工具类中在每个方法都加入了依屏幕像素密度来自动适配,而在实际中并不须要那么高清的图片,因此咱们能够根据设备的density来进行缩放,好比:在400>=density>240的状况下x0.8,在density>400的状况下x0.7,这样Bitmap所占用的内存将减小很是多,能够对面上面两个图片中bitmap和decodeBitmap两个值的大小,decodeBitmap只是对density进行了必定的缩放,而占用内存却减小很是多,并且显示效果也和原先的并没有区别。 
以后对比咱们进行了inSampleSize压缩的图片,进行压缩后的效果也看不出太大区别,而占用内存也减小了不少。

四、Bitmap的回收

4.一、Android 2.3.3(API 10)及如下的系统

在2.3如下的系统中,Bitmap的像素数据是存储在native中,Bitmap对象是存储在Java堆中的,因此在回收Bitmap时,须要回收两个部分的空间:native和java堆。 
即先调用recycle()释放native中Bitmap的像素数据,再对Bitmap对象置null,保证GC对Bitmap对象的回收

4.二、Android 3.0(API 11)及以上的系统

在3.0以上的系统中,Bitmap的像素数据和对象自己都是存储在java堆中的,无需主动调用recycle(),只需将对象置null,由GC自动管理

相关文章
相关标签/搜索