不同Android版本,对一张图片的内存处理方式是不一样的,使用不正确会导致OOM的发生,这篇文章带你梳理内存占用情况,选择适合你的图片加载模式,解决OOM问题。
你知道吗
在正式分析下面的内容前,先来看几个概念。
指屏幕的对角线的长度,单位是英寸,1英寸=2.54厘米。这个值是利用手机屏幕的长和宽,然后利用勾股定理,就可以算出斜边的长了。
即每英寸屏幕所拥有的像素数,英文简称ppi, 屏幕像素密度与屏幕尺寸和屏幕分辨率有关,屏幕密度越低在给定物理区域的像素就会较少。Android 将所有屏幕密度分为六组通用密度:ldpi( 低)、mdpi(中)、hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和xxxhdpi(超超超高)。
屏幕分辨率是指在横纵向上的像素点数,单位是px,1px=1个像素点,比如我们经常说的宽高像素为:4896*6528。
上面三个概念模糊吗?我们可以看一下下面这两张图,就可以理清上面三个概念了:
(图:分辨率计算公式)下面的分析,重要了解的是屏幕像素密度。
更多教程请访问码农之家
屏幕物理区域中的像素量,通常称为 dpi(每英寸点数)。屏幕密度越低在给定物理区域的像素就会较少。Android 将所有屏幕密度分为六组通用密度:ldpi( 低)、mdpi(中)、hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和xxxhdpi(超超超高)。
六种通用密度之间遵循 3:4:6:8:12:16 的缩放比率。
代码很简单,就是用一个ImageView包含一张背景图片,然后通过转换为Bitmap查看占用内存大小。
布局文件activity_main.xml
布局文件,就是一个ImageView控件,包含一张背景图。
MainAcivity.java
Android有一个特殊的文件夹res/drawable-nodpi/,放在里面的资源,不会被放大或者压缩,按照原大小展示,我们这里也把测试资源放在这个文件夹。
仍然以宽高像素为:4896*6528=31961088的图片举例,图片原始大小为5.48M,图片资源放在res/drawable-nodpi/下面,这时候找一个vivo X21手机,加载这张图片,占据内存情况为127844352byte:
而图片的原始图片像素总数为31961088,跟内存大小127844352byte好像没什么关系,但是真相是31961088* 4 = 127844352(Byte),原始图片尺寸大小与最终的内存占用大小呈倍数的关系,所以在这里与内存占用大小有直接关系的就是原始图片尺寸大小(例如:480x800),道理我都懂,但是倍数关系是从哪里来的呢,这就要谈论到Bitmap的像素格式了。
Android系统支持4种格式的像素格式,源码在Bitmap.Config中:
为了保证图片质量,官方默认使用ARGB_8888格式,导致图片的每个像素会占用4个Byte大小,所以demo里面的图片占用内存大小就是像素总数*像素格式,就是384000 * 4 = 1536000(Byte),这个时候应该有点成就感了,可以帮助你解决一部分实际项目问题了。
那么问题又来了,放在res/drawable-nodpi/文件夹下没问题,放在其他文件夹下呢?因为我们要适配不同的机器。
仍然以vivo X21举例,x21的目标图片文件夹是res/drawable-xxdpi/,屏幕密度480dpi。
看一下这个图片放在不同的文件夹下面,内存占用情况,单位:M。
可以看到,
或许你有这个疑问: 为什么在不同的文件夹下面,图片占据的内存资源基本一致,有的时候却发现不同文件夹下面,内存占据又是不一样的?
在回答这个问题前,你要搞清楚,google在图片加载时候,不同的Android版本,做了native堆栈和Java堆栈的区分。
这里也有个有意思的现象,在Android4.4到Android 8.0以下的机器,当你把这个图片放在不同的文件夹下面时,图片占据的内存是不一样的,那是因为图片内存的加载,是在Java 堆栈,所以你可能会遇到 Java 层面的OOM。
AndroidRuntime: java.lang.RuntimeException: Canvas: trying to draw too large(127844352bytes) bitmap.
8.0之后的内存分配是在native,Java层的bitmap创建之后,实际上像素内存的分配是在native层直接调用calloc,所以其像素分配的是在native heap上, 这也是为什么8.0之后的Bitmap消耗内存可以无限增长,直到耗尽系统内存,也不会提示Java OOM的原因。
glide加载图片资源的方式有两个:
Glide.with(context) .load(url) .apply(requestOptions.override(width, height)) .into(imageView);
有回调,使用下面加载方式,区别在into传入simpleTarget,而不是imageview
Glide.with(context) .asBitmap() .load(url) .apply(requestOptions) .into(simpleTarget);
其中的simpleTarget有两种定义方式:
simpleTarget = new SimpleTarget<Bitmap>(width, height) {}
simpleTarget = new SimpleTarget<Bitmap>() {}
区别就在于,当你传入了宽高的时候,图片就按照你传入的大小,缓存到了内存(Glide更多级存储大小此处不讨论)。当你不设置宽、高的时候,图片就按照原始的像素大小进行了缓存。
这是因为加载网络图片的时候,我们经常不知道宽、高是多少,我们设置本地资源imageview像素的时候,使用了wrap_content或者match_content,不确定最终的宽高,所以我们选择传入width = 0,height = 0,使用glide下载好图片后,再去做对应的设置。
这是因为,网络图片也好、本地图片也好,像素都不会太大,以像素类型为RGB_8888为例,一个1920*1080的图片,在内存占据内存为1920*1080*4Byte = 829440Byte = 7.9M。
此时设置宽、高(正常也就设置个几十dp)与不设置宽高,区别并不大。
04-27 17:39:53.154 31269-31269/? E/art: Throwing OutOfMemoryError "Failed to allocate a 227278860 byte allocation with 1048576 free bytes and 126MB until OOM"
因为本地的一张图片大小虽然为5.48M,像素为width = 4896 height = 6528,但是在内存占据大小为 4896 * 6528 * 4 = 127844352byte = 120M。这个内存足以使官网app在本来使用内存就高的情况下闪退。
看一下加载这个本地图片时的内存情况,从 320M 到 548M,飙升228M(还有后台事件带来内存波动,引起闪退的根本原因是Graphics的内存飙升)。
想办法去掉simpleTarget的B定义方法
如果你不知道需要现实的资源宽高是多少,设置下面这个参数,这样就以当前屏幕宽、高作为最高显示像素,downsample设置为DownsampleStrategy.AT_MOST。
这个表示:
当你的资源原始尺寸大于width * height(屏幕宽、高像素)时,以width * height为准。 当你的资源原始尺寸小于width * height时,以原始尺寸为准。 width * height作为图片保存到内存时的最大像素值。 闪退问题同样解决,此时内存使用情况从 290M 到 340M,增加50M(还有后台事件带来内存波动)。
其他:如果你有兴趣,可以验证 Android 8.0以下图片内存占用情况,会发现不一样的天地。