Android 进阶——性能优化之Bitmap位图内存管理及优化

Posted CrazyMo_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 进阶——性能优化之Bitmap位图内存管理及优化相关的知识,希望对你有一定的参考价值。

引言

位图

一、Bitmap概述

Bitmap 直接继承Object并实现了Parcelable接口,是用于描述图片内部像素、像素类型、像素内部存储的编码格式、长、宽、颜色等一系列描述信息的对象,是android 中一切图形图像与硬件关联的重要对象,也是底层决定出一切UI、图像的显示效果的关键对象(要通过OpenGL 绘制图形图像也是需要通过Bitmap来实现的)。

二、Bitmap 家族的重要成员对象

Bitmap 家族的重要成员对象有:BitmapBitmap.ConfigBitmap.CompressFormatBitmapFactoryBitmapFactory.OptionsBitmapRegionDecoderBitmapShader等,接下来一一介绍基本使用方法。

1、Bitmap

方法名说明
boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream)压缩位图,通过位图压缩并写到对应的输出流,其中quality取值为0-100。,0表示压缩小尺寸而100表示压缩以获得最高质量。但某些格式如无损的PNG,将忽略质量设置,压缩成功返回true
Bitmap copy (Bitmap.Config config, boolean isMutable)拷贝一个Bitmap的像素到一个新的指定信息配置的Bitmap,其中isMutable指示生成的新位图像素是否可变
static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height)通过给定的资源创建一个不可变的新位图,其中这个createBitmap方法有九个重载形式,最终都是执行了返回可变的私有方法
void copyPixelsFromBuffer(Buffer src)将处理完的像素数据赋给Bitmap对象
void copyPixelsToBuffer (Buffer dst)将像素数据复制到一块dst 缓存中
int getByteCount()返回可用于存储此位图像素的最小字节数返即回Bitmap 真实占用的内存大小
int getAllocationByteCount()返回Bitmap 真实占用的内存大小(API18以上),假如B 要复用 A时,A 本身的内存空间比B 实际需要的要大得多,如果此时调用getByteCount(),则只得到的是B 自己的大小而实际上B 占用的是整个A 的内存大小,调用getAllocationByteCount才是获得到真正占用的内存大小
boolean recycle()释放bitmap所占的内存
boolean isRecycled()判断是否回收内存
int getWidth()得到宽
int getHeight()得到高
boolean isMutable()是否可以改变
boolean sameAs(Bitmap other)判断两个bitmap大小,格式,像素信息是否相同

2、Bitmap.Config

Bitmap.Config是Bitmap的内部类,封装着描述像素色彩模式主要包括:ALPHA_8RGB_565ARGB_4444ARGB_8888 其中A代表透明度,而RGB分别是红、绿、蓝三种原色。

  • ARGB_8888————四个通道都是8位,每个像素占用4个字节,图片质量是最高的,但是占用的内存也是最大的(默认模式,适用于设置透明度且对图片质量要求又高),其中ARGB各占8位
  • RGB_565————没有A通道,每个像素占用2个字节,图片失真小,但是没有透明度,其中R5位、G6位、B5位即16位
  • ALPHA_8————只有A通道,每个像素占用1个字节大大小,只有透明度,没有颜色值(只在特殊场景使用比如设置遮盖效果等))
  • ARGB_4444————四个通道都是4位,每个像素占用2个字节,图片的失真比较严重(基本不用)

3、Bitmap.CompressFormat

Bitmap.CompressFormat是Bitmap的内部枚举类,封装着描述位图可压缩的格式主要有三:
JPEG、PNG、WEBP

  public enum CompressFormat 
        JPEG    (0),
        PNG     (1),
        WEBP    (2);

        CompressFormat(int nativeInt) 
            this.nativeInt = nativeInt;
        
        final int nativeInt;
    

4、BitmapFactory

BitmapFactory是用于从不同来源(包括文件、流、字节数组等)创建对应的位图

