每个人都要懂的图片压缩,有效解决 Android 程序 OOM

# 由来

在我们编写 Android 程序的时候,几乎永远逃避不了图片压缩的难题。除了应用图标之外,我们所要显示的图片基本上只有两个来源:

  • 来自网络下载
  • 本地相册中加载

不管是网上下载下来的也好,还是从系统图片库中读取的图片,都有一个相同的特点:像素一帮较高。同时我们都知道,Android 系统分配给我们每个应用的内存是有限的,由于解析、加载一张图片,需要占用的内存大小,是远大于图片自身大小的。所以,这时程序就可能因为占用了过多的内存,从而出现OOM 现象。那么什么是 OOM 呢?

Exception java.lang.OutOfMemoryError: Failed to allocate a 916 byte allocation with 8388608 free bytes and 369MB until OOM; failed due to fragmentation (required continguous free 65536 bytes for a new buffer where largest contiguous free 32768 bytes)
java.nio.CharBuffer.allocate (CharBuffer.java:54)
java.nio.charset.CharsetDecoder.allocateMore (CharsetDecoder.java:226)
java.nio.charset.CharsetDecoder.decode (CharsetDecoder.java:188)
org.java_websocket.util.Charsetfunctions.stringUtf8 (Charsetfunctions.java:77)
org.java_websocket.WebSocketImpl.decodeFrames (WebSocketImpl.java:375)
org.java_websocket.WebSocketImpl.decode (WebSocketImpl.java:158)
org.java_websocket.client.WebSocketClient.run (WebSocketClient.java:185)
java.lang.Thread.run (Thread.java:818)

OOMOutOfMemory 异常,也就是我们所说的 内存溢出 ,其一般表现为应用闪退等现象。那么我们该如何下手去解决呢?

# 解决方案

首先我们发现,我们所加载的这些图片的分辨率,要比我们手机屏幕高得多,更有甚者,我们在一个拇指大的控件上,去加载一个 4k 大图是完全没有必要的,也就是说,如果我们能让每个控件上都去显示相应大小的图片,那么这个问题也就迎刃而解了

那么,要怎样才能达到图片与控件的对号入座?这时我们就引进了图片压缩的方案:

  • 首先,获得原图片大小
  • 其次,获取控件大小
  • 接着,获取我们图片和控件的比例
  • 最后,根据这一比例,将图片压缩为适合显示的大小

那么就让我们开始吧:

# 获取原图大小

我们都知道,Android 向我们提供了 BitmapFactory 这个类,在这个类中有着诸如:decodeResource() decodeFile() decodeStream() 等:

public static Bitmap decodeResource(Resources res, int id)

public static Bitmap decodeFile(String pathName)

public static Bitmap decodeStream(InputStream is)

其中:

  • decodeResource() : 用于解析资源文件,即 res 文件夹下的图片
  • decodeFile() : 用于解析系统相册中的图片
  • decodeStream() : 用于解析输入输出流中图片通常,是采用 HttpClient 从下载的图片

其他的方法这里就不多说了,因为在源码中我们可有i看到,几乎所有的方法,最后都会将图片解析为流的形式,最后调用 decodeStream() 方法,实例化出我们的 Bitmap 对象。

虽然这些方法对我们是再熟悉不过的了,但对于某些初学者而言,却经常忽略了一个重要的内部类 :BitmapFactory.Options ,然而他确实我们图片压缩必不可少的,为什么需要这个参数呢?Options 的对象用于确定需要生成的 Bitmap 即目标图片的参数。
他的用法很简单,我们先 new 一个 BitmapFactory.Options 对象。再去调用含有 Options 参数的方法,如

  • public static Bitmap decodeResource(Resources res, int id, Options opts)
  • public static Bitmap decodeResourceStream(@Nullable Resources res,@Nullable TypedValue value,@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts)

调用完之后我们发现,除了方法放回给我们一个实例化出来的 Bitmap 图片之外,这个 Options 对象中长度、宽度、类型等等属性,也都被设置成了了我们图片的相应属性。所以,我们很容易想到:通过将 Options 对象传入,来获得图片的原始尺寸,为后期的压缩做准备,说干就干,我们将 Options 对象,和 Resources中一张 4k 图片的id 一块传入上诉方法中,来尝试获得它的尺寸,结果我们发现:程序 OOM 崩溃了!

