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

学Android

由来


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

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

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

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 异常,也就是咱们所说的 内存溢出 ,其通常表现为应用闪退等现象。那么咱们该如何下手去解决呢?git

解决方案


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

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

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

那么就让咱们开始吧:编程

获取原图大小


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

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 参数的方法,如app

  • 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 的动画框架,我跪搓衣板

Android 自定义时钟控件 时针、分针、秒针的绘制这一篇就够了

android 自定义控件之-绘制钟表盘

Android 进阶自定义 ViewGroup 自定义布局

Android 逐帧动画( Drawable 动画),这一篇就够了

欢迎关注_yuanhao的掘金!


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

源码 Demo 连接:Drop 我第一次写的 Android 项目,但愿你们点歌 star~ 谢谢!

请点赞!由于你的鼓励是我写做的最大动力!

学Android
相关文章
相关标签/搜索