探索App性能优化之Bitmap优化

2,392 阅读8分钟

一、Bitmap优化的原因

 Android程序要用到许多图片,不同图片总是会有不同的形状、不同的大小,但在大多数情况下,这些图片都会大于程序所需要的大小。比如说系统图片库里展示的图片大都是用手机摄像头拍出来的,这些图片的分辨率会比手机屏幕的分辨率高得多。

 应用程序都是有一定内存限制的,程序占用了过高的内存就容易出现OOM(OutOfMemory)异常。可以通过下面的代码看出每个应用程序最高可用内存是多少。

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");

概括有两点原因:

1、Android系统分配给每个应用程序的内存有限,而图片资源非常消耗内存,占用的内存占App内存的大部分。

2、对Bitmap的使用&内存管理不当,则可能造成内存溢出out of memory,从而造成应用崩溃Crash。

二、优化思路和方向

1、根据分辨率适配 & 压缩图片

2、使用合适的解码方式

3、图片缓存

4、使用完释放图片资源

三、具体方案

(一)根据分辨率适配 & 压缩图片

 现在的手机相机3000W+像素,差一点的手机拍出来的照片3M多,好点的手机拍出来接近8M,如果直接载入图片,那非常容易造成OOM,尤其是在差的手机上面。因此就需要对图像进行压缩处理。

 在展示高分辨率图片的时候,最好先将图片进行压缩。压缩后的图片大小应该和用来展示它的控件大小相近,在一个很小的ImageView上显示一张超大的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存,而且在性能上还可能会带来负面影响。因此对一张大图片进行适当的压缩,让它能够以最佳大小显示的同时,还能防止OOM的出现。

1、尺寸压缩

(1)采样率--inSimpleSize

 根据View的大小利用BitmapFactory.Options计算合适的inSimpleSize(采样率)来对Bitmap进行相对应的裁剪,以减少Bitmap对内存的使用。压缩图片的像素,一张图片所占内存的大小=图片类型*宽*高,通过改变三个值减小图片所占的内存,防止OOM,当然这种方式可能会使图片失真。

(2)预压缩处理--inJustDecodeBounds

 一般都会使用尺寸压缩,通过BitmapFactory.Options来压缩图片,主要用到它的inSampleSize参数(采样率)。利用inSampleSize(采样率)来压缩图片时,先设置BitmapFactory.Options的inJustDecodeBounds参数为true。就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。

将任意一张图片压缩成100*100的缩略图,并在ImageView上展示
ImageView imageView = (ImageView)findViewById(R.id.imageview);
imageView.setImageBitmap(scalePic(R.id.launcher, 100, 100));

public Bitmap scalePic(int reqWidth,int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        
    //  首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。
      
        然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,
        就可以得到合适的inSampleSize值了。
        
        最后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,
        就可以得到压缩后的图片了。
        
        options.inJustDecodeBounds = true;//只读取图片,不加载到内存中
        BitmapFactory.decodeResource(getResources(), R.mipmap.demo, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth,reqHeight);
        options.inJustDecodeBounds = false;
        bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.demo, options);
        return bitmap;
    }

public int calculateInSampleSize(BitmapFactory.Options options,
                                        int reqWidth, int reqHeight) {
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }
        return inSampleSize;
    }