为什么会发生这种情况?首先我们想想我们为什么要获得这个Options 对象?时为了获得图片的尺寸大小;那我们为什么要获得原图尺寸大小?是为了按照原图尺寸和控件尺寸的比例,将其压缩为适合显示的大小?那我们又为什么要去压缩它为合适的大小呢?是因为如果按照原大小去调用相应的 decode...()方法解析图片,会导致内存占有率过高触发OOM 异常,进而导致程序崩溃啊!没想到的是:结果我们为了获得 Options 而调用了相应的 decode...() 方法,的确 Options 是复制了,但由于该方法适用于生成图片,也就是 Bitmap 对象的。所以程序也在解析这张超大图的过程中OOM 崩溃了

那么难道就没方法了吗?

有的,我之前说过:Option 内部有着众多参数,其中有一个叫做: inJustDecodeBounds 。这个参数默认值为false 。但如果我们先把这个参数设置为 true 时,该方法便不在会去生成相应的 Bitmap ,而仅仅是去测量图片的各种属性,如长度、宽度、类型等等,然后放回一个 null 。所以,我们很容易想到:可以先通过将 inJustDecodeBounds 的值设为 true ,再去调用相应的相应的 decode...()方法,最后再将inJustDecodeBounds 的值改回 false 。这种做法有两个好处:

  1. 既能获得图片大小,由于后续操作
  2. 又成功避免了去解析图片,导致程序 OOM 而崩溃。

但这恰恰是被很多人所忽略的一点。

好了,现在给出具体的实现:

    public static void calculateOptionsById(@NonNull Resources res,@NonNull BitmapFactory.Options options, int imgId) {
        BitmapFactory.decodeResource(res, imgId, options);
    }

大家可能发现,这里只将 inJustDecodeBounds 设为true却没有改回false ,这是因为获得 Options 只是图片压缩的第一步,我们在后续方法中将会进行修改

# 如何进行压缩

我们继续看 Options 的构成。我们发现,其中有个名为 inSampleSize 的数据成员,他就是关键所在,那么他有着什么意义呢?

这里我给大家举个例子,比如我这有张 4000*1000 像素的图片:

  • 当我们把 inSampleSize 的值设为 4时,最后生成出来的图片大小将会是:1000 x 250 像素
  • 当我们把inSampleSize 的值设为5时,最后生成出来的图片大小将会是:800 x 200 像素。这是个什么概念?

这不仅仅是长宽都变为原来四分之一或者五分之一这么简单,而是其图片大小,直接变为原图的 1/(n^2)!也就是说:

  • 如果原图 2MB,那么当 inSampleSize 赋值为4加载时就只需要 0.125MB
  • 那 如果 inSampleSize 赋值为 5 呢?只需要 0.08 MB!连100k 都不到的小图啊!

那么下面我就给出这个方法的具体实现:

    public static int calculateInSamplesizeByOptions(@NonNull BitmapFactory.Options options, int reqWidth, int reqHeight) {
        int inSamplesize   = 1;
        int originalWidth  = options.outWidth;
        int originalHeight = options.outHeight;
        if (originalHeight > reqHeight || originalWidth > reqWidth) {
            int heightRatio = originalHeight / reqHeight;
            int widthRatio  = originalWidth  / reqWidth;
            inSamplesize = heightRatio > widthRatio ? heightRatio : widthRatio;
        }
        return inSamplesize;
    }

我们发现,这里我先计算出了,原图尺寸与目标大小大比例,在三目运算符中,将inSamplesize 赋值为较大的一个。为什么不用小的那一个呢?这里我就卖个关子,大家可以在评论区中发表自己的想法

# 生成目标图片

经过前面的两个步骤,想必大家已经能勾勒处这最后一步的做法了,思路非常简单:

  1. 先生成一个 Options对象
  2. Options 的 inJustDecodeBounds设置为true
  3. 接着调用方法一calculateOptionsById获得原图尺寸到Options
  4. 调用方法三calculateInSamplesizeByOptions 获得相应的inSampleSize 对象
  5. OptionsinJustDecodeBounds改回 false
  6. 再次调用 decode...()方法(这里是 decodeResource )获得压缩后的 Bitmap对象

