【0193】Android 性能优化之Bitmap内存管理及优化

1.Bitmap的加载

图片存在的几种形式:File、Stream、Bitmap内存的形式

常用的三个方法:最后都是调用的是decodeStream

static Bitmap    decodeFile(String pathName, BitmapFactory.Options opts)

static Bitmap    decodeResource(Resources res, int id, BitmapFactory.Options opts)

static Bitmap    decodeStream(InputStream is)

【源码】BitmapFactory.java

情况加载:如果是Assets目录下的资源(AssetManager.AssetInputStream)则会调用Nativite方法

 1 public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
 2         // we don't throw in this case, thus allowing the caller to only check
 3         // the cache, and not force the image to be decoded.
 4         if (is == null) {
 5             return null;
 6         }
 7         validate(opts);
 8 
 9         Bitmap bm = null;
10 
11         Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
12         try {
13             if (is instanceof AssetManager.AssetInputStream) {
14                 final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
15                 bm = nativeDecodeAsset(asset, outPadding, opts);  //调用的是本地方法cpp,详见Bitmap.cpp
16             } else {
17                 bm = decodeStreamInternal(is, outPadding, opts);  
18             }
19 
20             if (bm == null && opts != null && opts.inBitmap != null) {
21                 throw new IllegalArgumentException("Problem decoding into existing bitmap");
22             }
23 
24             setDensityFromOptions(bm, opts);
25         } finally {
26             Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
27         }
28 
29         return bm;
30     }

2.BitmapFactory的option参数

BitmapFactory在decode的时候还会参考inDensity和inTargetDensity两个属性

先认识下两个Bitmap的参数:

【inSampleSize】注意inSampleSize只能是2的幂,如不是2的幂下转到最大的2的幂,而且inSampleSize>=1

【inDensity】Bitmap的像素密度;Options中的inDensity属性会根据drawable文件夹的分辨率来赋值

 如果放置图片的drawable文件夹后跟”-xxxhdpi”字样, 那么decode这张图片时候inDensity属性就是640

   px = dp*Density

【inTargetDensity】Bitmap最终的像素密度,inTartgetDensity会根据屏幕的像素密度来赋值

 通过下面的代码可以获取屏幕的inTargentDensity;

1 //屏幕的density可由此代码获得,这个就是inTargetDensity
2 DisplayMetrics metrics = new DisplayMetrics();
3 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
4 wm.getDefaultDisplay().getMetrics(metrics);
5 int inTargetDensity = metircs.densityDpi; 

【非常重要的公式】计算仅针对于drawable文件夹的图片来说

   而对于一个file或者stream那么inDensity和inTargetDensity是不考虑的! 他们默认就是0

【重要公式】输出图片的宽高= 原图片的宽高 / inSampleSize * (inTargetDensity / inDensity)

3.decodeResource的优化源码

 调用的关系如下:

BitmapFactory.decodeResource();
      -->decodeResourceStream()

 【源码】

首先看inDesity的值设置:在inDesity值为0时,会根据TypeValue中类型进行赋值。类型为TypedValue.DENSITY_DEFAULT,inDensity赋值为160,否则不为空的状态下设置为传入的TypeValue类的density值。

再次获取该res文件夹对应的inTargetDensity值,传入res的目的也是为了获取该参数

最后再调用decodeStream()方法处理

 1 public static Bitmap decodeResourceStream(Resources res, TypedValue value,
 2             InputStream is, Rect pad, Options opts) {
 3         validate(opts);
 4         if (opts == null) {
 5             opts = new Options();
 6         }
 7 
 8         if (opts.inDensity == 0 && value != null) {
 9             final int density = value.density;
10             if (density == TypedValue.DENSITY_DEFAULT) {
11                 opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
12             } else if (density != TypedValue.DENSITY_NONE) {
13                 opts.inDensity = density;
14             }
15         }
16         
17         if (opts.inTargetDensity == 0 && res != null) {
18             opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
19         }
20         
21         return decodeStream(is, pad, opts);
22     }