(3)总结

 BitmapFactory这个类提供了多个解析方法(decodeByteArray,decodeFile,decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。

2、质量压缩

(1)品质

 在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,经过它压缩的图片文件大小会有改变,但是导入成bitmap后占得内存是不变的。因为要保持像素不变,所以它就无法无限压缩,到达一个值之后就不会继续变小了。显然这个方法并不适用于想通过压缩图片减少内存的适用,仅适用于想在保证图片质量的同时减少文件大小从而进行网络传输的场景。

 品质options = 100表示质量压缩方法,这里100表示不压缩;options = 90表示保留原图像80%的品质,压缩20%。

public Bitmap compressImage(Bitmap image,int imageSize) {  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos); 
        int options = 100; //质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中 
        
        //循环判断压缩后图片是否大于100kb,大于继续压缩 
        while ( baos.toByteArray().length / 1024>imageSize) {  
        
            baos.reset();//重置baos即清空baos  
            
            //这里压缩options%,把压缩后的数据存放到baos中 
            image.compress(Bitmap.CompressFormat.JPEG, options,baos); 
            
            options -= 10;//每次都减少10,90的品质,就保留原图像90%的品质,压缩10%
            
            //baos.toByteArray()所返回的byte[]数组大小确实变小了
            int compressedLen = baos.toByteArray().length;
        }  
    
        //把压缩后的数据baos存放到ByteArrayInputStream中
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());  
        //把ByteArrayInputStream数据生成图片 
        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null); 
        
        or
        
        Bitmap bitmap = BitmapFactory.decodeByteArray(baos.toByteArray(), 0,compressedLen);
        
        // 这里decode出来的图片byte[]数组大小和原始图片是一样的,并没有变小
        int newLen = compressedBm.getByteCount(); 
    
        return bitmap;  
    } 

(2)总结

  Bitmap.compress()确实可以压缩图片,但压缩的是存储大小,即放到disk上的大小。尝试过把品质设置为不同值,decode出来的Bitmap大小没变,但显示照片的质量非常差。 BitmapFactory.decodeByteArray方法对压缩后的Bitmap大小依然和未压缩过一样,如果你想要显示的Bitmap占用的内存少一点,还是需要去设置加载的像素长度和宽度(变成缩略图),即尺寸压缩。

(二)使用合适的解码方式

 一张图片到底占用多少内存,我们先假设我们有一张图片时 600 * 800 的,图片占用空间大小假设是 100KB。那么图片占用内存大小是多少呢?

1、图片内存大小和占用空间大小有什么关系?

 图片占用空间的大小不是图片占用内存的大小。占用空间是在磁盘上占用的空间,内存大小是加载到内存中占用的内存大小。两个只是单位是一样的,本质不是一个概念。

2、一张图片到底占用空间大小

 Bitmap所占用的内存空间数=Bitmap的每一行所占用的空间数 * Bitmap的行数,即

bitmap.getRowBytes()*bitmap.getHeight();

3、一张图片到底占用多少内存呢?

 图片占用内存大小的计算公式 = 图片高度 * 图片宽度 * 一个像素占用的内存大小 = 800 * 600 * 4byte = 1875KB = 1.83M (4byte是一种解码方式ARGB_8888)。其中A=Alpha=透明度、R=red=红色、G=green=绿色、B=blue=蓝色,用于描述&存储色彩颜色的信息。

属性 Bitmap.Config 含义 色彩组成 内存组成 1个像素点占用内存
ARGB_4444 16位ARGB位图 颜色+透明度 由4个4位组成(即16位)每个像素占4位,即A=4、R=4、G=4、B=4 16位=2字节
ARGB_8888 32位ARGB位图 颜色+透明度 由4个8位组成(即32位)每个像素占4位,即A=8、R=8、G=8、B=84 32位=4字节
RGB_565 16位RGB位图 颜色,无明度 每个像素分别占4:R=54、G=6、B=5 16位=2字节
ALPHA_8 8位aLPHA位图 透明度,无颜色 仅由8位的透明度组成,每个像素:A=8位 8位=1字节

4、特别注意

  • 位图位数越高,代表其可存储的颜色信息越多,图像越逼真
  • 一般情况下,默认使用ARGB_8888解码方式,其最占内存 = 1个像素占4字节
  • 使用参数:BitmapFactory.inPreferredConfig 设置合适的解码方式

(三)图片缓存

三级缓存:内存缓存-->本地缓存(硬盘、文件、数据库)-->网络缓存

参考文章:blog.csdn.net/Army_Jun/ar…

四、结束

掌握了以上三种方法,可以有效地避心OOM的问题。