Android之图片压缩

1. 引子

前几天跟服务端的一个妹子联调接口,服务器配置一张图片,几十KB就行,她问我图片从哪里找,我告诉她先随便在网上找个图片链接就行了。结果一运行程序,就崩溃了,出现了下面的异常。

java.lang.OutofMemoryError

内存溢出OOM,我当时一脸懵逼。


图-1 一脸懵逼

于是拿着后台返回的链接去查看了一下图片,是一张6M的壁纸。


图-2 我内心几乎是崩溃的

这只是一个简单的联调,而在联调过程中操作不当导致出现OOM问题,大家就当是个玩笑。其实在Android中很容易出现OOM的异常,特别是对图片操作的时候,所以当面对大图片,需要我们对图片进行适当的压缩,在不影响图片显示的情况下,尽量保证不出现OOM的异常。

2. 概述

在开发中,对于图片的操作,稍有不慎,可能就会消耗大量的内存,导致程序崩溃,所以了解一种通用的技术去处理和加载图片,同时保证UI流畅避免OOM现象,是非常有必要的。那么为什么在Android中对于图片的处理会如此棘手呢?主要有以下一些原因:

  • 通常情况下,移动设备的内存资源是有限的,Android系统会根据手机的屏幕大小和密度,为每个程序设置一个最大内存限制,应用程序消耗的内存不能超过这个最大内存限制,否则就会出现OOM现象。当然,这个内存限制是跟手机配置相关联的。
  • 图片的操作会消耗大量的内存,特别是细节丰富的图片,例如照片。以Galaxy Nexus相机为例子,它拍摄一张2592x1936像素的照片,如果使用的位图配置是ARGB_8888(默认从Android 2.3开始),那么这张照片加载到内存,大约会消耗19MB的内存(2592 x 1936 x 4字节),仅仅是图片消耗内存的数值可能已经超过了某些设备的内存限制
  • Android的UI经常会一次加载多张图片,例如,ListView、GridView、ViewPager等等

图片有各种形状和大小。通常情况下,它们普遍比设备所需要的图片要大一些,例如手机相册显示手机拍摄的照片,而手机的相机分辨率大多时候是要高于手机屏幕的分辨率。鉴于手机的内存有限,我们只需要在内存中加载一个低分辨率的照片版本就可以了,而这个低分辨率的照片应该与显示它的控件相匹配,这就需要对图片进行压缩处理了。

Android中有两种压缩图片的方法。

  • 第一种是针对图片的长宽进行压缩,在将图片加载到内存过程中将图片的长宽进行压缩,获取长宽压缩版的的图片
  • 第二种是针对图片的像素进行压缩,图片加载到内存后,针对图片质量进行压缩,会导致图片质量下降。

3. 图片长宽压缩

3.1 获取加载图片的属性

Android中的BitmapFactory类提供了一些解码方法,decodeByteArray()、decodeFile()、decodeResource()等等,根据不通的图片源选择不同的解码方法加载图片创建出Bitmap。这些方法中都会传入一个BitmapFactory.Options实例化对象,通过这个对象,可以更改一些加载图片的设置。由于这些解码方法用于解码加载图片,会占用内存构建Bitmap,因此很容易导致OOM的异常。

如果将options.inJustDecodeBounds设置为true,在解码过程中就不会申请内存去创建Bitmap,返回的是一个空的Bitmap,但是可以获取图片的一些属性,例如图片宽高,图片类型等等。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;      // 设置为true,不将图片解码到内存中
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;    // 图片高度
int imageWidth = options.outWidth;      // 图片宽度
String imageType = options.outMimeType; // 图片类型

一般来说,为了避免OOM的异常,在加载图片到内存之前,会先检查图片的尺寸,除非你能确保图片源不会导致OOM。

3.2 缩小图片的长宽来压缩图片

我们知道图片的大小之后,就可以决定是否将完整的图片加载到内存或者加载压缩版的图片到内存。可以基于以下几点做出决定:

  • 估计完整图片加载到内存中所使用内存
  • 可分配给加载图片的内存
  • 用于显示图片的控件的大小
  • 当前设备的屏幕大小和密度

例如,如果显示图片的控件大小为128x96像素,就没有必要将一个1024x768像素的图片加载到内存中。

