每个人都要学的图片压缩终极奥义,有效解决 Android 程序 OOM
由来
在我们编写
Android
程序的时候,几乎永远逃避不了图片压缩的难题。除了应用图标之外,我们所要显示的图片基本上只有两个来源:
- 来自网络下载
- 本地相册中加载
不管是网上下载下来的也好,还是从系统图片库中读取的图片,都有一个相同的特点:像素一帮较高。同时我们都知道,Android
系统分配给我们每个应用的内存是有限的,由于解析、加载一张图片,需要占用的内存大小,是远大于图片自身大小的。所以,这时程序就可能因为占用了过多的内存,从而出现OOM
现象。那么什么是 OOM
呢?
OOM
即 OutOfMemory
异常,也就是我们所说的 内存溢出 ,其一般表现为应用闪退等现象。那么我们该如何下手去解决呢?
解决方案
首先我们发现,我们所加载的这些图片的分辨率,要比我们手机屏幕高得多,更有甚者,我们在一个拇指大的控件上,去加载一个 4k 大图是完全没有必要的,也就是说,如果我们能让每个控件上都去显示相应大小的图片,那么这个问题也就迎刃而解了
那么,要怎样才能达到图片与控件的对号入座?这时我们就引进了图片压缩的方案:
- 首先,获得原图片大小
- 其次,获取控件大小
- 接着,获取我们图片和控件的比例
- 最后,根据这一比例,将图片压缩为适合显示的大小
那么就让我们开始吧:
获取原图大小
我们都知道,Android 向我们提供了 BitmapFactory
这个类,在这个类中有着诸如:decodeResource()
decodeFile()
decodeStream()
等。其中:
- 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
。这种做法有两个好处:
- 既能获得图片大小,由于后续操作
- 又成功避免了去解析图片,导致程序
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
赋值为较大的一个。为什么不用小的那一个呢?这里我就卖个关子,大家可以在评论区中发表自己的想法
生成目标图片
经过前面的两个步骤,想必大家已经能勾勒处这最后一步的做法了,思路非常简单:
- 先生成一个
Options
对象 - 将
Options 的 inJustDecodeBounds
设置为true
- 接着调用方法一
calculateOptionsById
获得原图尺寸到Options
中 - 调用方法三
calculateInSamplesizeByOptions
获得相应的inSampleSize
对象 - 将
Options
的inJustDecodeBounds
改回false
- 再次调用
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
前可能忘记,设置inJustDecodeBound
为true
,进而导致计算超大图时,直接发生OOM
- 调用完
calculateInSamplesizeByOptions
后可能忘记,设置inJustDecodeBounds
为false
,进而导致无法获得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
进行赋值呢?这主要是防止,有时我们可能只想得到计算相应比例来做其他操作,而不想改变原有属性,所以是否赋值,就交给用户去选择吧
总结
好了,到这里为止,历时有关图片压缩的所有坑坑洼洼都已经总结好了,我们从头理以边思路:
- 借助
options.inJustDecodeBounds
参数赋值true
时,不生成图片的特性,将原图尺寸保存在Options
中 - 通过
options
中原图尺寸与目标(控件)尺寸的比例,对options.inSampleSize
进行设置 - 生成目标图片
- 压缩的问题解决了,但是每次打开图片都压缩也太麻烦了!下面我将针对这个问题进行更有效地解决 ,有兴趣可以继续关注 _yuanhao 的编程世界
相关文章
Android 让你的 Room 搭上 RxJava 的顺风车 从重复的代码中解脱出来
ViewModel 和 ViewModelProvider.Factory:ViewModel 的创建者
单例模式-全局可用的 context 对象,这一篇就够了
缩放手势 ScaleGestureDetector 源码解析,这一篇就够了
看完这篇还不会 GestureDetector 手势检测,我跪搓衣板!
Android 属性动画框架 ObjectAnimator、ValueAnimator ,这一篇就够了
Android 逐帧动画( Drawable 动画),这一篇就够了
看完这篇再不会 View 的动画框架,我跪搓衣板
Android 自定义时钟控件 时针、分针、秒针的绘制这一篇就够了
android 自定义控件之-绘制钟表盘
请点赞!因为你的鼓励是我写作的最大动力!
源码 Demo 链接:Drop 我第一次写的 Android 项目,希望大家点歌 star~ 谢谢!
每个人都要学的图片压缩终极奥义,有效解决 Android 程序 OOM相关推荐
- 我在学python-我慌了,周围人都在学python...
闲来无事,跟朋友聊聊天,问他最近在干什么?他说,他最近在学python,说自己周围学PHP.java.web的同学都在学在了解,所以他也跟着了解下这门语言;问身边一个做网络营销的伙伴他最近在干嘛,他也 ...
- python语言有什么用-Python到底有什么用?为什么那么多人都在学Python?
原标题:Python到底有什么用?为什么那么多人都在学Python? 现如今无论是工作汇报.产品设计.后台设计甚至是数据大屏,越来越多的行业都离不开与数据打交道! Excel作为数据必备工具,一直以来 ...
- AI入门,从每个人都应该学的AI第一课开始
目前,人工智能是一个快速比较快速的行业,对人才的需求很大,而且和其他新兴行业比较,竞争性小,发展空间大,因此想必觉得各位小伙伴们可以去从事这个行业,现在是进入人工智能领域的好时机. 每个人都应该学的A ...
- 【答疑】软件测试是不是很简单,什么人都可以学?
昨天晚上在知乎上看到一个网友问题,我做了一个详细的回答,收到了许多测试人的喜欢与点赞,我把我的回答贴出来分享一下. 既然问题问的这么官方,那我来做一个科普?后面再来解答你的问题. 软件测试(Softw ...
- JavaScript权威Douglas Crockford:代码阅读和每个人都该学的编程
作者:Peter Seibel 关于JavaScript Seibel:在程序学习之路上有哪些令你后悔的事情? Crockford:我了解一些语言,但却一直没有机会使用.我花了不少时间学习APL并了解 ...
- 乔布斯、比尔·盖茨、周冬雨、李晨...说:每个人都应该学编程。
20年前,英语进入中小学课堂,成为人人必学的语言.这一次,轮到了编程语言. 为什么要学编程?不仅仅因为编程已经进入江苏.重庆等小学课堂,进入美国.英国所有小学课堂,因为未来的世界,是程序和机器的世界, ...
- 最优秀的人都在学数学,这就是法国数学如此强大的原因!
一.引子 在德国数学家高斯的一部传记中,作者引用了下面这段话: 有一个异乡人在巴黎问当地人,"为什么贵国历史上出了那么多伟大的数学家?" 巴黎人回答,"我们最优秀的人学习 ...
- 每个人都想学的Java
:::::::::::::::::::::::::::::::::::::::::::::::::::::::: Java 入门基础知识 ::::::::::::::::::::::::::::::: ...
- android图片压缩终极解决方案
如题,多种压缩方式常用的有尺寸压缩.质量压缩以及通过JNI调用libjpeg库来进行压缩,三种方式结合使用实现指定图片内存大小,清晰度达到最优,下面就先分别介绍下这几种压缩方式. 原文出处:http: ...
最新文章
- 完美解答35K月薪的MySQL面试题(四)MySQL是如何加行锁的?
- 什么是重构,什么不是重构
- 国家粮食与物资储备局揭示中国稻谷产毒真菌分布及仓储动态变化
- docker mysql 日志_在docker mysql容器中启用日志记录
- 野生前端的数据结构基础练习(5)——散列
- 北大新成果!首次成功地将CNN解码器用于代码生成 | 论文+代码
- 8 -- 深入使用Spring -- 2... Spring的“零配置”支持
- 有关js获取屏幕宽度问题
- Google离去,百度就能制衡?
- 记事本html特效,很漂亮的网页飘落特效代码
- keras中的K.gradients()函数
- php如何截取出视频中的指定帧作为图片
- 高考首日,为梦想加油!
- 【JavaWeb】之富文本编辑器
- 向上累积频数怎么算_累计频数怎么求(怎样计算频数和频率)
- ASP.NET WEBAPI 跨域请求 405错误
- ECMAScript 2016(ES7) 的新特性总结
- Docker 容器化技术(介绍)
- JavaScript 教程「3」:数据类型
- 尼康D780相机黑屏的故障原因