方法名说明
static Bitmap decodeFile(String pathName, Options opts)将文件路径解码为位图。如果指定的文件名为null或者无法解码为位图,则返回null
static Bitmap decodeResource(Resources res, int id, Options opts)将资源解码为位图,如返回null则代表解码失败,可能的原因有图像数据不能解码或者opts不为空或者opts仅请求返回大小(在opts.outWidth和opts.outHeight中)均会引发“Problem decoding into existing bitmap”异常,最终都会调用decodeStream方法
static Bitmap decodeResourceStream(Resources res, TypedValue value,InputStream is, Rect pad, Options opts)从InputStream解码新的Bitmap。 此InputStream是从我们传递的资源,以便能够相应地缩放位图,最终都会调用decodeStream方法
static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)将输入流解码为位图。 如果输入流为null或不能用于解码位图,该函数返回null,流的位置将是读取编码数据后的位置。比如说BitmapFactory的decodeXX函数只会读取数据,将读取的宽、高等属性设置进入Options。那么这里会读取掉InputStream中一部分内容。那么当我们真正需要解析图片获得Bitmap的时候InputStream中的数据肯定就不是完整的。这样你将得到一个" Image Format Not Support “,所以我们需要确保进行decode的数据是完整的
static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)从指定的字节数组解码不可变的位图
static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)从文件描述符解码位图。如果位图无法解码则返回null。返回时,描述符中的位置不会更改,因此可以按原样再次使用描述符。

5、BitmapFactory.Options

控制解码图片参数

属性名说明
Bitmap inBitmap设置复用的Bitmap
boolean inJustDecodeBounds是否只扫描轮廓,true则只会解析图片的原始信息,并不会真正的加载图片到内存中,读取图片outXxx系列参数,如outWidth与outHeight
int inSample设置图片解码缩放比,inSampleSize的值必须大于1时才会有效果,且采样率同时作用于宽和高,当inSampleSize=1时,采样后的图片为图片的原始大小;当inSampleSize=4时,加载图片宽高是原图的1/4,内存大小则是1/16(inSampleSize的取值应该总为2的整数倍,否则会向下取整,取一个最接近2的整数倍比如inSampleSize=5时系统会取inSampleSize=4)
Bitmap.Config inPreferredConfig设置图片解码后的像素格式,如ARGB_8888/RGB_565等色彩模式
int outWidthbitmap的宽
int outHeightbitmap的高
boolean inDither防抖动,默认false
int inDensity像素密度,表示这个bitmap的像素密度,根据drawable目录,其中drawable-ldpi为120、drawable-mdpi为160、drawable-hdpi为240、drawable-xhdpi为320、drawable-xxhdpi为480
int inTargetDensity表示要被画出来时的目标(屏幕)像素密度 , getResources().getDisplayMetrics().densityDpi
boolean inScaled是否可以缩放,默认true
boolean inMutable是否可变,设为ture,decode转换方法返回的结果全部可改变

6、BitmapRegionDecoder

BitmapRegionDecoder可用于解码图像中的矩形区域,当原始图像很大时,可以通过BitmapRegionDecoder指定腻所需要显示图像的某一部分,其实BitmapRegionDecoder是一个JNI 本地接口类,核心操作都是在本地方法实现的,甚至包括创建BitmapRegionDecoder实例。

方法名说明
Bitmap decodeRegion(Rect rect, BitmapFactory.Options options)解码由rect指定的图像中的矩形区域
int getWidth()得到原始图片的宽
int getHeight()得到原始图片的高
void recycle()释放与此区域解码器相关的内存

7、BitmapShader

BitmapShader可作为纹理绘制到图片上, 位图可以重复或通过设置平铺模式进行镜像,可以实现简单的滤镜效果。

三、Bitmap的内存