设置options.inSampleSize的数值,来控制压缩图片程度。例如,将options.inSampleSize设置为4,将一个2048x1536像素的图片解码加载到内存后产生的Bitmap大约为512x384像素,如果使用的位图配置是ARGB_8888,那么仅仅需要0.75M就加载了缩小版的图片到内存,而加载完整的图片需要12M。

也就是说,如果我们设置inSampleSize == 2,解码出来的位图的宽高是原图的1/2,图片所占用内存缩小了1/4(1/2 x 1/2)。如果inSampleSize设置的值小于等1,都会当做inSampleSize == 1来解码加载图片。

于是我们可以在加载图片的时候,根据控件的大小(显示到屏幕上的大小)来计算出加压缩版图片的inSampleSize值。

    /**
     * 计算inSampleSize值
     *
     * @param options
     *          用于获取原图的长宽
     * @param reqWidth
     *          要求压缩后的图片宽度
     * @param reqHeight
     *          要求压缩后的图片长度
     * @return
     *          返回计算后的inSampleSize值
     */
    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // 原图片的宽高
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // 计算inSampleSize值
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }

有人可能会疑问为什么每次inSampleSize都是乘以2,指数增长。这是因为在加载图片过程中,解析器使用的inSampleSize都是2的指数倍,如果inSampleSize是其他值,则找一个离这个值最近的2的指数值。

上面已经获取了inSampleSize,然后就可以根据这个值来加载压缩版的图片了。

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {
    // 先将inJustDecodeBounds设置为true来获取图片的长宽属性
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // 计算inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // 加载压缩版图片
    options.inJustDecodeBounds = false;
    // 根据具体情况选择具体的解码方法
    return BitmapFactory.decodeResource(res, resId, options);
}

获取到了压缩版的Bitmap之后就可以直接设置到屏幕的控件上了。

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

4. 图片质量压缩

4.1 方法介绍

上面一种方法是通过缩放图片的大小来达到压缩效果,基本不会对图片的显示效果有影响。但是现在介绍的这一种方法,可能会导致图片质量下降。

使用的是下面这个方法来进行压缩。

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

这个方法有三个参数,是布尔类型的返回值

  • CompressFormat 指定的Bitmap被压缩成的图片格式,只支持JPEG,PNG,WEBP三种
  • quality 图片压缩质量的控制,范围为0~100,0表示压缩后体积最小,但是质量也是最差,100表示压缩后体积最大,但是质量也是最好的(个人认为相当于未压缩),有些格式,例如png,它是无损的,所以会忽略这个值。
  • OutputStream 压缩后的数据会写入这个字节流中
  • 返回值表示返回的字节流是否可以使用BitmapFactory.decodeStream()解码成Bitmap,至于返回值是怎么得到的,因为是Native的代码,没法找到逻辑。

4.2 色位深度介绍

接下来说说为什么用这个方法可能会导致图片质量下降。在Bitmap中有一个Config的属性,这个属性是用来描述每个像素被储存的大小。目前Config有四个值:ALPHA_8RGB_565ARGB_4444ARGB_8888。这个说明一下(我个人的理解,真心不好解释),每一个像素会可能由四个属性组成,R(Red红色通道)、G(Green绿色通道)、B(Blue蓝色通道)、A(Alpha透明度通道)。

Config 每个像素占用的字节 说明
ALPHA_8 1 bytes 每个像素仅仅储存透明度通道
RGB_565 2 bytes 每个像素的RGB通道会保存,透明度不会保存,红色通道5位,有2^5=32种表现形式;绿色通道6位,有2^6=64种表现形式;蓝色通道5位,有2^5=32种表现形式
ARGB_4444 2 bytes 每个像素的ARGB通道都会保存,透明度/红色/绿色/蓝色通道4位,有2^4=16种表现形式
ARGB_8888 4 bytes 每个像素的ARGB通道都会保存,透明度/红色/绿色/蓝色通道8位,有2^8=256种表现形式

有什么区别呢?最简单的,当一个颜色表现形式越多,那么画面整体的色彩就会更丰富,图片质量就会越高,当然,图片占用的储存空间也越大。

4.3 图片质量下降原因介绍

