转载注明出处:http://blog.csdn.net/xiaohanluo/article/details/52485037

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);// 计算inSampleSizeoptions.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) {// 清空baosbaos.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);}// 在后台线程压缩图片@Overrideprotected Bitmap doInBackground(Integer... params) {data = params[0];return decodeSampledBitmapFromResource(getResources(), data, 100, 100));}// 压缩完成后,将图片设置到控件中@Overrideprotected 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工程师不要遇见高斯模糊的需求,因为,真的,很坑。但是如果遇见了,也不要怕,因为你已经知道该如何处理了。

经@Trent1985提醒,高斯模糊在C/C++层面处理不会有性能问题,而且效果比较好,本人没有用动态库实现过高斯模糊,大家有兴趣的可以尝试一下。

Android之图片压缩相关推荐

  1. Android LibJpeg图片压缩

    Android的图片压缩 Android的图片压缩的几种方式:质量压缩,尺寸压缩,采样率压缩,通过NDK调用libjpeg库进行压缩! 质量压缩 通过设置bitmap options属性,降低图片的质 ...

  2. Android的图片压缩并上传

    Android开发中上传图片很常见,一般为了节省流量会进行压缩的操作,本篇记录一下压缩和上传的方法. 图片压缩的方法 : import java.io.ByteArrayOutputStream; i ...

  3. Android中图片压缩分析(上)

    此文章首发:https://mp.weixin.qq.com/s/QZ-XTsO7WnNvpnbr3DWQmg 一.前言 在 Android 中进行图片压缩是非常常见的开发场景,主要的压缩方法有两种: ...

  4. Android中图片压缩方案详解

    如感觉排版不舒服,可移步至此处查看 图片的展示可以说在我们任何一个应用中都避免不了,可是大量的图片就会出现很多的问题,比如加载大图片或者多图时的OOM问题,可以移步到Android高效加载大图.多图避 ...

  5. Android关于图片压缩

    关于图片压缩 [Android图片压缩(质量压缩和尺寸压缩)&Bitmap转成字符串上传] http://blog.csdn.net/jdsjlzx/article/details/44228 ...

  6. Android BitMap图片压缩

    最近在网上看了一些关于图片压缩的博客,自己也动手实验了一遍,也算事对图片压缩有了一个了解,打算写个博客记录一下.文末附上参考链接. Android中涉及到图片的话一般都会用到BitMap类和Bitma ...

  7. Android图片系列-2.Android App图片压缩、裁剪分析整理

    移动端常用的图片格式有PNG和JPEG,目前ios手机和大部分安卓手机拍照生成的图片默认格式都是JPEG.我们开发APP的时候通常使用的是PNG,这可能是考虑到图片质量效果.PNG图片是无损压缩格式, ...

  8. Android实现图片压缩并上传到服务器

    最近公司又叫开发了一个新项目,这个项目中上传图片用的蛮多的,于是整理一下,记录自己的心得体验 刚入手的时候,对于图片的大小还没有概念,(以前上传图片都是用户头像,对大小没什么要求),心想之间上传就是了 ...

  9. Android 高效图片压缩

    使用libjpeg-turbo进行图片压缩 1. JEPG 是什么? 相信有一部分使用 iPhone 手机用微信发送图片的时候,明明图片大小只有 1M ,但清晰度比 Android 手机 5 M 图片 ...

最新文章

  1. valgrind——Callgrind检测程序代码的运行时间和调用过程,程序分析性能。
  2. 【GStreamer】gstreamer工具详解之:gst-discoverer-1.0
  3. 深入聊一聊 Spring AOP 实现机制
  4. 【廖雪峰python进阶笔记】函数式编程
  5. 信标节能电路模块第二版本调试-无线充电-2021-3-21
  6. pgpool-II中间件
  7. 苹果电脑删除软件_易我Mac数据恢复软件,解决苹果电脑T2芯片数据恢复难题!
  8. jsp九大内置对象和四种属性范围介绍
  9. 使用独立PID namespace防止误杀进程
  10. NOIP模拟题——LGTB与序列
  11. 管理与决策这属于计算机在什么方面的应用,闽高校计算机一级考试选择题题库...
  12. Mac版本Octane渲染器安装教程支持M1和英特尔全系列分享
  13. python实现爱心代码
  14. 基于Python+Opencv的银行卡号识别系统(附完整代码)
  15. 计算机一直进入安全模式开机,电脑启动时自动进入安全模式怎么办
  16. 前端职业规划-写给年轻的前端韭菜们
  17. 【Reactome 下载所有通路基因集】
  18. 测绘——如何在win10环境下安装CAD2006+CASS7.0
  19. Web 利用纯html和css画出一个android机器人
  20. Elixir元编程基础知识

热门文章

  1. RCF—用于C++的进程间通讯(1)
  2. SDN相关组织——ONF
  3. 用计算机算出用不用减肥,体脂率计算器有什么用
  4. Stashed changes conflicted with hook auto-fixes...
  5. 论文翻译—Ciphertext-Policy Attribute-Based Encryption
  6. 进行最大公约数和最小公倍数的求解
  7. iphone手机尺寸汇总
  8. 使用BitLocker实现磁盘加密、u盘加密、移动硬盘加密
  9. 五子棋(含较高级的人机对战)
  10. Go 语言编程 — 高级数据类型 — Interface、多态、Duck Typing 与泛式编程