【Android性能优化】图片内存占用及存储大小压缩算法

文章目录

  • 图片的物理内存的计算
  • 图片运行内存计算
  • 优化方案
  • jpg图片压缩算法
    • 使用Android系统压缩算法api
      • 质量压缩
      • 尺寸压缩
      • 采样率压缩
  • webp图片压缩
    • 使用自定义压缩算法
  • 扩展

图片是我们app开发中最常见的一种展示形态,因此对于图片的压缩和内存空间占用是非常有必要理解和掌握的,这篇文章借鉴和归纳了一下图片加载到内存空间的占用及常见压缩算法等,通过这样的总结和归纳希望我们再次面对图片问题的时候有一个很明朗的思维和处理方案。

图片的物理内存的计算

对于一张图片,无论是jpg、png、bmp或者其他格式,我们光知道他们像素总数是1280*720,是无法计算出图片大小的~~因为压缩方式、编码等都不一样.
但是我们要有一个这样的概念
bmp 无压缩情况下的原图
png 无损压缩算法格式
jpg 有损压缩算法格式
这里我们暂时不过多讨论物理内存占用情况,android也提供了webp的有损压缩格式很大程度上减小了物理内存的空间占用。这里我们特别的研究一下运行时内存占用及优化方案。

图片运行内存计算

我们常规的认知里计算图片占用运行时虚拟机内存的计算方法

图片分辨率 * 每个像素点大小

而每个像素点的大小的计算方式是和图片质量有关系如下表所示

Possible bitmap configurations. A bitmap configuration describes how pixels are stored. This affects the quality (color depth) as well as the ability to display transparent/translucent colors. 参考GoogleDeveloper

BitMap.Config 释义
ALPHA_8 此时图片只有alpha值,没有RGB值,一个像素占用一个字节
ARGB_4444 一个像素占用2个字节,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各占4个bites,共16bites,即2个字节
ARGB_8888 一个像素占用4个字节,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各占8个bites,共32bites,即4个字节这是一种高质量的图片格式,电脑上普通采用的格式。它也是Android手机上一个BitMap的默认格式。
RGB_565 从Android4.0开始,该选项无效。即使设置为该值,系统任然会采用 ARGB_8888来构造图片

?那么一张图片无论jpg或者png加载到内存中的时候占用的内存空间是分辨率*Bitmap.Config占用的字节数吗
是的,这个答案是不准确的。

如果我们要获取这个答案的话我们可以自己写demo去测试,分别加载不同资源目录下如assests;res下不同屏幕密度下的资源文件夹如drawable-hdpi,drawable-xdpi;磁盘目录下等 具体的测试过程可以参考这篇文章