前面提到过调用Bitmap.compress()方法时候,会传入一个压缩后的图片格式,但是由于并不是所有的图片格式都支持上面说的Config的所有通道,比如说,JPEG格式的图片,是不支持Alpha(透明度)属性的,这样将压缩后返回的字节流通过BitmapFactory.decodeStream()转换成Bitmap的过程中,会将透明度属性给丢弃,导致图片质量下降。

4.4 压缩过程介绍

压缩过程如下,通过依次减少图片质量,将图片大小控制在限制值范围内。

/**
 * 压缩图片
 *
 * @param bitmap
 *          被压缩的图片
 * @param sizeLimit
 *          大小限制
 * @return
 *          压缩后的图片
 */
private Bitmap compressBitmap(Bitmap bitmap, long sizeLimit) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    int quality = 100;
    bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);

    // 循环判断压缩后图片是否超过限制大小
    while(baos.toByteArray().length / 1024 > sizeLimit) {
        // 清空baos
        baos.reset();
        bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
        quality -= 10;
    }

    Bitmap newBitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(baos.toByteArray()), null, null);

    return newBitmap;
}

5. 更近一步的优化

上面提到的很多压缩方法,如果是在UI线程执行的话,很有可能阻塞到主线程,这是在开发过程中非常不愿意见到的事情,所以我们需要在后台线程去执行这些压缩图片比较耗时的操作,然后获取到压缩后的图片,设置到屏幕中。使用AsyncTask可以帮助我们很好的实现。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // 使用弱引用
        imageViewReference = new WeakReference<ImageView>(imageView);
    }

    // 在后台线程压缩图片
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // 压缩完成后,将图片设置到控件中
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

最终的执行代码。

    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);

6. 总结

图片的处理,时刻都需要注意,因为机型配置的不同,以及现场设备内存使用的情况,都有可能导致OOM的现象,上述提到了压缩方法,基本适用与大部分图片压缩情况。当然如果对图片画质显示有要求,可能就需要特殊的处理了,这个就不在大部分场景的考虑内。

7. 高斯模糊的建议

我在项目中遇见的关于图片操作的OOM异常,有80%源自于高斯模糊。是的,有些产品经理为了和iOS保持一致,需要将某些页面背景设置成高斯模糊效果。

一般的做法是将上一个页面截图,然后做高斯模糊处理,设置成背景。正好我接触过这种需求,说一下自己对于高斯模糊的建议。

  • 确定产品经理的需求,高斯模糊的效果是不是一定要上。之前遇见一个需求,需要高斯模糊,结果Android做出来效果很不理想,后来,我把背景直接设置成60%的透明度白色。产品经理看了之后,觉得Android的高斯模糊效果(其实是透明度)比iOS的要好一些,就让iOS改。所以,一定要首先确认产品经理的需求,产品经理想要的效果可能并不是他口中说出的效果,就像我遇见的这位,可能误将透明度和高斯模糊混合了。
  • 如果高斯模糊效果一定要上。先将图片长宽缩小,然后压缩图片质量,再进行高斯模糊的渲染,最后将高斯模糊之后的效果图放大至控件大小,显示到屏幕。
    • 缩放图片长宽
    • 压缩图片质量
    • 高斯模糊渲染
    • 放大高斯模糊效果图

最后,希望Android工程师不要遇见高斯模糊的需求,因为,真的,很坑。但是如果遇见了,也不要怕,因为你已经知道该如何处理了。

时间: 2024-11-04 15:46:41

Android之图片压缩的相关文章

Android BitmapFactory图片压缩处理(大位图二次采样压缩处理)

Android实际开发中,在加载大量图片的时候,比如ViewPager.GridView.ListView中,加载了大量的比较大图片就容易出现OOM(内存溢出)的异常,这是因为一个应用的最大内存使用只有16M,超过了这个值,就会出现OOM.所以我们实际开发中,要想避免OOM出现就要对相应的图片进行压缩处理. 本文即使用了BitmapFactory和BitmapFactory.Option这两个类,对图片进行相应的尺寸压缩处理.经测试,成功解决了未压缩图片之前出现的OOM异常. 实现效果图: 本D

Android 中图片压缩分析(上)

