首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Bitmap优化详解

Bitmap优化详解

原创
作者头像
大发明家
发布2021-12-15 15:22:02
发布2021-12-15 15:22:02
2.3K0
举报
文章被收录于专栏:技术博客文章技术博客文章
为什么Bitmap会导致OOM?

1.每个机型在编译ROM时都设置了一个应用堆内存VM值上限dalvik.vm.heapgrowthlimit,用来限定每个应用可用的最大内存,超出这个最大值将会报OOM。这个阀值,一般根据手机屏幕dpi大小递增,dpi越小的手机,每个应用可用最大内存就越低。所以当加载图片的数量很多时,就很容易超过这个阀值,造成OOM。

2.图片分辨率越高,消耗的内存越大,当加载高分辨率图片的时候,将会非常占用内存,一旦处理不当就会OOM。例如,一张分辨率为:1920x1080的图片。如果Bitmap使用 ARGB_8888 32位来平铺显示的话,占用的内存是1920x1080x4个字节,占用将近8M内存,可想而知,如果不对图片进行处理的话,就会OOM。

Bitmap基础知识

一张图片Bitmap所占用的内存 = 图片长度 x 图片宽度 x 一个像素点占用的字节数undefined 而Bitmap.Config,正是指定单位像素占用的字节数的重要参数。

代码语言:txt
复制
其中,A代表透明度;R代表红色;G代表绿色;B代表蓝色。
代码语言:txt
复制
ALPHA_8
代码语言:txt
复制
表示8位Alpha位图,即A=8,一个像素点占用1个字节,它没有颜色,只有透明度
代码语言:txt
复制
ARGB_4444
代码语言:txt
复制
表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位,2个字节
代码语言:txt
复制
ARGB_8888
代码语言:txt
复制
表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位,4个字节
代码语言:txt
复制
RGB_565
代码语言:txt
复制
表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位,2个字节

一张图片Bitmap所占用的内存 = 图片长度 x 图片宽度 x 一个像素点占用的字节数

根据以上的算法,可以计算出图片占用的内存,以100*100像素的图片为例

image.png

下面我们来开始学习Bitmap的优化方案

一、Bitmap质量压缩

通过Bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);方式降低图片质量

代码语言:txt
复制
     public static Bitmap compressImage(Bitmap bitmap){  
代码语言:txt
复制
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  
代码语言:txt
复制
            //质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中  
代码语言:txt
复制
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);  
代码语言:txt
复制
            int options = 100;  
代码语言:txt
复制
            //循环判断如果压缩后图片是否大于50kb,大于继续压缩  
代码语言:txt
复制
            while ( baos.toByteArray().length / 1024>50) {  
代码语言:txt
复制
                //清空baos  
代码语言:txt
复制
                baos.reset();  
代码语言:txt
复制
                bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);  
代码语言:txt
复制
                options -= 10;//每次都减少10  
代码语言:txt
复制
            }  
代码语言:txt
复制
            //把压缩后的数据baos存放到ByteArrayInputStream中  
代码语言:txt
复制
            ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());  
代码语言:txt
复制
            //把ByteArrayInputStream数据生成图片  
代码语言:txt
复制
            Bitmap newBitmap = BitmapFactory.decodeStream(isBm, null, null);  
代码语言:txt
复制
            return newBitmap;  
代码语言:txt
复制
        }
二、缩放法压缩
代码语言:txt
复制
        int ratio = 8;
代码语言:txt
复制
        //根据参数创建新位图
代码语言:txt
复制
        Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Bitmap.Config.ARGB_8888);
代码语言:txt
复制
        Canvas canvas = new Canvas(result);
代码语言:txt
复制
        Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
代码语言:txt
复制
        canvas.drawBitmap(bmp, null, rect, null);
代码语言:txt
复制
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
代码语言:txt
复制
        result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