具体实现如下

    public static Bitmap decodeBitmapById (@NonNull Resources res, int resId, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        calculateOptionsById(res, options, resId);
        options.inSampleSize = calculateInSamplesizeByOptions(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeResource(res, resId, options);
        return bitmap;
    }

非常棒,我们赶紧看看效果:

太棒了,几乎和原图效果一摸一样,但软件运行的流畅性确大大提高了!但是,这真的就完美了吗?

最求完美的我们可能会有个想法:如果调用我们方法的人,或者说特殊时候的我们。不想用这个已经写好的 decodeBitmapById方法,而是像自己通过前两个方法:calculateOptionsById calculateInSamplesizeByOptions 来实现图片压缩功能,这是问题就出现了:

  • 调用 calculateOptionsById 前可能忘记,设置 inJustDecodeBoundtrue ,进而导致计算超大图时,直接发生 OOM
  • 调用完 calculateInSamplesizeByOptions 后可能忘记,设置inJustDecodeBoundsfalse,进而导致无法获得Bitmap 对象,一脸懵逼
  • 啥都做了结果调用完 calculateInSamplesizeByOptions 没把没回的值赋给 options.inSampleSize ,白忙活一场

所以,我们需要在优化一下:

首先,在calculateOptionsById中,默认将 options.inJustDecodeBounds 设置为true

    public static void calculateOptionsById(@NonNull Resources res,@NonNull BitmapFactory.Options options, int imgId) {
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, imgId, options);
    }

其次,在 calculateInSamplesizeByOptions最后,默认将 options.inJustDecodeBounds设置为false

    public static int calculateInSamplesizeByOptions(@NonNull BitmapFactory.Options options, int reqWidth, int reqHeight) {
        int inSamplesize   = 1;
        int originalWidth  = options.outWidth;
        int originalHeight = options.outHeight;
        if (originalHeight > reqHeight || originalWidth > reqWidth) {
            int heightRatio = originalHeight / reqHeight;
            int widthRatio  = originalWidth  / reqWidth;
            inSamplesize = heightRatio > widthRatio ? heightRatio : widthRatio;
        }
        options.inJustDecodeBounds = false;
        return inSamplesize;
    }

为什么不在该方法后面,对 options.inSampleSize进行赋值呢?这主要是防止,有时我们可能只想得到计算相应比例来做其他操作,而不想改变原有属性,所以是否赋值,就交给用户去选择吧

# 总结

好了,到这里为止,历时有关图片压缩的所有坑坑洼洼都已经总结好了,我们从头理以边思路:

  1. 借助options.inJustDecodeBounds 参数赋值true时,不生成图片的特性,将原图尺寸保存在 Options
  2. 通过 options 中原图尺寸与目标(控件)尺寸的比例,对 options.inSampleSize 进行设置
  3. 生成目标图片
  4. 压缩的问题解决了,但是每次打开图片都压缩也太麻烦了!下面我将针对这个问题进行更有效地解决 ,有兴趣可以继续关注 _yuanhao 的编程世界

相关文章


Android 让你的 Room 搭上 RxJava 的顺风车 从重复的代码中解脱出来
ViewModel 和 ViewModelProvider.Factory:ViewModel 的创建者
单例模式-全局可用的 context 对象,这一篇就够了
缩放手势 ScaleGestureDetector 源码解析,这一篇就够了
Android 属性动画框架 ObjectAnimator、ValueAnimator ,这一篇就够了
看完这篇再不会 View 的动画框架,我跪搓衣板
看完这篇还不会 GestureDetector 手势检测,我跪搓衣板!
android 自定义控件之-绘制钟表盘
Android 进阶自定义 ViewGroup 自定义布局
看完这篇还不会自定义 View ,我跪搓衣板

欢迎关注_yuanhao的博客园!



定期分享Android开发湿货,追求文章幽默与深度的完美统一。

源码 Demo 链接:Drop 我第一次写的 Android 项目,希望大家点歌 star~ 谢谢!

请点赞!因为你的鼓励是我写作的最大动力!

原文地址:https://www.cnblogs.com/yuanhao-1999/p/11615941.html

时间: 2024-10-06 02:31:31

每个人都要懂的图片压缩,有效解决 Android 程序 OOM的相关文章

每个人都应该懂点函数式编程

目录 一个问题 函数式编程中的函数 数学与函数式编程 混合式编程风格 一个问题 假设现在我们需要开发一个绘制数学函数平面图像(一元)的工具库,可以提供绘制各种函数图形的功能,比如直线f(x)=ax+b.抛物线f(x)=ax2+bx+c或者三角函数f(x)=asinx+b等等.那么怎么设计公开接口呢?由于每种行数的系数(a.b.c等)不同,并且函数构造也不同.正常情况下我们很难提供一个统一的接口.所以会出现类似下面这样的公开方法: //绘制直线函数图像public void DrawLine(do

每个人都该懂点网络安全