每个机型在编译ROM时都会设置了一个应用堆内存VM值上限dalvik.vm.heapgrowthlimit,Android 会对每个应用进行内存限制(通过ActivityManager实例的getMemoryClass()查看,也可以查看/system/build.prop中的对应字段来查看App的最大允许申请内存) 。这个阀值一般根据手机屏幕dpi大小递增,dpi越小的手机,每个应用可用最大内存就越低,所以当加载图片的所需要的内存越来越大的时候就可能造成OOM。

  • dalvik.vm.heapstartsize—— 堆分配的初始大小
  • dalvik.vm.heapgrowthlimit —— 正常情况下dvm heap的大小是不会超过dalvik.vm.heapgrowthlimit的值
  • dalvik.vm.heapsize ——manifest中指定android:largeHeap为true的极限堆大小,这个就是堆的默认最大值

1、图片的像素与内存

图片是由一个个像素构成的,所以位图所占用的内存大小就是像素数组所占用内存的大小,具体计算公式为:width * height * getPixelsCout(bitmap.getConfig())

	int byteCout = width * height * getPixelsCout(bitmap.getConfig());//计算内存
    
    private int getPixelsCout(Bitmap.Config config)
        if (config == Bitmap.Config.ARGB_8888)
            return 4;
         else if (config == Bitmap.Config.RGB_565) 
            return 2;
         else if (config == Bitmap.Config.ARGB_4444) 
            return 2;
         else if (config == Bitmap.Config.ALPHA_8) 
            return 1;
        
        return 1;
    

对于一张240X320的图的像素就是长乘以宽的积(7680),不会因为其色彩空间而变化。当像素格式为Config.ARGB_8888,表示的是ARGB每个通道都使用2^8=256个级别(00~ff)来表示,其位深就是8(位)*4(通道) = 32,代表的是这个像素点占用内存为32 位 = 4 字节。因为Java 中int 型长度范围为2^32即一个int型占用内存为4字节,正好足够用来描述ARGB4个通道值,比如int 黑色 = 0xff000000 = 11111111 00000000 00000000 00000000,所以1024 * 1024像素的位图,保存时设置位深32位,无压缩且图片不添加额外描述信息时,在硬盘上一定是1024 * 1024 * 32位/8 = 4M(另外位图在硬盘上所显示的文件大小并不代表实际内存的空间);若位深24则为3M。而Config.ARGB_4444 = 4*4 = 16位深。Config.RGB_565 = 5+6+5 = 16位深。其中R通道和B通道颜色级别划分为2^5 个颜色,G 为2^ 6个颜色。总之,同样宽高不同格式不同大小(图片本身大小)的图片放在同个文件夹解码出来占用内存大小是一样的。所以图片的内存大小只取决于两种元素:像素的编码格式图片的大小尺寸(在Android中所放置图片的资源文件夹也直接影响到图片的大小)

四、位图的底层存储位置

Android 3.0以下时 Bitmap 像素数据是存储在 native 区域的,需要主动去释放;而到了Android 3.0 及以上时又改到了在Java 区域存储,可以不主动去释放;现在到了Android 8.0 的时候可能是为了减少Java堆内存的占用又存储到了 native 区域,如果不去主动释放的话所占内存可能无法被释放,可以通过弱引用队列管理释放。

五、Bitmap的压缩

通常Bitmap 是Android App运行时的内存消耗大户,很多内存溢出问题绝大部分都是因为位图,如果追求性能优化的话,在真正使用前,进行相应的压缩操作可以节省内存和流量,自然可以实现一定的性能优化。而在Android中使用文件压缩的方式有:质量压缩尺寸压缩选择合适的图片格式使用最优哈夫曼编码等。

1、质量压缩

质量压缩即对清晰度的压缩,执行压缩时牺牲掉了一些画面细节,这些丢失的细节或许可以被肉眼观察到,所以这种压缩也叫有损压缩。

2、尺寸压缩

尺寸压缩就是图片宽高的减小。图片宽高减小了,图片文件大小也就减小了。

3、选择合适的图片格式

