当前位置: 首页 > 科技观察

位图消耗的内存比你想象的要多-挂OOM

时间:2023-03-14 22:03:55 科技观察

1.前言在一个app中,难免会有一些Bitmap资源被打包到apk中,并随apk一起发布。而你在使用这些Bitmap资源的时候,需要占用多少内存空间呢?这是一个很实际的问题,如果把握不当,可能会导致各种OOM错误。本文将讨论本地Bitmap到底占用了多少内存空间?2、占用了多少内存?2.1如何获取占用的内存空间?既然我们要谈的是一个Bitmap资源加载到内存中占用的空间,那么我们就需要一个清晰的方法来获取它,准确知道它到底占用了多少空间。而Android也确实为我们提供了类似的API,即Bitmap.getByteCount()。比如现在工程中有一张400*200像素的图片,在drawable-xhdpi目录下,在Nexus6设备上,运行加载。看它的输出大小。看输出:I/cxmyDev:byteCound:720000可以看到getByteCount()是根据getRowBytes()*getHeight()计算出来的。getHeight()方法是Bitmap的高度,什么是getRowBytes()?2.2getRowBytes()的计算是基于getRowBytes()方法,最后调用了一个nativeRowBytes()方法,是native方法。既然要查,查到底,看看native代码是怎么实现的(文中native源码是基于Android5.1.1的,文末会有在线查看地址,以及附上行号以便于参考)。我们先看看Bitmap.cpp的代码中rowBytes()是如何实现的。我这里看到的是Android5.1.1的源代码。其实从Android6开始,就会使用LocalScopedBitmap来操作。它实际上只是SkBitmap的一个包。如下图,使用LocalScoopedBitmap操作rowBytes()。有兴趣的可以继续看它是如何实现的。可以看到,最后还是用了SkBitmap来实现。在SkBitmap.cpp中可以确认颜色为ARGB_8888,每个像素会占用4个字节。这样看,结合上面提到的Bitmap.getByteCount()的计算公式是:bitmapInRam=bitmapWidth*4bytes*bitmapHeight但是如果按照这个公式计算一个结果,你会发现得到的值会差很多比实际价值。前面demo中的图片加载到内存中,占用的内存是:720000。但是使用我们这里得到的计算方法,计算的结果是。400*200*4=320000那么,问题出在哪里呢?2.3Density影响Bitmap内存中的Demo2.1,明确指出需要存放图片的Drawable目录,以及使用的设备。其实都是有关联的,不是没有关联的路人A。就图片而言,放在不同的Drawable目录下,对应不同密度的设备。密度是设备的固有参数。除了density,还有densityDpi,也是和设备相关的,表示屏幕每英寸对应多少个点(非像素点)。它们之间的关系可以直接参考官方文档,这里不再赘述。https://developer.android.com/guide/practices/screens_support.html这里所说的密度其实代表了不同的drawable-xxx目录。以上是官方提供的一张经典图片。可以看到不同的目录代表不同的密度。比如xhdpi代表的密度是2,这里的密度对densityDip的基准是160,也就是说mdpi对应的densityDpi是160,xhdpi对应的densityDpi是320,它们的关系如下:densitydensityDpi在Android中可以通过标准API获取,使用DisplayMetrics即可。看Nexus5的输出:I/cxmyDev:density:3.0I/cxmyDev:densityDpi:480知道了设备的density和densityDpi,继续看加载Bitmap的过程,使用BitmapFactory.decodeResource()方法。从源码可以看出,其实是分两步完成的。使用openRawResource()方法获取图片的原始流。使用decodeResourceStream()方法解码和适配数据流。对于一个文件流,我们这里不需要关心。主要影响图像内存的是decodeResourceStream()方法中解码和适配数据流的过程。在这个方法中,传递了一个Options对象来配置当前图片的解码和适配。从代码中可以看出,影响图片内存比例的因素有两个:inDensity和inTargetDensity。Options中的这两个值都可以设置。如果不对它们进行额外的操作,它们分别默认代表的含义:inDensity:存放图片的Drawable文件夹所代表的密度Dpi。inTargetDensity:当前设备的固有密度Dpi。而使用他们的代码都是native的,继续看BitmapFactory.cpp的源码(源码太多,只贴重点),可以看到其实是通过两个密度计算出一个比例值尺度,它将按照scale指示的比例缩放图像的原始像素。也就是说,对于同一张图片,放在不同drawable文件夹下的图片,在不同设备上加载的大小其实是不一样的。计算图片内存的公式要调整为:scale=targetDensity/inDensitybitmapInRam=(bitmapWidth*scale)*(bitmapHeight*scale)*4bytes然后用新的公式计算上面图片的大小:400*(480/320)*200*(480/320)*4=720000可以看出最终的结果和我们程序中计算出来的值是一致的,所以这就是我们最终得到的计算图片在内存中所占比例的公式。然后重写上面的Demo,输出所有的细节。查看我们关心的Log输出:I/cxmyDev:byteCound:720000I/cxmyDev:rowBytes:2400I/cxmyDev:height:300I/cxmyDev:width:600I/cxmyDev:density:3.0I/cxmyDev:densityDpi:4803.4例子中给定的,图片的尺寸和设备的密度Dpi是非常有规律的。但不排除存在一些非标准设备,使用上述计算公式加载的图片仍然不正确。对于这个问题,我们还是要在源码中寻找答案。对于标准密度Dpi较少的设备,按照这个比例计算出来的大小可能是一个float值,也就是有小数,图片的大小都是以int为单位的。所以为了避免这样的问题,Android做了一个公差值(0.5)转换成int类型。代码仍在BitmapFactory..cpp中。因此,getByteCount()API得到的大小可能与我们前面使用的公式计算出的大小略有不同,这个值是小数点之间。4.总结完成。这里我们已经明确了一个本地Bitmap加载到内存时会占用多少内存。决定Bitmap内存大小的因素与图像文件在磁盘上占用的空间无关。总结起来有以下几点:颜色格式:比如ARGB_8888和RGB_5555,单位像素占用的内存空间不同。图像本身的像素尺寸。存储图像文件的可绘制目录。xhdpi和xxhdpi不一样。目标设备的densityDpi值。最后附上Android5.1.1的相关源码Bitmap.cpp供大家参考:http://androidxref.com/5.1.1_r6/xref/frameworks/base/core/jni/android/graphics/Bitmap.cppSkBitmap。cpp:http://androidxref.com/5.1.1_r6/xref/external/skia/src/core/SkBitmap.cppBitmapFactory.cpp:http://androidxref.com/5.1.1_r6/xref/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp【本文为专栏作者“张扬”原创稿件,转载请微信联系作者♂获取授权】点此查看作者更多好文