一. SQL注入(SQL Injection) 风险 数据库资料被窃取,被删除, 如果没做备份的话, 可能公司就废了 原理 所谓SQL注入,就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,提交一段数据库查询代码,操纵执行后端的 DB 查询,从而绕过认证机制,获得本不为用户所知数据的技术,甚至删除服务器的数据. 示例 statement = "SELECT * FROM Users WHERE id= " + variable + " ; 上面这条语句

为什么说编程和英语是每个人都必须掌握的技能?

为什么说编程和英语是每个人都必须掌握的技能? 我们今天的主题要从奥巴马的一段演讲开始说起. 这段视频是奥巴马去年应邀为美国在线编程教育网站code.org举办的“编程一小时”活动做的宣传片.为了呼吁美帝人民学习编程知识,奥巴马甚至还亲自挽起袖子写了一段javascript代码,这也让他成为美国历史上第一位会写代码的总统.(虽然写得不怎么样) 在code.org 随后推出的一段<编程,学校不会交给你的课程>的广告中,包括 Bill Gates,Mark Zuckberg等各界名人也纷纷献身说法,

Android LruCache 压缩图片 有效避免程序OOM

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9316683 本篇文章主要内容来自于Android Doc,我翻译之后又做了些加工,英文好的朋友也可以直接去读原文. http://developer.android.com/training/displaying-bitmaps/index.html 压缩加载大图片 我们在编写Android程序的时候经常要用到许多图片,不同图片总是会有不同的形状.不同的大小,但在大多数情况下,这

约瑟夫问题 算法很简单保证每隔人都能看懂用数组实现 利用循环删除数组中的元素

#include<iostream> using namespace std; const int size = 1000; void ArrDel() { int arr[size]; //循环结束标志,一直循环到数组中只剩下最后一个元素结束 int currentNum = size; int count = 0; for (int k = 0; k < size; k++) { arr[k] = k; } //currentNum==1表示数组中只剩下最后一个元素 是循环结束的标志

针对明棋类游戏的策梅洛定理的每个人都能读懂的证明过程(下象棋、围棋、国际象棋等的必胜下法求解)

网上流传的博弈论策梅洛定理的中文证明资料,其证明都不是那么容易理解读懂.因此这里描述一下我的通俗一点的证明. 策梅洛定理:在有限步数内一定会结束的已经明确规则的明棋类游戏中(例如中国象棋.国际象棋),对于特定的任一局面,设A先下,B后下,那么要么A有必胜下法,要么B有必胜下法,要么A.B都有必和下法. 证明用的是归纳法. 把相邻的A走一步棋.B走一步棋的下法组合作为本证明中的一回合走法.证明对于有限n回合的棋类游戏局面(正规开局也算是一种局面),要么A有必胜下法(1),要么B有必胜下法(2),要

图片压缩(pc端和移动端都适用)

最近在做移动端遇到了一个问题就是: 手机拍照后,图片过大如果上传到服务器务必会浪费带宽,最重要的是流量啊 别慌,好事儿来了,务必就会有人去研究研究图片的压缩: 鄙人结合前人的经验,结合自己实战,总结出一个方法供大家参考. /** * 图片压缩,默认同比例压缩 * @param {Object} path * pc端传入的路径可以为相对路径,但是在移动端上必须传入的路径是照相图片储存的绝对路径 * @param {Object} obj * obj 对象 有 width, height, qual

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

在网上调查了图片压缩的方法并实装后,大致上可以认为有两类压缩:质量压缩(不改变图片的尺寸)和尺寸压缩(相当于是像素上的压缩):质量压缩一般可用于上传大图前的处理,这样就可以节省一定的流量,毕竟现在的手机拍照都能达到3M左右了,尺寸压缩一般可用于生成缩略图.两种方法都实装在了我的项目中,结果却发现在质量压缩的模块中,本来1.9M的图片压缩后反而变成3M多了,很是奇怪,再做了进一步调查终于知道原因了.下面这个博客说的比较清晰: android图片压缩总结 总结来看,图片有三种存在形式:硬盘上时是fi

android图片压缩总结

首先该文章是总结, 不是原创, 是通过看网上其他大神的文章和自己的一些实践总结出来的. 一.图片的存在形式 1.文件形式(即以二进制形式存在于硬盘上)2.流的形式(即以二进制形式存在于内存中)3.Bitmap形式 这三种形式的区别: 文件形式和流的形式对图片体积大小并没有影响,也就是说,如果你手机SD卡上的如果是100K,那么通过流的形式读到内存中,也一定是占100K的内存,注意是流的形式,不是Bitmap的形式,当图片以Bitmap的形式存在时,其占用的内存会瞬间变大, 我试过500K文件形式