代码语言:txt
复制
        try {
代码语言:txt
复制
            FileOutputStream fos = new FileOutputStream(file);
代码语言:txt
复制
            fos.write(baos.toByteArray());
代码语言:txt
复制
            fos.flush();
代码语言:txt
复制
            fos.close();
代码语言:txt
复制
        } catch (Exception e) {
代码语言:txt
复制
            e.printStackTrace();
代码语言:txt
复制
        }
三、采样率压缩(大小压缩)

Bitmap优化加载的核心思想就是采用BitmapFactory.Options来加载所需尺寸的图片。

比如通过ImageView来显示图片,很多时候ImageView并没有图片的原始尺寸那么大,如果把整个图片加载进来,再设置给ImageView,ImageView是无法显示原始的图片。通过BitmapFactory.Options就可以按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高了Bitmap加载时的性能。BitmapFactory提供的加载图片的四个类方法都支持BitmapFactory.Options参数,通过它就可以很方便对一个图片进行采样缩放。

为了避免OOM异常,最好在解析每张图片的时候,先检查一下图片的大小,然后可以决定是把整张图片加载到内存还是把图片压缩后加载到内存。需要考虑以下几个因素:

1.预估一下加载整张图片所需占用的内存

2.为了加载一张图片你所愿意提供多少内存

3.用于展示这张图片的控件的实际的大小

4.当前设备的屏幕尺寸和分辨率

通过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等等。

如何获取采样率?

1.将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片;undefined 2.从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数;undefined 3.根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize;undefined 4.将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。

