Android 说说Bitmap那些事
前言
过了一个年,发现自己懈怠,没怎么去写博客了,项目中遇到的问题也很想把它写出来,但是都没有付诸行动,最近重构完项目的一些烂代码,闲下来时也是时候把项目中遇到的问题分享给大家。
好了,唠叨说完,今天主要说下图片压缩那些事,在Android开发中,我们无可避免地都会和图片打交道,其中图片压缩就是我们比较常见和棘手的问题,处理过程中需要注意失真和内存的问题:图片马赛克了,业务或测试就找上门了;Android大量位图(Bitmap)加载导致内存溢出。现在我们谈谈Bitmap一些基本概念。
Bitmap存储格式
Android加载图片的对象就是我们老生常谈的Bitmap了,Bitmap是位图,是由像素点组成的,那它是如何存储在内存中的呢?Bitmap像素点有几种存储方式,对应Bitmap.Config中的枚举值:
- ALPHA_8:
只保存透明度,不保存颜色。每个像素存储为单个半透明(alpha)通道,1个像素点占1个字节,不常用。创建此类型图后,无法在上面画颜色。 - RGB_565:
只存储色值,不存储透明度(不支持alpha通道),默认不透明,RGB分别占5、6、5位,一个像素点占用16位2个字节。当使用不需要高色彩保真度的不透明位图时,此配置比较适合。 - ARGB_4444:
ARGB各用4位存储,1个像素点16位占2个字节。此类型的图片配置导致画质质量较差,建议使用ARGB_8888。 - ARGB_8888:
ARGB各用8位存储,1个像素点32位占4个字节。每个通道(RGB和alpha为半透明)以8位精度(256个可能值)存储,配置灵活,图片也很清晰,应尽可能使用此种方式,缺点比较占内存。 - RGBA_F16:
每个通道(RGB和半透明的alpha)存储为半精度浮点值。每个像素存储在8个字节上。它非常适合用于广色域宽屏和HDR(高动态范围的图片),它所占用的内存是最高的,因此显示的效果也非常好(API26以上才能用)。 - HARDWARE:
硬件位图,其像素数据是存储在显存中,并对图片仅在屏幕上绘制的场景做了优化。简而言之,它把图片的内存只在GPU上存一份,而不需要应用本身的副本,这样的话,理论上通过Hardware方式加载一张图片,内存占用可以比原来少一半,因为像素数据是在显存中的,在有些场景下访问像素数据会发生异常,详见硬件位图。
Bitmap内存计算方法
前面我们讲了Bitmap的几种方式及其像素点所占字节,那一张图片Bitmap在内存中的大小是多少呢?这里我们以像素为500*313的jpg格式图片为例:
这里我们看到了文件的大小34.1KB,那么将这张图片加载到内存中大小是多少呢?Android的BitmapFactory给我们提供了几种加载图片的方式:
BitmapFactory.decodeResource()
:从资源文件中通过id加载bitmapBitmapFactory.decodeFile()
:传入文件路径加载,比如加载sd卡中的文件BitmapFactory.decodeStream()
:从输入流中加载图片BitmapFactory.decodeByteArray()
:从byte数组中加载图片
它们有很多重载函数,具体可去看源码,Bitmap对象的创建过程就不说了,网上也有很多介绍,现在我们一般都会将图片资源放到drawable-xxhdpi目录下,然后调用decodeResource()加载Bitmap对象,根据网上相关资料,采用ARGB_8888(一个像素点32B,即4字节)格式存储的这张图片内存大小(非文件存储大小) = 原宽×原高×像素点所占字节数:
500* 313* (32/8)B = 626000B = 0.6MB
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
Log.e(TAG, "原始大小: " + SampleUtils.getBitmapSize(bitmap));
BitmapFactory.Options options1 = new BitmapFactory.Options();
options1.inPreferredConfig = Bitmap.Config.RGB_565;
// options1.inDensity = 160;
// options1.inScaled = false;
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.test, options1);
Log.e(TAG, "RGB_565: " + SampleUtils.getBitmapSize(bitmap1)
+ " inTargetDensity=" + bitmap1.getDensity()
+ " width=" + bitmap1.getWidth()
+ " height=" + bitmap1.getHeight()
+ " totalSize=" + bitmap1.getWidth() * bitmap1.getHeight() * 2);
BitmapFactory.Options options2 = new BitmapFactory.Options();
options2.inPreferredConfig = Bitmap.Config.ARGB_8888;
// options2.inDensity = 160;
// options2.inScaled = false;
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.test, options2);
Log.e(TAG, "ARGB_8888: " + SampleUtils.getBitmapSize(bitmap2)
+ " inTargetDensity=" + bitmap2.getDensity()
+ " width=" + bitmap2.getWidth() +
" height=" + bitmap2.getHeight()
+ " totalSize=" + bitmap2.getWidth() * bitmap2.getHeight() * 4);
获取Bitmap大小:
/**
* 得到bitmap的大小
*/
public static int getBitmapSize(Bitmap bitmap) {
//API 19
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return bitmap.getAllocationByteCount();
}
//API 12
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount();
}
//在低版本中用一行的字节x高度
return bitmap.getRowBytes() * bitmap.getHeight();
}
上面打印出来的log日志如下:
从日志中推断出
- BitmapFactory.Options.inPreferredConfig默认参数应该是Bitmap.Config.ARGB_8888,一般情况下,这个参数就是我们设置的需要存储像素的格式,所以是可以通过设置它的参数来减少内存,但是也会有不符合配置的情况,详见Android inpreferredconfig参数分析;
- 一般情况下,图片内存大小 = 原宽×原高×像素所占字节数
本着严谨的态度,我们再做一个测试,将图片放到drawable目录下,打印日志结果:
将上面两幅图整理比较,在dpi为480的设备下,加载分辨率为500*313的jpg格式图片:
存储格式 | drawable 目录 | width(px) | height(px) | 像素点字节数(B) | 内存大小 |
---|---|---|---|---|---|
RGB_565 | drawable | 1500 | 939 | 2 | 2817000(2.68MB) |
RGB_565 | drawable-xxhdpi | 500 | 313 | 2 | 313000(0.30MB) |
ARGB_8888 | drawable | 1500 | 939 | 4 | 5634000(5.37MB) |
ARGB_8888 | drawable-xxhdpi | 500 | 313 | 4 | 626000(0.60) |
看到这里,在同种存储格式,不同的drawable目录下,图片的分辨率(宽*高)不一样,drawable目录下的图片加载到内存中宽和高都变为原来的3倍,分辨率变成原来的9倍,所以内存也就变成原来的9倍,由此我们可以猜测图片内存大小与图片存放drawable目录有关,通过查看 decodeResource() 源码,如果没有Options参数传入会生成默认的,最终调用 decodeResourceStream() 方法:
/**
* Decode a new Bitmap from an InputStream. This InputStream was obtained from
* resources, which we pass to be able to scale the bitmap accordingly.
*/
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
//可理解为图片放置在drawable对应的dpi
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//手机的dpi
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
这里说明 decodeResourceStream() 内部做了对Bitmap的密度适配,然后再调用 decodeStream(),Bitmap的decode过程实际上是在native层完成的,跟踪到BitmapFactory.cpp#nativeDecodeStream(),相关缩放代码如下:
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
...
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
...
if (willScale) {
const float sx = scaledWidth / float(decoded->width());
const float sy = scaledHeight / float(decoded->height());
bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
bitmap->allocPixels(&javaAllocator, NULL);
bitmap->eraseColor(0);
SkPaint paint;
paint.setFilterBitmap(true);
SkCanvas canvas(*bitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}
一目了然了,缩放值scale的由来:
scale = (float) targetDensity / density;
这里列出不同drawable目录所对应的设备dpi值:
不同目录 | drawable | drawable-ldpi | drawable-mdpi | drawable-hdpi | drawable-xhdpi | drawable-xxhdpi |
---|---|---|---|---|---|---|
对应设备的dpi | 160 | 120 | 160 | 240 | 320 | 480 |
结合上面两个表格和源码分析,初步得出Android在加载res目录下的资源图片时,会根据不同drawable目录下的图片做一次分辨率转换,转换规则:
- 新图的高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi )
- 新图的宽度 = 原图宽度 * (设备的 dpi / 目录对应的 dpi )
按照我们初步的理论,将表格中存储格式为RGB_565两行数据代入:
- drawable对应160dpi,新图高度:1500 = 500 * (480/160) ;新图宽度: 939 = 313 * (480/160)。
- drawable-xxhdpi对应480dpi,新图高度:500 = 500 * (480/480) ;新图宽度: 313 = 313 * (480/480)。
如果想要进一步验证该结论,可采用dpi为240的测试机进行控制变量的实验,这里就不做比较了,所以我们说前面图片:
内存大小 = 原宽×原高×像素点所占字节数
是不太对的,它还与设备的 dpi 和不同的资源目录有关,具体体现在位于res不同资源目录中的图片,当加载进内存时,会先经过一次分辨率的转换,然后再计算大小。 最终计算方式:
Bitmap内存占用 ≈ 原宽 × 原高× (设备dpi/资源目录对应dpi)^2 × 像素点所占字节数
既然分析了res中图片加载到内存的计算方法,那其它资源图片加载到内存中是不是同样的计算方法呢?现在我们不妨来分析下 decodeFile() 方法,其源码如下:
public static Bitmap decodeFile(String pathName, Options opts) {
validate(opts);
Bitmap bm = null;
InputStream stream = null;
try {
stream = new FileInputStream(pathName);
bm = decodeStream(stream, null, opts);
} catch (Exception e) {
Log.e("BitmapFactory", "Unable to decode stream: " + e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
}
}
}
return bm;
}
内部根据文件路径创建FileInputStream,最终和 decodeResource() 方法一样调用 decodeStream() 方法进解码图片,不同之处在于并没有进行分辨率的转换,所以图片内存大小的计算方法应该为我们最初的公式:
内存大小 = 原宽×原高×像素点所占字节数
下面测试一下(先申请存储权限):
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "/TestPath/test.jpg");
BitmapFactory.Options options3 = new BitmapFactory.Options();
options3.inPreferredConfig = Bitmap.Config.ARGB_8888;
Log.e(TAG, "ARGB_8888: "
+ SampleUtils.getBitmapSize(bitmap3)
+ " inTargetDensity=" + bitmap3.getDensity()
+ " width=" + bitmap3.getWidth() +
" height=" + bitmap3.getHeight()
+ " totalSize=" + bitmap3.getWidth() * bitmap3.getHeight() * 4);
结果如下:
这里只做了一个实验,你也可以用另一张图片重复此操作,结果都是一样的:
内存大小 = 原宽×原高×像素点所占字节数
除此之外,其它方式加载到内存的计算方法也是一样的,如网路资源(本质上也是下载到手机储存)、assert目录、SD卡资源等。所以这里我们得出结论:
只有res内的图片资源会进行分辨率的转换,采用新的分辨率去计算内存大小,而其它资源用的是原图分辨率去计算内存大小
前面长篇大论主要是为了总结这几点:
- 对于非res目录下的图片资源,如本地文件图片,网络图片等,Bitmap内存占用 ≈ 原宽 × 原高× × 像素点所占字节数
- 对于res目录下的不同drawable目录下图片资源,Bitmap内存占用 ≈ 原宽 × 原高× (设备dpi/资源目录对应dpi)^2 × 像素点所占字节数
这里,不得不说下BitmapFactory.Options类的一些参数及其意义:
类型 | 参数 | 意义 |
---|---|---|
boolean | inJustDecodeBounds | 如果为true,解码后不会返回Bitmap对象,但Bitmap宽高将返回到options.outWidth与options.outHeight中;反之返回。主要用于只需获取解码后的Bitmap的大小而不会将bitmap载入内存,浪费内存空间。 |
boolean | inMutable | 为true,代表返回可变属性的Bitmap,反之不可变 |
boolean | inPreferredConfig | 根据指定的Config来进行解码,如:Bitmap.Config.RGB_565等 |
boolean | inSampleSize | 如果值大于1,在解码过程中将按比例返回占更小内存的Bitmap。例如值为2,则对宽高进行缩放一半。这个值很有用,大多数图片压缩都有用到。 |
boolean | inScaled | 如果为true,且inDesity与inTargetDensity都不为0,那么在加载过程中将会根据inTargetDensityl来缩放,在drawn中不依靠于图片自身的缩放属性。 |
boolean | inDensity | Bitmap自身的密度 ,默认为图片所在drawable目录对应的dpi |
boolean | inTargetDensity | Bitmap drawn过程中使用的密度,默认采用当前设备的dpi,同inScreenDensity |
图片文件存储格式
上面我们讲了图片加载到内存大小的计算方法,现在我们来看看图片文件存储大小,常见的图片文件几种格式:JPEG(JPG)、PNG、WEBP。
我们可以将它们理解为图片的容器,它们是经过相对应的压缩算法将原图每个像素点信息转换用另一种数据格式表示,以此达到压缩目的,从而减少图片文件大小。
总而言之,这几种格式就是不同的压缩算法,对应Bitmap.CompressFormat:生成的图片使用指定的图片存储格式
- Bitmap.CompressFormat.JPEG:
采用JPEG压缩算法,是一种有损压缩格式,会在压缩过程中改变图像原本质量,画质越差,对原来的图片质量损伤越大,但是得到的文件比较小,而且JPEG不支持透明度,当遇到透明度像素时,会以黑色背景填充。 - Bitmap.CompressFormat.PNG:
采用PNG算法,是一种支持透明度的无损压缩格式,拥有丰富的颜色显示效果,即使在压缩情况下也能做到不降低图像质量。 - Bitmap.CompressFormat.WEBP:
WEBP是一种同时提供了有损压缩和无损压缩的图片文件格式,在14<=api<=17时,WEBP是一种有损压缩格式,而且不支持透明度,在api18以后WEBP是一种无损压缩格式,而且支持透明度,有损压缩时,在质量相同的情况下,WEBP格式的图片体积比JPEG小40%,但是编码时间比JPEG长8倍。在无损压缩时,无损的WEBP图片比PNG压缩小26%,但是WEBP的压缩时间是PNG格式压缩时间的5倍。
更多详细可参考 Bitmap.CompressFormat
图片压缩方法
前面我们讲了如何计算bitmap在内存的大小,例如我们要从网络上下载一张1920*1080分辨率的图片采用ARGB_8888模式显示,那这张图片所占内存大小:
1920*1080*4B = 7.91MB
一张图片竟然要占用这么大的内存,手机内存是有限的,如果我们不加以控制,那加载几十张又会是什么样场景,最后的结果肯定是OOM闪退了,这显然是无法接受的,所以我们更加要小心翼翼地处理这些图片。
根据前面所说的及相关计算方式,Bitmap的内存优化方法主要是图片存放在合适的drawable目录下、采取合适的存储格式去降低像素点所占字节数、降低图片的分辨率以及Bitmap的复用和缓存,即:
- 将图片存放在合适的drawable目录下
- 减少每个像素点大小
- 降低分辨率
- 复用和缓存
依据中间两点,就有了下面这几种压缩方法
RGB_565压缩
这是通过设置像素点占用的内存大小来达到压缩的效果,一般不建议使用ARGB_4444,因为画质实在是敢看,如果对透明度没有要求,建议可以改成RGB_565,相比ARGB_8888将节省一半的内存开销。
/**
* RGB565压缩
*
* @param context 上下文
* @param id 图片资源id
* @return 压缩后的图片Bitmap
*/
public static Bitmap compressByRGB565(Context context, int id) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeResource(context.getResources(), id, options);
}
质量压缩
保持像素的前提下改变图片的位深及透明度,通过抹除某点附件相近像素,达到降低质量、压缩文件的目的。加载Bitmap的内存并不会减少,文件会变小,用用于服务器上传图片的压缩或保存本地图片文件。
/**
* 质量压缩
*
* @param bmp 图片位图
* @param quality 质量参数0-100,100为不压缩,PNG为无损压缩,此参数对Bitmap.CompressFormat.PNG无效
* @param file 保存压缩后的图片文件
*/
public static void qualityCompress(Bitmap bmp, int quality, File file) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 把压缩后的数据存放到bos中
bmp.compress(Bitmap.CompressFormat.JPEG, quality, bos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(bos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
尺寸压缩(缩放压缩)
改变图片的尺寸,即压缩图片宽度和高度的像素点,从上面的计算公式我们可以得知,这样会降低图片bitmap内存的占用,从而一定程度上减少OOM的概率。但这里我们需要注意,如果压缩比太大,也会由于像素点降低导致图片失真严重,最后图片由高清变成了马赛克。
/**
* 缩放压缩
*
* @param bmp 图片位图
* @param radio 缩放比例,值越大,图片尺寸越小
* @param file 保存压缩后的图片文件
*/
public static void scaleCompress(Bitmap bmp, int radio, File file) {
//设置缩放比
Bitmap result = Bitmap.createBitmap(bmp.getWidth() / radio, bmp.getHeight() / radio, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
RectF rectF = new RectF(0, 0, bmp.getWidth() * 1.0f / radio, bmp.getHeight() * 1.0f / radio);
//将原图画在缩放之后的矩形上
canvas.drawBitmap(bmp, null, rectF, null);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 把压缩后的数据存放到bos中
bmp.compress(Bitmap.CompressFormat.JPEG, 100, bos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(bos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
采样率压缩
其实采样率压缩和尺寸压缩原理是一样的,都是通过减少图片尺寸,压缩图片宽度和高度的像素点,这里充分利用了Options类里的参数设置(可参考上面表格):
- inSampleSize:采样率,为整数,且为2的n次幂,n可以为0,即采样率为1,处理后的图片尺寸与原图一致。当采样率为2时,即宽、高均为原来的1/2,像素则为原来的1/4,其占有内存也为原来的1/4。当设置的采样率小于1时,其效果与1一样。当设置的inSampleSize大于1,不为2的指数时,系统会向下取一个最接近2的指数的值。
- inJustDecodeBounds:当设置为true时,BitmapFactory只会解析图片的原始宽/高信息,不会真正的去加载图片,这一设置堪称绝了。
/**
* 采样率压缩
*
* @param context 上下文
* @param id 图片资源id
* @param destW 目标宽大小
* @param destH 目标高大小
* @return 压缩后的图片Bitmap
*/
public static Bitmap sampleSizeCompress(Context context, int id, int destW, int destH) {
Bitmap bm = null;
int inSampleSize = 1;
//第一次采样
BitmapFactory.Options options = new BitmapFactory.Options();
//该属性设置为true只会加载图片的宽高、类型信息,并不会加载图片具体的像素点
options.inJustDecodeBounds = true;
bm = BitmapFactory.decodeResource(context.getResources(), id, options);
Log.e(TAG, "sampleSizeCompress--压缩之前图片的宽:" + options.outWidth +
"--压缩之前图片的高:" + options.outHeight +
"--压缩之前图片大小:" + options.outWidth * options.outHeight * 4 / 1024 + "kb");
int iWidth = options.outWidth;
int iHeight = options.outHeight;
//对缩放比例进行调整,直到宽和高符合我们要求为止
while (iWidth > destW || iHeight > destH) {
//如果宽高的任意一方的缩放比例没有达到要求,都继续增大缩放比例
//inSampleSize应该为2的n次幂,如果给inSampleSize设置的数字不是2的n次幂,那么系统会就近取
//宽高均为原图的宽高的1/2 内存约为原来的1/4
inSampleSize = inSampleSize * 2;
iWidth = iWidth / inSampleSize;
iHeight = iHeight / inSampleSize;
}
//二次采样开始
//二次采样时我需要将图片完整加载出来显示,inJustDecodeBounds属性要设置为false
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
//设置像素颜色信息,默认Bitmap.Config.ARGB_8888
//bitmapFactoryOptions.inPreferredConfig = Bitmap.Config.RGB_565;
bm = BitmapFactory.decodeResource(context.getResources(), id, options);
Log.e(TAG, "sampleSizeCompress--图片的宽:" + bm.getWidth() +
"--图片的高:" + bm.getHeight() +
"--图片大小:" + bm.getWidth() * bm.getHeight() * 4 / 1024 + "kb");
//返回压缩后的照片
return bm;
}
采样率压缩好处是不会先将大图片读入内存,大大减少了内存的使用,也不必考虑将大图片读入内存后的释放事宜,不足的地方是inSampleSize参数因为是整数,不能很好地保证图片的质量。
图片压缩的过程可能会比较耗时,请不要放到主线程执行,可以使用线程池,然后再将结果回调给主线程即可,这里就不写代码了。当然,这几种压缩方法,你也可以结合使用,充分利用它们各自的优点才是好出路,如质量压缩并不会减少图片转换为bitmap时的内存消耗,为避免出现OOM,我们建议先进行合适的尺寸压缩,然后再进一步进行质量压缩。
现在比较出名的三方压缩库就是号称最接近微信朋友圈图片压缩的 Luban了,它里面最核心的东西就是采样率的算法,感兴趣的同学可以去看看。
微信分享问题
压缩方法介绍完后,这里给大家分享一个案例:微信分享图片的大小,为什么有时候微信分享不成功呢?官方文档说分享图片限制10M,实际结果我1M的图片就分享不出去了。
首先看了下微信分享文档,理清一些事情:微信图片分享是有两种的,一是本地图片路径的分享(10M),二是图片字节流分享,这个是通过intent开启活动分享给微信的,既然涉及到意图,Android开发应该知道intent传递数据是有限制的吧,官方文档说是只能传递1M的数据,但是综合各种复杂情况和手机机型等,512KB才是一个比较通用的值。我们处理的方式就是通过循环质量压缩将图片压缩到512KB下,这里的512KB指的是文件大小,而不是前面的Bitmap占用内存大小,不要搞混了。
/**
* 循环质量压缩便于支持微信分享
*
* 注意:微信限制分享图片10M以下,但是微信分享图片会涉及到启动一个新活动,新活动涉及到Intent传值,而Intent传值有大小限制(不同机型不一样),
* 所以为了适配所有机型,此处图片大小应低于512KB
* PS:如果要传不压缩的大图可以采用imagePath的方式,先把图片先保存在本地(涉及存储权限),然后直接传递图片的地址。
*
* @param bitmap 需要压缩的图片Bitmap
* @param callBack 回调压缩后的图片字节数组
*/
public static void asyncCompressImageForWXShare(final Bitmap bitmap, final OnBitmap2ByteCallBack callBack) {
ThreadUtils.getCachedPool().execute(new Runnable() {
@Override
public void run() {
ByteArrayOutputStream byteAOStream = new ByteArrayOutputStream();
// 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到byteAOStream中
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteAOStream);
byte[] bytes = byteAOStream.toByteArray();
int options = 100;
// 循环判断如果压缩后图片是否大于512KB,大于继续压缩
while (bytes.length / 1024 > 512 && options >= 10) {
// 每次都减少10
options -= 10;
if (options < 0) {
options = 0;
}
// 重置byteAOStream,不然会累加
byteAOStream.reset();
// 把压缩后的数据存放到byteAOStream中
bitmap.compress(Bitmap.CompressFormat.JPEG, options, byteAOStream);
// 将流转换成字节数组
bytes = byteAOStream.toByteArray();
}
Log.e(TAG, "微信分享图片字节数组大小:" + bytes.length);
final byte[] finalBytes = bytes;
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
callBack.onBitmap2ByteCallBack(finalBytes);
}
});
}
});
}
/**
* 异步转换图片字节流的回调
*/
public interface OnBitmap2ByteCallBack {
void onBitmap2ByteCallBack(byte[] imageBytes);
}
这里的线程池引入的依赖是
implementation 'com.blankj:utilcode:1.30.6'
总结
本篇文章,我们主要讲了Bitmap一些重要的概念、压缩方法及其内存优化等等,以上的理论都是基于最原始的加载图片方式,并没有涉及到三方强大的图片处理库,如Glide、fresco等,这些库对于图片的加载、内存肯定是做了很好地优化了,我们也不用花费太多的精力去处理这些问题了,但是我们有必要去了解最基本的东西,整体有个认识,最好也是看看优秀的开源库是怎么处理这些问题,这对我们的能力绝对是一大截的提升。
最后,我想说的是,业务千奇百怪,问题各种各样,不同的人会有不同的情况,我们只需要从中找到一些蛛丝马迹,再结合自己的思考,我相信问题终会得到解决。因为自己最近一直在和图片Bitmap打交道,所以查阅了各种资料,积累了下,再结合自己遇到的问题,才写出这篇文章。
好了,如果讲得不对的地方或有什么疑惑,欢迎评论区留言,也请大家不要舍不得自己的赞👍哦!!