4.图片的质量压缩

【原理】通过算法抠掉(同化)了图片中的一些某个些点附近相近的像素,达到降低质量介绍文件大小的目的。降低的图片质量,减小了图片的信息。

形象的说法:相邻四个像素值:#FFCCFF #FFCCFE #FFCCFD #FFCCFC 经过压缩之后都变为了#FFCCFF。在信息存储上大小会改变,同样,存储4个不同的值和存储4个一样的值,当然是存储4个一样的值更省空间。

【注意】它其实只能实现对file的影响,对加载这个图片出来的bitmap内存是无法节省的,只改变图像的存储空间的大小,不会改变图片的分辨率

              因为bitmap在内存中的大小是按照像素计算的,也就是width*height,对于质量压缩,并不会改变图片的真实的像素(像素大小不会变)

public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)

【参数1】【format】压缩之后的图片的格式

【参数2】【quality】范围:0-100。 0表示小尺寸压缩,100表示压缩以获得最高质量。 某些格式,如无损的PNG,忽略quality设置。

【参数3】【stream】压缩之后的输出流

【返回值】true 压缩成功并将数据成功写入到指定的输出流

Bitmap压缩之后写入指定的输出流,如下面的实例实现了此功能。

如果返回true,则可以通过将相应的输入流传递给BitmapFactory.decodeStream()来重建Bitmap。

注意:并非所有Formats都直接支持所有位图配置,因此BitmapFactory返回的位图的位深会改变,可能丢失了每像素的alpha(例如,JPEG仅支持不透明像素)。

使用此方法可能比较耗时,因此需要开辟子线程。

 1 public class MainActivity extends AppCompatActivity {
 2 
 3     private File imageFile;
 4     private File sdFile;
 5 
 6     @Override
 7     protected void onCreate(Bundle savedInstanceState) {
 8         super.onCreate(savedInstanceState);
 9         setContentView(R.layout.activity_main);
10         sdFile = Environment.getExternalStorageDirectory();
11         imageFile = new File(sdFile, "Chrysanthemum2.jpg");
12     }
13 
24     public void qualityCompress(View v){
25 
26         BitmapFactory.Options options = new BitmapFactory.Options();
27         Bitmap bitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options);
28         //压缩图片
29         compressImageToFile(bitmap, new File(sdFile,"qualityCompress.jpeg"));
30     }
31     
32     public static void compressImageToFile(Bitmap bmp,File file){
33         //0~100
34         int quality = 50;  //设置图判断图片的压缩质量值
35         ByteArrayOutputStream baos = new ByteArrayOutputStream();
36         bmp.compress(Bitmap.CompressFormat.JPEG, quality , baos );
37         try {
38             FileOutputStream fos = new FileOutputStream(file);
39             fos.write(baos.toByteArray());
40             fos.flush();
41             fos.close();
42         } catch (Exception e) {
43             // TODO Auto-generated catch block
44             e.printStackTrace();
45         }
46     }

质量压缩之后的图片的大小的改变对比:

5.图片的分辨率的压缩

压缩之后的分辨率宽高分别缩小为原来的1/4。

【源码】

 1 public void sizeCompress(View v){
 2         File sdFile = Environment.getExternalStorageDirectory();
 3         File imageFile = new File(sdFile, "Chrysanthemum2.jpg");
 4         BitmapFactory.Options options = new BitmapFactory.Options();
 5         Bitmap bitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options);
 6         compressBitmapToFileBySize(bitmap, new File(sdFile,"sizeCompress.jpeg"));
 7 
 8     }
 9 
