在Android开发过程中,Bitmap往往会给开发者带来一些困扰,因为对Bitmap操作不慎,就容易造成OOM(Java.lang.OutofMemoryError - 内存溢出)
Bitmap常用方法
Bitmap是Android系统中的图像处理的最重要类之一。用它可以获取图像文件信息,进行图像剪切、旋转、缩放等操作,并可以指定格式保存图像文件。
- public void recycle() // 回收位图占用的内存空间,把位图标记为Dead
- public final boolean isRecycled() //判断位图内存是否已释放
- public final int getWidth() //获取位图的宽度
- public final int getHeight() //获取位图的高度
- public final boolean isMutable() //图片是否可修改
- public int getScaledWidth(Canvas canvas) //获取指定密度转换后的图像的宽度
- public int getScaledHeight(Canvas canvas) //获取指定密度转换后的图像的高度
- public boolean compress(CompressFormat format, int quality, OutputStream stream) //按指定的图片格式以及画质,将图片转换为输出流。
format:压缩图像的格式,如Bitmap.CompressFormat.PNG或 Bitmap.CompressFormat.JPEG
quality:画质,0-100.0表示最低画质压缩,100以最高画质压缩。对于PNG等无损格式的图片,会忽略此项设置。
stream: OutputStream中写入压缩数据。
return: 是否成功压缩到指定的流。 - public static Bitmap createBitmap(Bitmap src) //以src为原图生成不可变得新图像
- public static Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter) //以src为原图,创建新的图像,指定新图像的高宽以及是否可变。
- public static Bitmap createBitmap(int width, int height, Config config) //创建指定格式、大小的位图
- public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height) //以source为原图,创建新的图片,指定起始坐标以及新图像的高宽。
BitmapFactory工厂类:
Option 参数类:
- public boolean inJustDecodeBounds //如果设置为true,不获取图片,不分配内存,但会返回图片的高度宽度信息。如果将这个值置为true,那么在解码的时候将不会返回bitmap,只会返回这个bitmap的尺寸。这个属性的目的是,如果你只想知道一个bitmap的尺寸,但又不想将其加载到内存时。这是一个非常有用的属性。
- public int inSampleSize //图片缩放的倍数, 这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4。
- public int outWidth //获取图片的宽度值
- public int outHeight //获取图片的高度值 ,表示这个Bitmap的宽和高,一般和inJustDecodeBounds一起使用来获得Bitmap的宽高,但是不加载到内存。
- public int inDensity //用于位图的像素压缩比
- public int inTargetDensity //用于目标位图的像素压缩比(要生成的位图)
- public byte[] inTempStorage //创建临时文件,将图片存储
- public boolean inScaled //设置为true时进行图片压缩,从inDensity到inTargetDensity
- public boolean inDither //如果为true,解码器尝试抖动解码
- public Bitmap.Config inPreferredConfig //设置解码器,这个值是设置色彩模式,默认值是ARGB_8888,在这个模式下,一个像素点占用4bytes空间,一般对透明度不做要求的话,一般采用RGB_565模式,这个模式下一个像素点占用2bytes。
- public String outMimeType //设置解码图像
- public boolean inPurgeable //当存储Pixel的内存空间在系统内存不足时是否可以被回收
- public boolean inInputShareable //inPurgeable为true情况下才生效,是否可以共享一个InputStream
- public boolean inPreferQualityOverSpeed //为true则优先保证Bitmap质量其次是解码速度
- public boolean inMutable //配置Bitmap是否可以更改,比如:在Bitmap上隔几个像素加一条线段
- public int inScreenDensity //当前屏幕的像素密度
Bitmap加载方式
Bitmap的加载方式有Resource资源加载、本地(SDcard)加载、网络加载等加载方式。
为什么Bitmap会导致OOM?
每个机型在编译ROM时都设置了一个应用堆内存VM值上限dalvik.vm.heapgrowthlimit,用来限定每个应用可用的最大内存,超出这个最大值将会报OOM。这个阀值,一般根据手机屏幕dpi大小递增,dpi越小的手机,每个应用可用最大内存就越低。所以当加载图片的数量很多时,就很容易超过这个阀值,造成OOM。
图片分辨率越高,消耗的内存越大,当加载高分辨率图片的时候,将会非常占用内存,一旦处理不当就会OOM。例如,一张分辨率为:1920x1080的图片。如果Bitmap使用 ARGB_8888 32位来平铺显示的话,占用的内存是1920x1080x4个字节,占用将近8M内存,可想而知,如果不对图片进行处理的话,就会OOM。
在使用ListView, GridView,RecyclerView等这些大量加载view的组件时,如果没有合理的处理缓存,大量加载Bitmap的时候,也将容易引发OOM
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 一个像素点占用的字节数
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
}
}
}
在不配置Options的情况下:
1.decodeFile、decodeStream在解析时不会对Bitmap进行一系列的屏幕适配,解析出来的将是原始大小的图
2.decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,并且Bitmap的大小将比原始的大
在不配置Options的情况下:
- decodeFile、decodeStream在解析时不会对Bitmap进行一系列的屏幕适配,解析出来的将是原始大小的图
- decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,并且Bitmap的大小将比原始的大
Bitmap的优化策略:如何对Bitmap进行优化加载呢
经过上面的分析,我们可以得出Bitmap优化的思路:
我们将Bitmap优化的策略总结为以下3种:
1.对图片质量进行压缩
2.对图片尺寸进行压缩
3.使用libjpeg.so库进行压缩所以我们根据以上的思路,我们将Bitmap优化的策略
对图片质量进行压缩
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;
}
对图片尺寸进行压缩
Bitmap优化加载的核心思想就是采用BitmapFactory.Options来加载所需尺寸的图片。
比如通过ImageView来显示图片,很多时候ImageView并没有图片的原始尺寸那么大,如果把整个图片加载进来,再设置给ImageView,ImageView是无法显示原始的图片。通过BitmapFactory.Options就可以按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高了Bitmap加载时的性能。BitmapFactory提供的加载图片的四个类方法都支持BitmapFactory.Options参数,通过它就可以很方便对一个图片进行采样缩放。
为了避免OOM异常,最好在解析每张图片的时候,先检查一下图片的大小,然后可以决定是把整张图片加载到内存还是把图片压缩后加载到内存。需要考虑以下几个因素:
- 预估一下加载整张图片所需占用的内存
- 为了加载一张图片你所愿意提供多少内存
- 用于展示这张图片的控件的实际的大小
- 当前设备的屏幕尺寸和分辨率
通过BitmapFactory.Options来缩放图片,主要用到了它的inSampleSize参数,即采样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小;当inSampleSize大于1时,比如2,那么采样后的图片宽高均为原图大小的1/2,像素数为原图的1/4,其占有的内存大小也为原图的1/4。
采样率必须是大于1的整数,图片才会有缩小的效果,并且采样率同时作用于宽和高,缩放比例为1/(inSampleSize的2次方),比如inSampleSize为4,那么缩放比例就是1/16。官方文档指出,inSampleSize的取值为2的指数:1、2、4、8、16等等。
通过采样率可以优化加载图片,那么如何获采样率呢?
通过以下4个步骤:
将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片;
从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数;
根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize;
将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
/**
* 按图片尺寸压缩 参数是bitmap
* @param bitmap
* @param pixelW
* @param pixelH
* @return
*/
public static Bitmap compressImageFromBitmap(Bitmap bitmap, int pixelW, int pixelH) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
if( os.toByteArray().length / 1024>512) {//判断如果图片大于0.5M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出
os.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);//这里压缩50%,把压缩后的数据存放到baos中
}
ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
BitmapFactory.Options options = new BitmapFactory.Options();
//第一次采样
options.inJustDecodeBounds = true;//只加载bitmap边界,占用部分内存
options.inPreferredConfig = Bitmap.Config.RGB_565;//设置色彩模式
BitmapFactory.decodeStream(is, null, options);//配置首选项
//第二次采样
options.inJustDecodeBounds = false;
options.inSampleSize = computeSampleSize(options , pixelH > pixelW ? pixelW : pixelH ,pixelW * pixelH );
is = new ByteArrayInputStream(os.toByteArray());
//把最终的首选项配置给新的bitmap对象
Bitmap newBitmap = BitmapFactory.decodeStream(is, null, options);
return newBitmap;
}
/**
* 动态计算出图片的inSampleSize
* @param options
* @param minSideLength
* @param maxNumOfPixels
* @return
*/
public static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels);
int roundedSize;
if (initialSize <= 8) {
roundedSize = 1;
while (roundedSize < initialSize) {
roundedSize <<= 1;
}
} else {
roundedSize = (initialSize + 7) / 8 * 8;
}
return roundedSize;
}
private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
double w = options.outWidth;
double h = options.outHeight;
int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
int upperBound = (minSideLength == -1) ? 128 :(int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength));
if (upperBound < lowerBound) {
return lowerBound;
}
if ((maxNumOfPixels == -1) && (minSideLength == -1)) {
return 1;
} else if (minSideLength == -1) {
return lowerBound;
} else {
return upperBound;
}
}
}
使用libjpeg.so库进行压缩(进阶,亮点,掌握最好)
除了通过设置simpleSize根据图片尺寸压缩图片和通过Bitmap.compress方法通过压缩图片质量两种方法外,我们还可以使用libjpeg.so这个库来进行压缩。
Bitmap的内存优化详解
首先Bitmap在Android虚拟机中的内存分配,在Google的网站上给出了下面的一段话
大致的意思也就是说,在Android3.0之前,Bitmap的内存分配分为两部分,一部分是分配在Dalvik的VM堆中,而像素数据的内存是分配在Native堆中,而到了Android3.0之后,Bitmap的内存则已经全部分配在VM堆上,这两种分配方式的区别在于,Native堆的内存不受Dalvik虚拟机的管理,我们想要释放Bitmap的内存,必须手动调用Recycle方法,而到了Android 3.0之后的平台,我们就可以将Bitmap的内存完全放心的交给虚拟机管理了,我们只需要保证Bitmap对象遵守虚拟机的GC Root Tracing的回收规则即可。OK,基础知识科普到此。接下来分几个要点来谈谈如何优化Bitmap内存问题。
结论
- 同一张图片,放在不同资源目录下,其分辨率会有变化,
- bitmap分辨率越高,其解析后的宽高越小,甚至会小于图片原有的尺寸(即缩放),从而内存占用也相应减少
- 图片不特别放置任何资源目录时,其默认使用mdpi分辨率:160
- 资源目录分辨率和设备分辨率一致时,图片尺寸不会缩放
解决方案
- 使用低色彩的解析模式,如RGB565,减少单个像素的字节大小
- 资源文件合理放置,高分辨率图片可以放到高分辨率目录下
- 图片缩小,减少尺寸
第一种方式,大约能减少一半的内存开销。Android默认是使用ARGB8888配置来处理色彩,占用4字节,改用RGB565,将只占用2字节,代价是显示的色彩将相对少,适用于对色彩丰富程度要求不高的场景。
第二种方式:(图片适配)和图片的具体分辨率有关,建议开发中,高分辨率的图像应该放置到合理的资源目录下,注意到Android默认放置的资源目录是对应于160dpi,目前手机屏幕分辨率越来越高,此处能节省下来的开销也是很可观的。理论上,图片放置的资源目录分辨率越高,其占用内存会越小,但是低分辨率图片会因此被拉伸,显示上出现失真。另一方面,高分辨率图片也意味着其占用的本地储存也变大。
第三种方式:(参考尺寸和质量压缩)理论上根据适用的环境,是可以减少十几倍的内存使用的,它基于这样一个事实:源图片尺寸一般都大于目标需要显示的尺寸,因此可以通过缩放的方式,来减少显示时的图片宽高,从而大大减少占用的内存。
不同Android版本时的Bitmap内存模型
我们知道Android系统中,一个进程的内存可以简单分为Java内存和native内存两部分,而Bitmap对象占用的内存,有Bitmap对象内存和像素数据内存两部分,在不同的Android系统版本中,其所存放的位置也有变化。Android Developers上列举了从API 8 到API 26之间的分配方式:
总结:
可以看到,最新的Android O之后,谷歌又把像素存放的位置,从java 堆改回到了 native堆。API 11的那次改动,是源于native的内存释放不及时,会导致OOM,因此才将像素数据保存到Java堆,从而保证Bitmap对象释放时,能够同时把像素数据内存也释放掉。
在Android 8.0中,关于Bitmap的改动有两方面还需深入探究的:1、Config配置为Hardware时的优劣。Hardware配置实际上没有改变像素的位储存大小(还是默认的ARGB8888),但是改变了bitmap像素的存储位置(存放到GPU内存中),对实际应用的影响会如何?;2、Bitmap在8.0后又回归到native存放bitmap像素数据,而这部分数据的回收时机和触发方式又是如何?一般测试下,可以通过native分配Bitmap超过1G的内存数据而不发生崩溃。
总结和问题
优化原因
使用完毕后 释放图片资源
优化原因
使用完毕后若不释放图片资源,容易造成内存泄露,从而导致内存溢出
优化方案
a. 在 Android2.3.3(API 10)前,调用 Bitmap.recycle()方法
b. 在 Android2.3.3(API 10)后,采用软引用(SoftReference)
c.在android o以上
具体描述
在 Android2.3.3(API 10)前、后,Bitmap对象 & 其像素数据 的存储位置不同,从而导致释放图片资源的方式不同,具体如下图
注:若调用了Bitmap.recycle()后,再绘制Bitmap,则会出现Canvas: trying to use a recycled bitmap错误
根据分辨率适配 & 缩放图片
优化原因
若 Bitmap 与 当前设备的分辨率不匹配,则会拉伸Bitmap,而Bitmap分辨率增加后,所占用的内存也会相应增加
因为Bitmap 的内存占用 根据 x、y的大小来增加的
优化方案
按需 选择合适的解码方式
优化原因
不同的图片解码方式 对应的 内存占用大小 相差很大,具体如下
优化方案
根据需求 选择合适的解码方式
使用参数:BitmapFactory.inPreferredConfig 设置
默认使用解码方式:ARGB_8888
设置 图片缓存
优化原因
重复加载图片资源耗费太多资源(CPU、内存 & 流量)