private void loadResImage(ImageView imageView) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.weixin, options);
    //Bitmap bitmap = BitmapFactory.decodeFile("mnt/sdcard/weixin.png", options);
    imageView.setImageBitmap(bitmap);
    Log.i("!!!!!!", "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
    Log.i("!!!!!!", "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());
    Log.i("!!!!!!", "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);
    Log.i("!!!!!!", "imageview.width:" + imageView.getWidth() + ":::imageview.height:" +    imageView.getHeight());
}

这里我们只备注一下测试结果:
当我们把图片放在不同位置去分别加载的时候,得到的在内存中占用大小是不一样的如

资源文件夹 对应像素密度值
mdpi 160
hdpi 240
xhdpi 320
xxhdpi 480

结论如下(当我们测试一张1024*768分辨率的图片并且图片质量为ARGB_8888也就是像素占用字节数为4)

  • 图片放在assests和磁盘里加载的话占用内存大小计算方式为10247684
  • 图片放在不同密度对应的文件夹时占用内存大小为1024密度值/260768密度值/2604
    我们看到在不同密度值代表的文件夹的图片会经过一次分辨率转换 然后在进行图片加载;这些理论是基于Android原生的图片处理算法得出的,如果使用了第三方的图片加载库,这些理论就有可能不成立了,因为他们对不同的图片进行了相应的图片压缩算法的处理。如文章最后我们要说的一个图片压缩算法一样。

优化方案

基于以上的知识储备,因此理解图片加载的优化方案就很好理解我们可以通过以下几个方案来处理图片优化相关

  • 存放在apk中的资源文件如果是大文件的话可以考虑使用jpg格式替换png格式;
  • 加载资源文件res不是只放一份文件就可以,如果需要兼容不同的设备就需要放置不同的文件防止造成图片加载到内存空间占用过大
  • 处理图片的分辨率或者图片质量达到兼容视觉和内存的效果
  • google推荐使用Glide处理图片缓存问题,但是这些基础的图片处理策略我们还是要清楚

jpg图片压缩算法

首先明确一下这里的压缩算法,针对的都是对于图片存储的物理空间大小的算法。对于图片的压缩不一定能改变其在内存空间占用的大小(具体参考上面篇幅)

android 原生的图片压缩算法是基于一个叫skia开源的引擎(基于JPEG引擎修改),但是去掉了比较消耗cpu的哈夫曼算法;这样处理的结果是减少了cpu的消耗但是图片处理的效果并不一定是最好的,改为定长算法 导致了拍一张高清图片会占用很大的存储空间。这也还是计算机领域中存储空间和性能的博弈;备注:android原生的图片处理引擎解码目前依然保留的是哈夫曼算法

使用Android系统压缩算法api

质量压缩

质量压缩是保持像素的前提下改变图片的位深及透明度,(即:通过算法抠掉(同化)了图片中的一些某个些点附近相近的像素),达到降低质量压缩文件大小的目的。常用在不需要直接操作原图的一些场景中,如 上传图片,图片另存

使用原生api的进行质量压缩一些示例:

 /**
     * 质量压缩方法
     *
     * @param image
     * @return
     */
    public static Bitmap compressImage(Bitmap image) {

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        int options = 100;
        while (baos.toByteArray().length / 1024 > 100) {  //循环判断如果压缩后图片是否大于100kb,大于继续压缩
            baos.reset();//重置baos即清空baos
            //第一个参数 :图片格式 ,第二个参数: 图片质量,100为最高,0为最差  ,第三个参数:保存压缩后的数据的流
            image.compress(Bitmap.CompressFormat.JPEG, options, baos);//这里压缩options%,把压缩后的数据存放到baos中
            options -= 10;//每次都减少10
        }
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中
        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream数据生成图片
        return bitmap;
    }


 /**
     * 质量压缩
     * 设置bitmap options属性,降低图片的质量,像素不会减少
     * 第一个参数为需要压缩的bitmap图片对象,第二个参数为压缩后图片保存的位置
     * 设置options 属性0-100,来实现压缩
     *
     * @param bmp
     * @param file
     */ public static void qualityCompress(Bitmap bmp, File file) {
          // 0-100 100为不压缩
         int quality = 20;
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         // 把压缩后的数据存放到baos中
        bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
     }


尺寸压缩

属于一种减少了像素的压缩方式,达到一种缩略图片的效果,常用在 加载缩略图

使用原生api的进行质量压缩一些示例:

 /**
     * 压缩图片使用,采用BitmapFactory.decodeFile。这里是尺寸压缩
     * @param context
     * @param imageUri
     * @return
     */
    public Bitmap bitmapFactory(Context context,Uri imageUri){
        String[] filePathColumns = {MediaStore.Images.Media.DATA};
        Cursor c = context.getContentResolver().query(imageUri, filePathColumns, null, null, null);
        c.moveToFirst();
        int columnIndex = c.getColumnIndex(filePathColumns[0]);
        String imagePath = c.getString(columnIndex);
        c.close();

        // 配置压缩的参数
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true; //获取当前图片的边界大小,而不是将整张图片载入在内存中,避免内存溢出
        BitmapFactory.decodeFile(imagePath, options);
        options.inJustDecodeBounds = false;
        inSampleSize的作用就是可以把图片的长短缩小inSampleSize倍,所占内存缩小inSampleSize的平方
        options.inSampleSize = caculateSampleSize(options,500,50);
        Bitmap bm = BitmapFactory.decodeFile(imagePath, options); // 解码文件
         return  bm;
    }

    /**
     * 计算出所需要压缩的大小
     * @param options
     * @param reqWidth  我们期望的图片的宽,单位px
     * @param reqHeight 我们期望的图片的高,单位px
     * @return
     */
    private int caculateSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        int sampleSize = 1;
        int picWidth = options.outWidth;
        int picHeight = options.outHeight;
        if (picWidth > reqWidth || picHeight > reqHeight) {
            int halfPicWidth = picWidth / 2;
            int halfPicHeight = picHeight / 2;
            while (halfPicWidth / sampleSize > reqWidth || halfPicHeight / sampleSize > reqHeight) {
                sampleSize *= 2;
            }
        }
        return sampleSize;
    }



采样率压缩

采样率压缩是改变了图片的像素,他是通过先读取图片的边,然后在自己设定图片的边,然后根据设定,读取图片的像素。是图片压缩常用到的一种处理方案因为他会只把目标尺寸加载进内存这样能一定程度上降低oom的可能性

使用原生api的进行质量压缩一些示例:

public static Bitmap compressImageToBitmap(String imagePath){
		BitmapFactory.Options options = new BitmapFactory.Options();
		//只计算图片的边界不降图片实体加载进内存
		options.inJustDecodeBounds = true;
		Bitmap bitmap = BitmapFactory.decodeFile(imagePath,options);
		//加载真实的图片内容进入内存
		options.inJustDecodeBounds = false;
		//降低图片占用空间为原图片的1/4;宽高都减小了一倍
		options.inSampleSize = 2;
		bitmap = BitmapFactory.decodeFile(imagePath,options);
		return bitmap;

webp图片压缩

WebP(发音:weppy)是一种同时提供了有损压缩与无损压缩(可逆压缩)的图片文件格式,派生自影像编码格式VP8,被认为是WebM多媒体格式的姊妹项目,是由Google在购买On2 Technologies后发展出来,以BSD授权条款发布。

这里不做过多解释,Google推荐使用,压缩效果也比较明显可以作为一种减小压缩包的一种尝试,android 4.4以后完全兼容

使用自定义压缩算法

在上面我们讲到了android为了减少cpu性能消耗,删减了图片处理引擎的哈夫曼算法,这里我们就可以根据这个原理直接使用libjpeg库,使用哈夫曼算法替代原生的定长算法来达到最佳图片压缩;当然这里我们可以根据机型来控制.对于一些低端机型还是建议使用原生的方法来兼容性能的平衡

自定义压缩算法的核心还是libjpeg库
libjpeg下载地址

每一个像素都有三个信息RGB这里通过计算Bitmap的rgb值保存到一维数组;然后通过使用jpeg引擎的哈夫曼算法来优化图片压缩的程度。

核心代码


#include "bitherlibjni.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

//统一编译方式
extern "C" {
#include "jpeg/jpeglib.h"
#include "jpeg/cdjpeg.h"		/* Common decls for cjpeg/djpeg applications */
#include "jpeg/jversion.h"		/* for version message */
#include "jpeg/android/config.h"
}


#define LOG_TAG "jni"
#define LOGW(...)  __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#define true 1
#define false 0

typedef uint8_t BYTE;

char *error;
struct my_error_mgr {
  struct jpeg_error_mgr pub;
  jmp_buf setjmp_buffer;
};

typedef struct my_error_mgr * my_error_ptr;

METHODDEF(void)
my_error_exit (j_common_ptr cinfo)
{
  my_error_ptr myerr = (my_error_ptr) cinfo->err;
  (*cinfo->err->output_message) (cinfo);
  error=(char*)myerr->pub.jpeg_message_table[myerr->pub.msg_code];
  LOGE("jpeg_message_table[%d]:%s", myerr->pub.msg_code,myerr->pub.jpeg_message_table[myerr->pub.msg_code]);
 // LOGE("addon_message_table:%s", myerr->pub.addon_message_table);
//  LOGE("SIZEOF:%d",myerr->pub.msg_parm.i[0]);
//  LOGE("sizeof:%d",myerr->pub.msg_parm.i[1]);
  longjmp(myerr->setjmp_buffer, 1);
}

int generateJPEG(BYTE* data, int w, int h, int quality,
		const char* outfilename, jboolean optimize) {

	//jpeg的结构体,保存的比如宽、高、位深、图片格式等信息,相当于java的类
	struct jpeg_compress_struct jcs;

	//当读完整个文件的时候就会回调my_error_exit这个退出方法。setjmp是一个系统级函数,是一个回调。
	struct my_error_mgr jem;
	jcs.err = jpeg_std_error(&jem.pub);
	jem.pub.error_exit = my_error_exit;
	if (setjmp(jem.setjmp_buffer)) {
		return 0;
	}

	//初始化jsc结构体
	jpeg_create_compress(&jcs);
	//打开输出文件 wb:可写byte
	FILE* f = fopen(outfilename, "wb");
	if (f == NULL) {
		return 0;
	}
	//设置结构体的文件路径
	jpeg_stdio_dest(&jcs, f);
	jcs.image_width = w;//设置宽高
	jcs.image_height = h;

	//看源码注释,设置哈夫曼编码:/* TRUE=arithmetic coding, FALSE=Huffman */
	jcs.arith_code = false;
	int nComponent = 3;
	/* 颜色的组成 rgb,三个 # of color components in input image */
	jcs.input_components = nComponent;
	//设置结构体的颜色空间为rgb
	jcs.in_color_space = JCS_RGB;

	//全部设置默认参数/* Default parameter setup for compression */
	jpeg_set_defaults(&jcs);
	//是否采用哈弗曼表数据计算 品质相差5-10倍
	jcs.optimize_coding = optimize;
	//设置质量
	jpeg_set_quality(&jcs, quality, true);
	//开始压缩,(是否写入全部像素)
	jpeg_start_compress(&jcs, TRUE);

	JSAMPROW row_pointer[1];
	int row_stride;
	//一行的rgb数量
	row_stride = jcs.image_width * nComponent;
	//一行一行遍历
	while (jcs.next_scanline < jcs.image_height) {
		//得到一行的首地址
		row_pointer[0] = &data[jcs.next_scanline * row_stride];

		//此方法会将jcs.next_scanline加1
		jpeg_write_scanlines(&jcs, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数
	}
	jpeg_finish_compress(&jcs);//结束
	jpeg_destroy_compress(&jcs);//销毁 回收内存
	fclose(f);//关闭文件

	return 1;
}

/**
 * byte数组转C的字符串
 */
char* jstrinTostring(JNIEnv* env, jbyteArray barr) {
	char* rtn = NULL;
	jsize alen = env->GetArrayLength( barr);
	jbyte* ba = env->GetByteArrayElements( barr, 0);
	if (alen > 0) {
		rtn = (char*) malloc(alen + 1);
		memcpy(rtn, ba, alen);
		rtn[alen] = 0;
	}
	env->ReleaseByteArrayElements( barr, ba, 0);
	return rtn;
}

jstring Java_net_bither_util_NativeUtil_compressBitmap(JNIEnv* env,
		jclass thiz, jobject bitmapcolor, int w, int h, int quality,
		jbyteArray fileNameStr, jboolean optimize) {
	BYTE *pixelscolor;
	//1.将bitmap里面的所有像素信息读取出来,并转换成RGB数据,保存到二维byte数组里面
	//处理bitmap图形信息方法1 锁定画布
	AndroidBitmap_lockPixels(env,bitmapcolor,(void**)&pixelscolor);

	//2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
	BYTE *data;
	BYTE r,g,b;
	data = (BYTE*)malloc(w*h*3);//每一个像素都有三个信息RGB
	BYTE *tmpdata;
	tmpdata = data;//临时保存data的首地址
	int i=0,j=0;
	int color;
	for (i = 0; i < h; ++i) {
		for (j = 0; j < w; ++j) {
			//解决掉alpha
			//获取二维数组的每一个像素信息(四个部分a/r/g/b)的首地址
			color = *((int *)pixelscolor);//通过地址取值
			//0~255:
//			a = ((color & 0xFF000000) >> 24);
			r = ((color & 0x00FF0000) >> 16);
			g = ((color & 0x0000FF00) >> 8);
			b = ((color & 0x000000FF));
			//改值!!!----保存到data数据里面
			*data = b;
			*(data+1) = g;
			*(data+2) = r;
			data = data + 3;
			//一个像素包括argb四个值,每+4就是取下一个像素点
			pixelscolor += 4;
		}
	}
	//处理bitmap图形信息方法2 解锁
	AndroidBitmap_unlockPixels(env,bitmapcolor);
	char* fileName = jstrinTostring(env,fileNameStr);
	//调用libjpeg核心方法实现压缩
	int resultCode = generateJPEG(tmpdata,w,h,quality,fileName,optimize);
	if(resultCode ==0){
		jstring result = env->NewStringUTF("-1");
		return result;
	}
	return env->NewStringUTF("1");
}

扩展

mipmap文件夹和drawable文件夹下的图片文件放置规则
参考google官网支持不同的像素密度

将应用图标放在 mipmap 目录中
与其他所有位图资源一样,对于应用图标,您也需要提供特定于密度的版本。不过,某些应用启动器显示的应用图标会比设备的密度级别所要求的大差不多 25%。

例如,如果设备的密度级别为 xxhdpi 且您提供的最大应用图标在 drawable-xxhdpi 中,那么启动器应用会放大此图标,这会使其看起来不太清晰。因此,您应在 mipmap-xxxhdpi 目录中提供一个密度更高的启动器图标,而后启动器便可改用 xxxhdpi 资源。

由于应用图标可能会像这样放大,因此您应将所有应用图标都放在 mipmap 目录中,而不是放在 drawable 目录中。与 drawable 目录不同,所有 mipmap 目录都会保留在 APK 中,即使您构建特定于密度的 APK 也是如此。这样,启动器应用便可选取要显示在主屏幕上的最佳分辨率图标。

上面三段描述的都是启动图标,那么平时使用的图片资源文件应该放在哪里呢,经验之谈还是放在drawable文件夹下区分不同的屏幕密度,启动图标放在mipmap下区分不同的屏幕密度

  • 放在mipmap的图片文件和放在drawable相同层级文件夹的图片占用运行时内存空间一致没有节省内存的作用 ,因此这里不做区分关键
  • 使用mipmap的资源图片会和设计稿存一定的误差,具体可能和mipmap的压缩算法有一定关系可研究mipmap纹理过滤
  • 在App中,无论你将图片放在drawable还是mipmap目录,系统只会加载对应density中的图片。而在Launcher中,如果使用mipmap,那么Launcher会自动加载更加合适的密度的资源,所以启动图标一定要区分适配不同屏幕密度的分辨率并放置在对应的目录里放置在launcher中展示失真

【参考文档1】https://www.jianshu.com/p/e49ec7d053b3
【参考文档2】https://developer.android.com/reference/android/graphics/Bitmap.Config.html
【可借鉴源码1】鲁班库
【可借鉴源码2】Compressor
【在线处理png】TinyPng

你可能感兴趣的:(#,Android开发)