10     /**
11      * 2.尺寸压缩
12      * 通过减少单位尺寸的像素值,正真意义上的降低像素。1020*8880--
13      * 使用场景:缓存缩略图的时候(头像处理)
14      *
15      * @param bmp
16      * @param file
17      */
18     public static void compressBitmapToFileBySize(Bitmap bmp, File file){
19         //压缩尺寸倍数,值越大,图片的尺寸就越小
20         int ratio = 4;
//[1]新建了一个空的bitmap对象,设置参数
21 Bitmap result = Bitmap.createBitmap(bmp.getWidth()/ratio, bmp.getHeight()/ratio, 22 Bitmap.Config.ARGB_8888); 23 //【2】将空的bitmap作为参数,新建一个canvas 24 Canvas canvas = new Canvas(result); 25 RectF rect = new RectF(0, 0, bmp.getWidth()/ratio, bmp.getHeight()/ratio);
//【3】 以原bitmap,在画布上画bitmap
26 canvas.drawBitmap(bmp, null, rect , null); 27 28 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 29 /**【4】输出bitmap 30 * 从此处着手,可以增加质量压缩 31 */ 32 result.compress(Bitmap.CompressFormat.JPEG, 100, baos); 33 try { 34 FileOutputStream fos = new FileOutputStream(file); 35 fos.write(baos.toByteArray()); 36 fos.flush(); 37 fos.close(); 38 } catch (Exception e) { 39 e.printStackTrace(); 40 } 41 }

6.采样率压缩

 1         /**
 2          * 设置图片的采样率,降低图片像素
 3          * @param filePath
 4          * @param file
 5          */
 6         public static void compressBitmap(String filePath, File file){
 7             // 数值越高,图片像素越低
 8             int inSampleSize = 8;
 9             BitmapFactory.Options options = new BitmapFactory.Options();
10             options.inJustDecodeBounds = false;
11 //            options.inJustDecodeBounds = true;//为true的时候不会真正加载图片,而是得到图片的宽高信息。
12             //采样率
13             options.inSampleSize = inSampleSize;
14             Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
15 
16             ByteArrayOutputStream baos = new ByteArrayOutputStream();
17             // 把压缩后的数据存放到baos中
18             bitmap.compress(Bitmap.CompressFormat.JPEG, 100 ,baos);
19             try {
20                 if(file.exists())
21                 {
22                     file.delete();
23                 }
24                 else {
25                     file.createNewFile();
26                 }
27                 FileOutputStream fos = new FileOutputStream(file);
28                 fos.write(baos.toByteArray());
29                 fos.flush();
30                 fos.close();
31             } catch (Exception e) {
32                 e.printStackTrace();
33             }
34         }

7.图片性能增加哈夫曼算法

7.1 引子

【问】IOS拍照1M的图片要比安卓拍照排出来的5M的图片还要清晰。
都是在同一个环境下,保存的都是JPEG。为什么?

【答】与图像处理引擎相关

95年 JPEG处理引擎,用于最初的在PC上面处理图片的引擎。

05年 skia开源的引擎, 开发了一套基于JPEG处理引擎的第二次开发。便于浏览器的使用。

07年, 安卓上面用的什么引擎?
skia引擎,阉割版。
谷歌拿了skia 思考了半天做了一个决定,去掉一个编码算法---哈夫曼算法。采用定长编码算法。
但是解码还是保留了哈夫曼算法。
导致了图片处理后文件变大了。
理由:当时由于CPU和内存在手机上都非常吃紧 性能差,由于哈夫曼算法非常吃CPU,被迫用了其他的算法。

我们的优化:绕过安卓Bitmap API层,来自己编码实现----修复使用哈夫曼算法

【jpeg官网地址】https://libjpeg-turbo.org/

【jpeg引擎】主要工作:jpeg的编码和解码

【webp格式】摘自百度百科

WebP格式,谷歌(google)开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有JPEG的2/3,并能节省大量的服务器宽带资源和数据空间。Facebook Ebay等知名网站已经开始测试并使用WebP格式。
但WebP是一种有损压缩。相较编码JPEG文件,编码同样质量的WebP文件需要占用更多的计算资源。
桌面版Chrome可打开WebP格式。

 7.2 算法简述

【参考文章】https://blog.csdn.net/liufeng_king/article/details/8720896

