内存优化二Bitmap优化
Posted 小图包
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了内存优化二Bitmap优化相关的知识,希望对你有一定的参考价值。
Bitmap基础知识
一张图片Bitmap所占用的内存 = 图片长度 x 图片宽度 x 一个像素点占用的字节数
而Bitmap.Config,正是指定单位像素占用的字节数的重要参数。
其中,A代表透明度;R代表红色;G代表绿色;B代表蓝色。
ALPHA_8
表示8位Alpha位图,即A=8,一个像素点占用1个字节,它没有颜色,只有透明度
ARGB_4444
表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位,2个字节
ARGB_8888
表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位,4个字节
RGB_565
表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位,2个字节
一张图片Bitmap所占用的内存 = 图片长度 x 图片宽度 x 一个像素点占用的字节数
根据以上的算法,可以计算出图片占用的内存,以100*100像素的图片为例
BitmapFactory解析Bitmap的原理
BitmapFactory提供的解析Bitmap的静态工厂方法有以下五种:
Bitmap decodeFile(...)
Bitmap decodeResource(...)
Bitmap decodeByteArray(...)
Bitmap decodeStream(...)
Bitmap decodeFileDescriptor(...)
其中常用的三个:decodeFile、decodeResource、decodeStream。
decodeFile和decodeResource其实最终都是调用decodeStream方法来解析Bitmap
decodeFile方法代码:
public static Bitmap decodeFile(String pathName, Options opts)
Bitmap bm = null;
InputStream stream = null;
try
stream = new FileInputStream(pathName);
bm = decodeStream(stream, null, opts);
catch (Exception e)
/* do nothing.
If the exception happened on open, bm will be null.
*/
Log.e("BitmapFactory", "Unable to decode stream: " + e);
finally
if (stream != null)
try
stream.close();
catch (IOException e)
// do nothing here
decodeResource方法的代码:
public static Bitmap decodeResource(Resources res, int id, Options opts)
Bitmap bm = null;
InputStream is = null;
try
final TypedValue value = new TypedValue();
is = res.openRawResource(id, value);
bm = decodeResourceStream(res, value, is, null, opts);
catch (Exception e)
/* do nothing.
If the exception happened on open, bm will be null.
If it happened on close, bm is still valid.
*/
finally
try
if (is != null) is.close();
catch (IOException e)
// Ignore
if (bm == null && opts != null && opts.inBitmap != null)
throw new IllegalArgumentException("Problem decoding into existing bitmap");
return bm;
decodeStream的逻辑如下:
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
// we don't throw in this case, thus allowing the caller to only check
// the cache, and not force the image to be decoded.
if (is == null)
return null;
Bitmap bm = null;
Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
try
if (is instanceof AssetManager.AssetInputStream)
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
bm = nativeDecodeAsset(asset, outPadding, opts);
else
bm = decodeStreamInternal(is, outPadding, opts);
if (bm == null && opts != null && opts.inBitmap != null)
throw new IllegalArgumentException("Problem decoding into existing bitmap");
setDensityFromOptions(bm, opts);
finally
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
return bm;
private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts)
// ASSERT(is != null);
byte [] tempStorage = null;
if (opts != null) tempStorage = opts.inTempStorage;
if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
return nativeDecodeStream(is, tempStorage, outPadding, opts);
从上面的代码可以看出,decodeStream的代码最终会调用以下两个native方法之一
ativeDecodeAsset()
nativeDecodeStream()
这两个native方法只是对应decodeFile和decodeResource、decodeStream来解析的,像decodeByteArray、decodeFileDescriptor也有专门的native方法负责解析Bitmap。
decodeFile、decodeResource的区别在于他们方法的调用路径不同:
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方法中主要做了两件事:
1.对opts.inDensity赋值,没有则赋默认值160
2.对opts.inTargetDensity赋值,没有则赋当前设备的densityDpi值
之后参数将传入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的情况下:
1.decodeFile、decodeStream在解析时不会对Bitmap进行一系列的屏幕适配,解析出来的将是原始大小的图
2.decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,并且Bitmap的大小将比原始的大
经过上面的分析,我们可以得出Bitmap优化的思路:
1、BitmapConfig的配置
2、使用decodeFile、decodeResource、decodeStream进行解析Bitmap时,配置inDensity和inTargetDensity,两者应该相等,值可以等于屏幕像素密度*0.75f
3、使用inJustDecodeBounds预判断Bitmap的大小及使用inSampleSize进行压缩
4、Bitmap的回收
所以我们根据以上的思路,我们将Bitmap优化的策略总结为以下4种:
1.对图片质量进行压缩
2.对图片尺寸进行压缩
3.采样率压缩
4.使用libjpeg.so库进行压缩
Bitamp质量压缩
通过算法抠掉(同化)了图片中的一些某个些点附近相近的像素,达到降低质量介绍文件大小的目的。减小了图片质量,缩小文件大小。
通过Bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);方式降低图片质量
public static Bitmap compressImage(Bitmap bitmap)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
int options = 100;
//循环判断如果压缩后图片是否大于50kb,大于继续压缩
while ( baos.toByteArray().length / 1024>50)
//清空baos
baos.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);
options -= 10;//每次都减少10
//把压缩后的数据baos存放到ByteArrayInputStream中
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
//把ByteArrayInputStream数据生成图片
Bitmap newBitmap = BitmapFactory.decodeStream(isBm, null, null);
return newBitmap;
尺寸压缩
public static void compressBitmapToFileBySize(Bitmap bmp,File file)
//压缩尺寸倍数,值越大,图片的尺寸就越小
int ratio = 4;
//压缩Bitmap到对应尺寸
Bitmap result = Bitmap.createBitmap(bmp.getWidth()/ratio, bmp.getHeight()/ratio, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
RectF rect = new RectF(0, 0, bmp.getWidth()/ratio, bmp.getHeight()/ratio);
canvas.drawBitmap(bmp, null, rect , null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//把压缩后的数据存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
try
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
catch (Exception e)
e.printStackTrace();
采样率压缩
/**
* 设置图片的采样率,降低图片像素
* @param filePath
* @param file
*/
public static void compressBitmap(String filePath, File file)
// 数值越高,图片像素越低
int inSampleSize = 8;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
//options.inJustDecodeBounds = true;//为true的时候不会真正加载图片,而是得到图片的宽高信息。
//采样率,必须是2的倍数
options.inSampleSize = inSampleSize;
Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
bitmap.compress(Bitmap.CompressFormat.JPEG, 100 ,baos);
try
if(file.exists())
file.delete();
else
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
catch (Exception e)
e.printStackTrace();
libjpeg.so库压缩
libjpeg是广泛使用的开源JPEG图像库,安卓也依赖libjpeg来压缩图片。但是安卓并不是直接封装的libjpeg,而是基于了另一个叫Skia的开源项目来作为的图像处理引擎。Skia是谷歌自己维护着的一个大而全的引擎,各种图像处理功能均在其中予以实现,并且广泛的应用于谷歌自己和其它公司的产品中(如:Chrome、Firefox、 android等)。Skia对libjpeg进行了良好的封装,基于这个引擎可以很方便为操作系统、浏览器等开发图像处理功能
Bitmap图片复用机制
Android提供的图片复用机制
inBitmap图片复用的思路是:当生成的Bitmap不再使用时,将不再使用的Bitmap缓存起来,需要生成新Bitmap时,不再分配新内存,而是,直接拿符合条件缓存的Bitmap修改其数据,之后,将新生成的Bitmap给业务方使用,这样就避免了新Bitmap的创建。
根据官方的建议,在 Android 2.3 及以下的版本中建议使用 recycle() 回收内存,防止 OOM. 但是,使用这个方法的前提是需要确保这个位图不再被使用,否则回收之后再使用将会导致运行时错误。所以,官方的建议是通过引用计数的方式统计位图的引用,只有当位图不再被引用的时候再真正调用该方法进行回收。
官方文档参考:https://developer.android.com/topic/performance/graphics/manage-memory
在 Android 3.0 上面引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项,那么采用 Options 对象的解码方法会在加载内容时尝试重复使用现有位图。这样可以复用现有的 Bitmap,减少对象创建,从而减少发生 GC 的概率。不过,inBitmap 的使用方式存在某些限制。特别是在 Android 4.4(API 级别 19)之前,系统仅支持大小相同的位图。在 Android 4.4 之后的版本,只要内存大小不小于需求的 Bitmap 都可以复用。即 BitmapFactory.Options.inSampleSize 可以大于1
下面是google关于inBitmap的介绍视频
https://www.youtube.com/watch?v=_ioFW3cyRV0&index=17&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE
需要注意的是inBitmap只能在3.0以后使用。2.3上,bitmap的数据是存储在native的内存区域,并不是在Dalvik的内存堆上。
使用inBitmap,在4.4之前,只能重用相同大小的bitmap的内存区域,而4.4之后你可以重用任何bitmap的内存区域,只要这块内存比将要分配内存的bitmap大就可以。这里最好的方法就是使用LRUCache来缓存bitmap,后面来了新的bitmap,可以从cache中按照api版本找到最适合重用的bitmap,来重用它的内存区域。
BitmapFactory.Options options = new BitmapFactory.Options();
// 只解码出 outxxx参数 比如 宽、高
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources,id,options);
//根据宽、高进行缩放
int w = options.outWidth;
int h = options.outHeight;
//设置缩放系数
options.inSampleSize = calcuteInSampleSize(w,h,maxW,maxH);
if (!hasAlpha)
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
//设置成能复用
options.inMutable=true;
options.inBitmap=reusable;
使用图片缓存
android中有一个LruCache是基于最记最少使用算法实现的一个线程安全的数据缓存类,当超出设定的缓存容量时,优先淘汰最近最少使用的数据LruCache的LRU缓存策略是利用LinkedHashMap来实现的,并通过封装get/put等相关方法来实现控制缓存大小以及淘汰元素,但不支持为null的key和value。 我们可以使用JakeWharton提供的一个开源库github.com/JakeWharton… 来实现我们图片缓存的逻辑
以上是关于内存优化二Bitmap优化的主要内容,如果未能解决你的问题,请参考以下文章
Android 进阶——性能优化之Bitmap位图内存管理及优化概述
Android 进阶——性能优化之Bitmap位图内存管理及优化概述