作者: shawnzhao,QQ音乐技术团队一员 一.前言 在 Android 中进行图片压缩是非常常见的开发场景,主要的压缩方法有两种:其一是质量压缩,其二是下采样压缩. 前者是在不改变图片尺寸的情况下,改变图片的存储体积,而后者则是降低图像尺寸,达到相同目的. 由于本文的篇幅问题,分为上下两篇发布. 二.Android 质量压缩逻辑 在Android中,对图片进行质量压缩,通常我们的实现方式如下所示: ByteArrayOutputStream outputStream = new Byte

Android 之 图片压缩

在上一篇文章中(Android之图片变换)主要说明了bitmap的使用,当然其中也包括一点图片压缩的内容,但是没有详细描述,这篇文章就来阐述一下平时Android使用的图片压缩技术 从图片的压缩方式区分:质量压缩和尺寸压缩. 质量压缩是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,经过它压缩的图片文件大小会有改变,但是导入成bitmap后占得内存是不变的.因为要保持像素不变,所以它就无法无限压缩,到达一个值之后就不会继续变小了.显然这个方法并不适用与缩略图,其实也不适用于想通

Android中图片压缩方案详解

如感觉排版不舒服,可移步至此处查看 图片的展示可以说在我们任何一个应用中都避免不了,可是大量的图片就会出现很多的问题,比如加载大图片或者多图时的OOM问题,可以移步到Android高效加载大图.多图避免程序OOM.还有一个问题就是图片的上传下载问题,往往我们都喜欢图片既清楚又占的内存小,也就是尽可能少的耗费我们的流量,这就是我今天所要讲述的问题:图片的压缩方案的详解. 1.质量压缩法 设置bitmap options属性,降低图片的质量,像素不会减少 第一个参数为需要压缩的bitmap图片对象,

Android中图片压缩(质量压缩和尺寸压缩)

关于Android 图片压缩的学习: 自己总结分为质量压缩和像素压缩.质量压缩即:将Bitmap对象保存到对应路径下是所占用的内存减小,但是当你重新读取压缩后的file为Bitmap时,它所占用的内存并没有改变,它会改变其图像的位深和每个像素的透明度,也就是说JPEG格式压缩后,原来图片中透明的元素将消失,所以这种格式很可能造成失真.像素压缩:将Bitmap对象的像素点通过设置采样率,减少Bitmap的像素点,减小占用内存.两种压缩均可能对图片清晰度造成影响. (一) /** * * @desc

Android的图片压缩类ThumbnailUtils

从Android 2.2开始系统新增了一个缩略图ThumbnailUtils类,位于framework包下的android.media.ThumbnailUtils位置,可以帮助我们从mediaprovider中获取系统中的视频或图片文件的缩略图,该类提供了三种静态方法可以直接调用获取. 1.extractThumbnail (source, width, height): Java代码   /** * * 创建一个指定大小的缩略图 * @param source 源文件(Bitmap类型) *

(转)Android学习-使用Async-Http实现图片压缩并上传功能

(转)Android学习-使用Async-Http实现图片压缩并上传功能 文章转载自:作者:RyaneLee链接:http://www.jianshu.com/p/940fc7ba39e1 让我头疼一个星期的图片批量上传服务器的问题最后受这篇文章的作者启发而解决,自己之前一直执着于通过uri地址找到图片然后上传图片,却没想过直接上传图片本身.感谢作者的博客和启发. 前言 (转载请注明出处,谢谢!) 最近在做一个小项目,项目中要实现上传图片到服务器,而这个例子是实现图片的尺寸压缩,将获取到的压缩图

Android中的图片压缩

1.android中计算图片占用堆内存的kB大小跟图片本身的kB大小无关,而是根据图片的尺寸来计算的. 比如一张 480*320大小的图片占用的堆内存大小为: 480*320*4/1024=600kB  之所以要乘以4,是因为在android中使用的ARGB图片,图片一个像素占用四个字节. 2.手机出厂时 堆内存(Heap)是固定的,所以为了不造成OOM,我们就需要生成bitmap时对图片进行压缩处理. 实际使用中我们压缩图片的标准是手机屏幕大小作为参照的,这个主要是因为,即便是图片尺寸跟屏幕尺

android图片压缩的3种方法实例

android 图片压缩方法: 第一:质量压缩法: private Bitmap compressImage(Bitmap image) { ByteArrayOutputStream baos = new ByteArrayOutputStream();        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中        int options = 100