argb
一个像素点包涵四个信息:alpha,red,green,blue

a b c d e

abcde acdbe bacde ……

101010100011100
a:001
b:010
c:011
d:100
e:101
用3位来表示一个字符信息,属于定长编码的最优。

abcde
001 010 011 100 101
加权信息编码

a:80%
b:10%
c:10%
d:0%
e:0%
这种情况,编码就可以优化了
a:01
b:10
c:11
优化后的abc:01 10 11
优化前的abc:001 010 011

由上面的讲述可知,Android去掉编码算法---哈夫曼算法,但是解码还是保留了哈夫曼算法。因此在改进的时候只需要做编码工作即可。

 7.3 代码实现

【1】下载库:http://www.ijg.org/

微信中使用的也是jpeg库,是经过自己改良的。但是,在最新版本微信apk解压之后没有看到该库。

目录结构:

步骤流程:

1.导入库文件libjpegbither.so
2.导入头文件
3.写mk文件
Android.mk
Applicatoin.mk

4.写代码
C++: XX.cpp
C: XX.c

1.将android的bitmap解码,并转换成RGB数据
一个图片信息---像素点(argb)
alpha去掉

2.JPEG对象分配空间以及初始化
3.指定压缩数据源
4.获取文件信息
5.为压缩设置参数,比如图像大小、类型、颜色空间
boolean arith_code; /* TRUE=arithmetic coding, FALSE=Huffman */
6.开始压缩
jpeg_start_compress()
7.压缩结束
jpeg_finish_compress()
8.释放资源

 【Android.mk】

 1 LOCAL_PATH := $(call my-dir)
 2 include $(CLEAR_VARS)
 3 LOCAL_MODULE    :=jpegbither  //定义一个模块的名字
 4 LOCAL_SRC_FILES :=libjpegbither.so  //引用的库.so
 5 include $(PREBUILT_SHARED_LIBRARY) 
 6 include $(CLEAR_VARS)
 7 LOCAL_MODULE    :=bitherjni
 8 LOCAL_SRC_FILES :=bitherlibjni.cpp  //书写源码的文件
 9 LOCAL_SHARED_LIBRARIES :=jpegbither
10 LOCAL_LDLIBS := -ljnigraphics -llog  
11 include $(BUILD_SHARED_LIBRARY)

 

 【Application.mk】

