Android bitmap加载占用内存分析(draw too large bitmap)

一、问题描述

最近被反馈了一个导致应用崩溃的bug,在极少低版本的手机会必现。对于能必现的bug,还是有十足的把握解决的,毕竟不解决也不能下班。

简单看了一眼如下的崩溃日志。
Android bitmap加载占用内存分析(draw too large bitmap)_第1张图片

创建一个132M的bitmap对象,这肯定是个很低级的错误。现在Android手机配置都很好了,所以在低端一点的手机上面会出现崩溃,内存大点的没有出现崩溃。根据堆栈,快速定位到是显示启动图片时候发生的,这个bitmap就是启动图片那张,看了一下资源里面的图片是一张1920 * 1080、95K大小的jpg图片。为什么加载的时候需要创建一个132M的bitmap呢?

查了一下,很快找到原因,是因为原本应该放在drawable-xxhdpi的2倍图放在了drawable目录下面,Android系统在加载资源的时候,会把图片放大。将图片移动至drawable-xxhdpi,问题得到解决。

为什么放在drawable下面的图片会放大,规则是什么样的,一张图片对应在内存中的bitmap对象大小是怎么计算的呢?

二、bitmap 占用内存计算

这个问题我觉得还是很有价值的,毕竟在Android的内存占用上面,bitmap占大头,一些重图片浏览的应用,必须做到很合理的分配和释放,才能在时间和空间上面找到一个平衡点,达到最佳的用户体验。

经过查阅一些资料,得到下面的计算公式:

bitmapInRam = bitmapWidth * bitmapHeight * 4bytes

很简单,总的像素点乘以每个像素点占用内存得到bitmap的大小,4bytes来源于 ARGB_8888 也就是我们写颜色值的时候比如#FFFFFFFF刚好4个字节。如果按照这个计算公式,上面的启动图片的大小是8.3M,和上面的132M比起来还有很大的差距,上面说了是因为图片放在错误的drawable目录下面导致内存增大的。

size = 1920 * 1080 * 4 = 8294400

这个height和width是怎么计算的呢?

首先要知道density、densityDpi、dp、px是什么?

1、densityDpi 像素密度,一英寸内像素点的个数。假如每英寸有160个像素,那么该设备的屏幕的

像素密度=160dpi;

2、dp (density-independent pixel) 与终端上的实际物理像素点无关,长度固定是一英寸的1/160;

3、density表示dp与px之间的倍数,换算关系为1dp=density px,比如一个设备的densityDpi为320,表示一英寸内有320个像素点,那么1dp= 2 px 此时 density = 2。

4、density与densityDpi的关系如下表:

density 1 1.5 2 3 3.5 4
densityDpi 160 240 320 480 560 640

一般情况下设计 UI 的同学会使用px为单位,开发会使用dp为单位,因为在不同的设备上面显示出来的尺寸是一致的。

drawable下面的图片是怎么放大的?

不同的drawable目录对应这个一种densityDpi,设备在加载资源的时候会优先去和屏幕densityDpi一致的目录下面找,如果没有的话才会去其他的目录加载。drawable和densityDpi对应关系如下:

ldpi mdpi hdpi xhdpi xxhdpi xxxhdpi
120dpi 160dpi 240dpi 320dpi 480dpi 640dpi

如果一个设备是480dpi,而需要加载的图片在drawable-mdpi下面,会将图片放大480/160倍,也就是3倍。如果把1920*1080的图片放在mdpi下面,用xxhdpi的手机加载,内存计算就成了74.6M,如果是放在drawable(120dpi)下面,刚好是上面崩溃报出来的132710400。

bitmapInRam = bitmapWidth * 3 * bitmapHeight * 3 * 4bytes
size = 1920 * 3 * 1080 * 3 * 4 = 74649600

按照上面的规则,很容易计算放在不同drawable下面的图片占用内存的大小,下面通过demo验证一下结论。

首先将启动图片放在drawable下面,打印bitmap的height、width已经占用内存,通过getByteCount可以获取。测试的手机是华为荣耀9 densityDpi=480dpi。

override fun onWindowFocusChanged(hasFocus: Boolean) {
    super.onWindowFocusChanged(hasFocus)
    val drawable = iv_splash.drawable
    if (drawable != null && drawable is BitmapDrawable) {
        val bitmap = drawable.bitmap
        Log.i("MainActivity", "height:${bitmap.height},width:${bitmap.width}")
        Log.i("MainActivity", "bitmapInRam:${bitmap.byteCount}")
    }
}

在这里插入图片描述

测试的结果和上面的计算公式计算出来的一致。

接下来放在drawable-xxhdpi试试
在这里插入图片描述

因为手机的像素密度和drawable-xxhdpi一致,所以bitmap的长宽和图片一致。

这个问题算是分析得比较清楚了,其实Android这样设计也是合理的。是想如果严格按照规定,不同dpi的手机加载不同的drawable下面的资源,那么一张图片就需要出各种尺寸,这样会大大增加apk的体量。设计成可以加载其他目录下的图片,也算是一种曲线策略,这样必然就会有一个图片的放大和缩小,比如一个只有160像素的图片放到480像素的位置,肯定就需要放大三倍。但是这种放大并不会将图片变得清晰,就像是在浏览照片的时候不断放大,最终一个像素点会成为一个马赛克的一块,而在内存中对应的大小是硬件上面的像素点。如果将本来应该放在xxhdpi下面的图片放在了ldpi,这时候就会变得很夸张。

一般情况下 UI 同学给到我们的是2倍或者3倍图,是因为现在主流的设备是480dpi,这样就是在满足大部分机型的情况下,只使用一套图来缩小APK的size。

PS:有缘看到本文的朋友,如果发现文中有任何错误,或者有不同理解的,请多多指教。

(はじめまして どうぞ よろしく お願いします)

你可能感兴趣的:(Android,内存分析,移动开发)