代码语言:txt
复制
 /**
代码语言:txt
复制
     * 按图片尺寸压缩 参数是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;
    }
代码语言:txt
复制
    /**
代码语言:txt
复制
     * 动态计算出图片的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;
    }
代码语言:txt
复制
    private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
代码语言:txt
复制
        double w = options.outWidth;
代码语言:txt
复制
        double h = options.outHeight;
代码语言:txt
复制
        int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
代码语言:txt
复制
        int upperBound = (minSideLength == -1) ? 128 :(int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength));
代码语言:txt
复制
        if (upperBound < lowerBound) {
代码语言:txt
复制
            return lowerBound;
代码语言:txt
复制
        }
代码语言:txt
复制
        if ((maxNumOfPixels == -1) && (minSideLength == -1)) {
代码语言:txt
复制
            return 1;
代码语言:txt
复制
        } else if (minSideLength == -1) {
代码语言:txt
复制
            return lowerBound;
代码语言:txt
复制
        } else {
代码语言:txt
复制
            return upperBound;
代码语言:txt
复制
        }
代码语言:txt
复制
    }
代码语言:txt
复制
}
四、Bitmap色彩模式压缩

Android默认是使用ARGB8888配置来处理色彩,占用4字节,改用RGB565,将只占用2字节,代价是显示的色彩将相对少,适用于对色彩丰富程度要求不高的场景。

代码语言:txt
复制
 BitmapFactory.Options options = new BitmapFactory.Options();
代码语言:txt
复制
 options.inPreferredConfig = Bitmap.Config.RGB_565;//设置色彩模式
五、libjpeg.so库压缩

libjpeg是广泛使用的开源JPEG图像库,安卓也依赖libjpeg来压缩图片。但是安卓并不是直接封装的libjpeg,而是基于了另一个叫Skia的开源项目来作为的图像处理引擎。Skia是谷歌自己维护着的一个大而全的引擎,各种图像处理功能均在其中予以实现,并且广泛的应用于谷歌自己和其它公司的产品中(如:Chrome、Firefox、 Android等)。Skia对libjpeg进行了良好的封装,基于这个引擎可以很方便为操作系统、浏览器等开发图像处理功能。

**Java的本地方法如下:

public static native String compressBitmap(Bitmap bit, int w, int h, int

quality, byte[] fileNameBytes, boolean optimize);**

以下C代码具体步骤如下:

1、将Android的bitmap解码并转换为RGB数据undefined 2、为JPEG对象分配空间并初始化undefined 3、指定压缩数据源undefined 4、获取文件信息undefined 5、为压缩设定参数,包括图像大小,颜色空间undefined 6、开始压缩undefined 7、压缩完毕undefined 8、释放资源

代码语言:txt
复制
#include <string.h>
代码语言:txt
复制
#include <bitmap.h>
代码语言:txt
复制
#include <log.h>
代码语言:txt
复制
#include "jni.h"
代码语言:txt
复制
#include <stdio.h>
代码语言:txt
复制
#include <setjmp.h>
代码语言:txt
复制
#include <math.h>
代码语言:txt
复制
#include <stdint.h>
代码语言:txt
复制
#include <time.h>
代码语言:txt
复制
#include "jpeg/android/config.h"
代码语言:txt
复制
#include "jpeg/jpeglib.h"
代码语言:txt
复制
#include "jpeg/cdjpeg.h"        /* Common decls for cjpeg/djpeg applications */
代码语言:txt
复制
#define LOG_TAG "jni"
代码语言:txt
复制
//#define LOGW(...)  __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
代码语言:txt
复制
//#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
代码语言:txt
复制
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
代码语言:txt
复制
#define true 1
代码语言:txt
复制
#define false 0
代码语言:txt
复制
typedef uint8_t BYTE;
代码语言:txt
复制
char *error;
代码语言:txt
复制
struct my_error_mgr {
代码语言:txt
复制
    struct jpeg_error_mgr pub;
代码语言:txt
复制
    jmp_buf setjmp_buffer;
代码语言:txt
复制
};
代码语言:txt
复制
typedef struct my_error_mgr * my_error_ptr;
代码语言:txt
复制
METHODDEF(void)
代码语言:txt
复制
my_error_exit (j_common_ptr cinfo)
代码语言:txt
复制
{
代码语言:txt
复制
my_error_ptr myerr = (my_error_ptr) cinfo->err;
代码语言:txt
复制
(*cinfo->err->output_message) (cinfo);
代码语言:txt
复制
error=(char*)myerr->pub.jpeg_message_table[myerr->pub.msg_code];
代码语言:txt
复制
LOGE("jpeg_message_table[%d]:%s", myerr->pub.msg_code,myerr->pub.jpeg_message_table[myerr->pub.msg_code]);
代码语言:txt
复制
// LOGE("addon_message_table:%s", myerr->pub.addon_message_table);
代码语言:txt
复制
//  LOGE("SIZEOF:%d",myerr->pub.msg_parm.i[0]);
代码语言:txt
复制
//  LOGE("sizeof:%d",myerr->pub.msg_parm.i[1]);
代码语言:txt
复制
longjmp(myerr->setjmp_buffer, 1);
代码语言:txt
复制
}
代码语言:txt
复制
int generateJPEG(BYTE* data, int w, int h, int quality,
代码语言:txt
复制
                 const char* outfilename, jboolean optimize) {
代码语言:txt
复制
    int nComponent = 3;
代码语言:txt
复制
    // jpeg的结构体,保存的比如宽、高、位深、图片格式等信息
代码语言:txt
复制
    struct jpeg_compress_struct jcs;
代码语言:txt
复制
    struct my_error_mgr jem;
代码语言:txt
复制
    jcs.err = jpeg_std_error(&jem.pub);
代码语言:txt
复制
    jem.pub.error_exit = my_error_exit;
代码语言:txt
复制
    if (setjmp(jem.setjmp_buffer)) {
代码语言:txt
复制
        return 0;
代码语言:txt
复制
    }
代码语言:txt
复制
    jpeg_create_compress(&jcs);
代码语言:txt
复制
    // 打开输出文件 wb:可写byte
代码语言:txt
复制
    FILE* f = fopen(outfilename, "wb");
代码语言:txt
复制
    if (f == NULL) {
代码语言:txt
复制
        return 0;
代码语言:txt
复制
    }
代码语言:txt
复制
    // 设置结构体的文件路径
代码语言:txt
复制
    jpeg_stdio_dest(&jcs, f);
代码语言:txt
复制
    jcs.image_width = w;
代码语言:txt
复制
    jcs.image_height = h;
代码语言:txt
复制
    // 设置哈夫曼编码
代码语言:txt
复制
    jcs.arith_code = false;
代码语言:txt
复制
    jcs.input_components = nComponent;
代码语言:txt
复制
    if (nComponent == 1)
代码语言:txt
复制
        jcs.in_color_space = JCS_GRAYSCALE;
代码语言:txt
复制
    else
代码语言:txt
复制
        jcs.in_color_space = JCS_RGB;
代码语言:txt
复制
    jpeg_set_defaults(&jcs);
代码语言:txt
复制
    jcs.optimize_coding = optimize;
代码语言:txt
复制
    jpeg_set_quality(&jcs, quality, true);
代码语言:txt
复制
    // 开始压缩,写入全部像素
代码语言:txt
复制
    jpeg_start_compress(&jcs, TRUE);
代码语言:txt
复制
    JSAMPROW row_pointer[1];
代码语言:txt
复制
    int row_stride;
代码语言:txt
复制
    row_stride = jcs.image_width * nComponent;
代码语言:txt
复制
    while (jcs.next_scanline < jcs.image_height) {
代码语言:txt
复制
        row_pointer[0] = &data[jcs.next_scanline * row_stride];
代码语言:txt
复制
        jpeg_write_scanlines(&jcs, row_pointer, 1);
代码语言:txt
复制
    }
代码语言:txt
复制
    jpeg_finish_compress(&jcs);
代码语言:txt
复制
    jpeg_destroy_compress(&jcs);
代码语言:txt
复制
    fclose(f);
代码语言:txt
复制
    return 1;
代码语言:txt
复制
}
代码语言:txt
复制
typedef struct {
代码语言:txt
复制
    uint8_t r;
代码语言:txt
复制
    uint8_t g;
代码语言:txt
复制
    uint8_t b;
代码语言:txt
复制
} rgb;
代码语言:txt
复制
char* jstrinTostring(JNIEnv* env, jbyteArray barr) {
代码语言:txt
复制
    char* rtn = NULL;
代码语言:txt
复制
    jsize alen = (*env)->GetArrayLength(env, barr);
代码语言:txt
复制
    jbyte* ba = (*env)->GetByteArrayElements(env, barr, 0);
代码语言:txt
复制
    if (alen > 0) {
代码语言:txt
复制
        rtn = (char*) malloc(alen + 1);
代码语言:txt
复制
        memcpy(rtn, ba, alen);
代码语言:txt
复制
        rtn[alen] = 0;
代码语言:txt
复制
    }
代码语言:txt
复制
    (*env)->ReleaseByteArrayElements(env, barr, ba, 0);
代码语言:txt
复制
    return rtn;
代码语言:txt
复制
}
代码语言:txt
复制
jstring Java_com_effective_bitmap_utils_EffectiveBitmapUtils_compressBitmap(JNIEnv* env,
代码语言:txt
复制
                                                       jobject thiz, jobject bitmapcolor, int w, int h, int quality,
代码语言:txt
复制
                                                       jbyteArray fileNameStr, jboolean optimize) {
代码语言:txt
复制
    AndroidBitmapInfo infocolor;
代码语言:txt
复制
    BYTE* pixelscolor;
代码语言:txt
复制
    int ret;
代码语言:txt
复制
    BYTE * data;
代码语言:txt
复制
    BYTE *tmpdata;
代码语言:txt
复制
    char * fileName = jstrinTostring(env, fileNameStr);
代码语言:txt
复制
    if ((ret = AndroidBitmap_getInfo(env, bitmapcolor, &infocolor)) < 0) {
代码语言:txt
复制
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
代码语言:txt
复制
        return (*env)->NewStringUTF(env, "0");;
代码语言:txt
复制
    }
代码语言:txt
复制
    if ((ret = AndroidBitmap_lockPixels(env, bitmapcolor, (void**)&pixelscolor)) < 0) {
代码语言:txt
复制
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
代码语言:txt
复制
    }
代码语言:txt
复制
    BYTE r, g, b;
代码语言:txt
复制
    data = NULL;
代码语言:txt
复制
    data = malloc(w * h * 3);
代码语言:txt
复制
    tmpdata = data;
代码语言:txt
复制
    int j = 0, i = 0;
代码语言:txt
复制
    int color;
代码语言:txt
复制
    for (i = 0; i < h; i++) {
代码语言:txt
复制
        for (j = 0; j < w; j++) {
代码语言:txt
复制
            color = *((int *) pixelscolor);
代码语言:txt
复制
            r = ((color & 0x00FF0000) >> 16);
代码语言:txt
复制
            g = ((color & 0x0000FF00) >> 8);
代码语言:txt
复制
            b = color & 0x000000FF;
代码语言:txt
复制
            *data = b;
代码语言:txt
复制
            *(data + 1) = g;
代码语言:txt
复制
            *(data + 2) = r;
代码语言:txt
复制
            data = data + 3;
代码语言:txt
复制
            pixelscolor += 4;
代码语言:txt
复制
        }
代码语言:txt
复制
    }
代码语言:txt
复制
    AndroidBitmap_unlockPixels(env, bitmapcolor);
代码语言:txt
复制
    int resultCode= generateJPEG(tmpdata, w, h, quality, fileName, optimize);
代码语言:txt
复制
    free(tmpdata);
代码语言:txt
复制
    if(resultCode==0){
代码语言:txt
复制
        jstring result=(*env)->NewStringUTF(env, error);
代码语言:txt
复制
        error=NULL;
代码语言:txt
复制
        return result;
代码语言:txt
复制
    }
代码语言:txt
复制
    return (*env)->NewStringUTF(env, "1"); //success
代码语言:txt
复制
}
六、三级缓存(LruCache和DiskLruCache实现)