JPEG比PNG压缩率高,WEBP压缩率一般比JPEG高,在Android4.0之后尽量使用WEBP,油管使用的就是WEBP。

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.cmo);

        /**
         * 质量压缩
         */
        compress(bitmap, Bitmap.CompressFormat.JPEG,50,Environment.getExternalStorageDirectory()+"/test_q.jpeg");

        /**
         * 尺寸压缩
         */
        //filter 图片滤波处理 色彩更丰富
        Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 300, 300, true);
        compress(scaledBitmap, Bitmap.CompressFormat.JPEG,100,Environment.getExternalStorageDirectory()+"/test_scaled.jpeg");

        //png格式
        compress(bitmap, Bitmap.CompressFormat.PNG,100,Environment.getExternalStorageDirectory()+"/test.png");
        //webp格式
        compress(bitmap, Bitmap.CompressFormat.WEBP,100,Environment.getExternalStorageDirectory()+"/test.webp");

        nativeCompress(bitmap,50,Environment.getExternalStorageDirectory()+"/test_native.jpeg");
    

    /**
     * 压缩图片到制定文件
     * @param bitmap 待压缩图片
     * @param format 压缩的格式
     * @param q      质量
     * @param path  文件地址
     */
    private void compress(Bitmap bitmap, Bitmap.CompressFormat format, int q, String path) 
        FileOutputStream fos = null;
        try 
             fos = new FileOutputStream(path);
             bitmap.compress(format,q,fos);
         catch (FileNotFoundException e) 
            e.printStackTrace();
         finally 
            if (null != fos)
                try 
                    fos.close();
                 catch (IOException e) 
                    e.printStackTrace();
                
            
        
    

位图的压缩底层原理是基于阉割版的Skia引擎( Google 研发、开源的C++二维图形库 )的调用,对JPEG的处理基于libjpeg,而对PNG则是基于libpng。不过早期由于cpu吃紧,将libjpeg中的最优哈夫曼编码关闭了,直到7.0才打开(即在SKImageDecoder_libjpeg.cpp中把cinfo.optimize_coding=TRUE)。哈弗曼编码是一种字符编码方式,常用于数据文件的压缩,其主要思想就是采取可变长编码方式,对文件中出现次数多的字符采取比较短的编码,对于出现次数少的字符采取比较长的编码,可以有效地减小总的编码长度,举个例子一张图片中有红、黄、蓝、绿、白五种颜色如下:

如果采用定长编码的话至少需要三个字节来表示每种颜色,而五种的话至少需要15个字节,而采用变长哈夫曼编码的话只需要12个字节,把占比最多的红色采用最短的编码,而占比少的则采用长编码(当然这仅仅是举个简单的例子,与深入了解请参阅其他资料)

其实对于图片内存的降低,无论是选择JPEG还是PNG抑或是WEBP,这些都是毫无意义的。而且JPEG是属于有损压缩,虽然我们看见的JPEG比PNG文件小,那是因为压缩率高,但这都是属于文件存储范畴。而对于内存来说,我们加载一张不带alpha通道使用RGB_565格式的PNG与一张JPEG占用的内存大小都是一样的,因此对于位图内存的压缩我们能做的就是缩小图片尺寸与改变像素格式

4、在Android 7.0以下通过JNI使用最优哈夫曼编码

从上得知开启最优哈夫曼编码可以减小些许内存空间,所以开启最优哈夫曼也可以算是压缩的一种方式,而且几乎无损,但是我们无法直接操作,只能操作Bitmap.java中的api(本质上是通过使用Skia引擎间接使用libjpeg),目前我们要想完全自由地使用libjpeg-turbo库,只能自己去下载 libjpeg源码编译并通过JNI的方式使用,步骤如下:

4.1、下载 libjpeg-turbo源码

4.2、详见下文。

以上是关于Android 进阶——性能优化之Bitmap位图内存管理及优化的主要内容,如果未能解决你的问题,请参考以下文章

Android 进阶——性能优化之Bitmap位图内存管理及优化概述

Android 进阶——性能优化之Bitmap位图内存管理及优化概述

Android 进阶——性能优化之Bitmap位图内存管理及优化

Android 进阶——性能优化之Bitmap位图内存管理及优化

Android 进阶——性能优化之Bitmap位图内存管理及优化

Android 进阶——性能优化之Bitmap位图内存管理及优化