1 APP_ABI := armeabi-v7a armeabi     #表示 编译目标 ABI(应用二进制接口)
2 APP_PLATFORM := android-9

 

 【bitherlibjni.cpp】部分截取

 1 jstring Java_net_bither_util_NativeUtil_compressBitmap(JNIEnv* env,
 2         jclass thiz, jobject bitmapcolor, int w, int h, int quality,
 3         jbyteArray fileNameStr, jboolean optimize) {
 4     BYTE *pixelscolor;
 5     //1.将bitmap里面的所有像素信息读取出来,并转换成RGB数据,保存到二维byte数组里面
 6     //处理bitmap图形信息方法1 锁定画布
 7     AndroidBitmap_lockPixels(env,bitmapcolor,(void**)&pixelscolor);
 8 
 9     //2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
10     BYTE *data;
11     BYTE r,g,b;
12     data = (BYTE*)malloc(w*h*3);//每一个像素都有三个信息RGB
13     BYTE *tmpdata;
14     tmpdata = data;//临时保存data的首地址
15     int i=0,j=0;
16     int color;
17     for (i = 0; i < h; ++i) {
18         for (j = 0; j < w; ++j) {
19             //解决掉alpha
20             //获取二维数组的每一个像素信息(四个部分a/r/g/b)的首地址
21             color = *((int *)pixelscolor);//通过地址取值
22             //0~255:
23 //            a = ((color & 0xFF000000) >> 24);
24             r = ((color & 0x00FF0000) >> 16);
25             g = ((color & 0x0000FF00) >> 8);
26             b = ((color & 0x000000FF));
27             //改值!!!----保存到data数据里面
28             *data = b;
29             *(data+1) = g;
30             *(data+2) = r;
31             data = data + 3;
32             //一个像素包括argb四个值,每+4就是取下一个像素点
33             pixelscolor += 4;  //保存的是原始的像素的信息
34         }
35     }
36     //处理bitmap图形信息方法2 解锁
37     AndroidBitmap_unlockPixels(env,bitmapcolor);
38     char* fileName = jstrinTostring(env,fileNameStr);
39     //调用libjpeg核心方法实现压缩
40     int resultCode = generateJPEG(tmpdata,w,h,quality,fileName,optimize);
41     if(resultCode ==0){
42         jstring result = env->NewStringUTF("-1");
43         return result;
44     }
45     return env->NewStringUTF("1");
46 }

 

 【核心代码】

 1 int generateJPEG(BYTE* data, int w, int h, int quality,
 2         const char* outfilename, jboolean optimize) {
 3 
 4     //jpeg的结构体,保存的比如宽、高、位深、图片格式等信息,相当于java的类
 5     struct jpeg_compress_struct jcs;
 6 
 7     //当读完整个文件的时候就会回调my_error_exit这个退出方法。setjmp是一个系统级函数,是一个回调。
 8     struct my_error_mgr jem;
 9     jcs.err = jpeg_std_error(&jem.pub);
10     jem.pub.error_exit = my_error_exit;
11     if (setjmp(jem.setjmp_buffer)) {
12         return 0;
13     }
14 
15     //初始化jsc结构体
16     jpeg_create_compress(&jcs);
17     //打开输出文件 wb:可写byte
18     FILE* f = fopen(outfilename, "wb");
19     if (f == NULL) {
20         return 0;
21     }
22     //设置结构体的文件路径
23     jpeg_stdio_dest(&jcs, f);
24     jcs.image_width = w;//设置宽高
25     jcs.image_height = h;
26 //    if (optimize) {
27 //        LOGI("optimize==ture");
28 //    } else {
29 //        LOGI("optimize==false");
30 //    }
31 
32     //看源码注释,设置哈夫曼编码:/* TRUE=arithmetic coding, FALSE=Huffman */
33     jcs.arith_code = false;
34     int nComponent = 3;
35     /* 颜色的组成 rgb,三个 # of color components in input image */
36     jcs.input_components = nComponent;
37     //设置结构体的颜色空间为rgb
38     jcs.in_color_space = JCS_RGB;
39 //    if (nComponent == 1)
40 //        jcs.in_color_space = JCS_GRAYSCALE;
41 //    else
42 //        jcs.in_color_space = JCS_RGB;
43 
44     //全部设置默认参数/* Default parameter setup for compression */
45     jpeg_set_defaults(&jcs);
46     //是否采用哈弗曼表数据计算 品质相差5-10倍
47     jcs.optimize_coding = optimize;
48     //设置质量
49     jpeg_set_quality(&jcs, quality, true);
50     //开始压缩,(是否写入全部像素)
51     jpeg_start_compress(&jcs, TRUE);
52 
53     JSAMPROW row_pointer[1];
54     int row_stride;
55     //一行的rgb数量
56     row_stride = jcs.image_width * nComponent;
57     //一行一行遍历
58     while (jcs.next_scanline < jcs.image_height) {
59         //得到一行的首地址
60         row_pointer[0] = &data[jcs.next_scanline * row_stride];
61 
62         //此方法会将jcs.next_scanline加1
63         jpeg_write_scanlines(&jcs, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数
64     }
65     jpeg_finish_compress(&jcs);//结束
66     jpeg_destroy_compress(&jcs);//销毁 回收内存
67     fclose(f);//关闭文件
68 
69     return 1;
70 }

 

【源码下载地址】 https://download.csdn.net/download/wsxingjun/10570553

 

posted @ 2018-07-26 11:16  OzTaking  阅读(1119)  评论(0)    收藏  举报