第一次从网络中载入到图片之后,将图片缓存在内存和sd卡中。这样,我们就不用频繁的去网络中载入图片,为了非常好的控制内存问题,则会考虑使用LruCache作为Bitmap在内存中的存放容器,在sd卡则使用DiskLruCache来统一管理磁盘上的图片缓存。

SoftReference和inBitmap參数的结合

采用这种方式存贮作为被LruCache淘汰掉的复用池

採用LruCache作为存放Bitmap的容器,而在LruCache中有一个方法值得留意,那就是entryRemoved,依照文档给出的说法,在LruCache容器满了须要淘汰存放当中的对象腾出空间的时候会调用此方法(注意。这里仅仅是对象被淘汰出LruCache容器,但并不意味着对象的内存会马上被Dalvik虚拟机回收掉),此时能够在此方法中将Bitmap使用SoftReference包裹起来,并用事先准备好的一个HashSet容器来存放这些即将被回收的Bitmap。有人会问。这样存放有什么意义?之所以会这样存放,还须要再提及到inBitmap參数(在Android3.0才開始有的,详情查阅API中的BitmapFactory.Options參数信息)。这个參数主要是提供给我们进行复用内存中的Bitmap.

在满足以上条件的时候。系统对图片进行decoder的时候会检查内存中是否有可复用的Bitmap。避免我们频繁的去SD卡上载入图片而造成系统性能的下降,毕竟从直接从内存中复用要比在SD卡上进行IO操作的效率要提高几十倍.

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么Bitmap会导致OOM?
    • Bitmap基础知识
  • 一、Bitmap质量压缩
  • 二、缩放法压缩
  • 三、采样率压缩(大小压缩)
  • 四、Bitmap色彩模式压缩
  • 五、libjpeg.so库压缩
  • 六、三级缓存(LruCache和DiskLruCache实现)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档