部分内容转载自:
https://www.jianshu.com/p/0fbcadfd4213?winzoom=1
https://zhuanlan.zhihu.com/p/20732309?refer=bennyhuo
笔者曾经在面试时,也曾经遇到过这个问题:一个Bitmap对象, 究竟占用多大内存空间?
一般来讲,Bitmap的占用的内存空间,有下面的计算方法:
bitmap的内存空间 = 图片宽度 * 图片高度 * 单位像素所占字节数(Byte)
图片宽度和高度比较好理解,单位像素标识每个像素所占字节数。在不同的编码下有所不同。例如RGB_8888 ARGB四个通道,每个通道占8位,即每个像素点占32位,4个字节。RGB_565三个通道,分别占5、6、5位,即每个像素点占16位,2个字节。
假设一张长400像素,宽300像素,jpg格式。我们把它放到 assets 目录中。
图片的占用内存,我们可以用Bitmap的getByteCount方法得到,在运行程序前,我们先按照公式算出该图内存占用是多少。由于Android默认采用ARGB_8888格式解码,即一个像素点占4个字节,因此:
该图占用内存 = 400 * 300 * 4 = 480000 Byte
PS:不同资源目录下,图片内存占用情况
图片放在drawable-hdpi目录时,内存占用1920000字节:
图片放在drawable-xhdpi目录时,内存占用1080000字节:
图片放在drawable-xxhdpi目录时,内存占用480000字节:
屏幕密度与Drawable系列目录的对应关系如下:
目录 | 屏幕密度 |
---|---|
drawable-ldpi | 120dpi |
drawable-mdpi | 160dpi |
drawable-hdpi | 240dpi |
drawable-xhdpi | 320dpi |
drawable-xxhdpi | 480dpi |
测试机型为小米6,手机屏幕DPI为480。其根本原因在于,默认加载图片时会根据图片所处的文件目录的屏幕密度,与当前手机的屏幕密度进行对比缩放。
因此,我们的例子中,图片放入hdpi文件夹时所占内存为:
scale = 480 / 240 = 2;
内存 = int(400 * 2 + 0.5) * int(300 * 2 + 0.5) * 4 = 1920000
图片放入xhdpi文件夹时所占内存:
scale = 480 / 320 = 1.5;
内存 = int(400 * 1.5 + 0.5) * int(300 * 1.5 + 0.5) * 4 = 1080000
源码在BitmapFactory.java中:
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
//实际上,我们这里的opts是null的,所以在这里初始化。
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density; //这里density的值如果对应资源目录为hdpi的话,就是240
}
}
if (opts.inTargetDensity == 0 && res != null) {
//请注意,inTargetDensity就是当前的显示密度,比如三星s6时就是640
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
大图加载避免OOM,算是老话题了,网上的资料很多,在此笔者只介绍几种方法。OOM名为内存溢出,在避免内存溢出前,我们先搞清楚一个问题:为什么会发生内存溢出?
这要从Android的内存管理说起,Android系统的手机在系统底层指定了堆内存的上限值,我们的程序在申请内存空间时,为了确保能够成功申请到内存空间,应该保证当前已分配的内存加上当前需要分配的内存值的总大小不能超过当前堆的最大内存值(手机厂商会根据手机的配置情况来对其进行调整)。
如果解析后图片的大小加上目前已分配的堆内存大小超过堆内存最大值,系统先会执行一遍gc操作,若gc后仍然超过堆内存最大值,这时候将会抛出异常,这是就是发生了我们常说的OOM。
查看手机的堆内存大小,我们有两种方式:
通过代码查看:
ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
Log.d("AchillesL","size: " + activityManager.getMemoryClass());
OR,通过adb指令查看:
adb shell getprop|grep heapgrowthlimit
小米,查到堆内存最大值为256M。
但是可以通过AndroidManifest.xml中的Application节点中声明android:largeHeap="true"
标识,即可以分配到更大的堆内存空间。
很多情况下,网络下载的图片都使用jpg格式。对于jpg图片,使用RGB_565格式进行解码比ARGB_8888解码会节省更多内存。
设置图片的解码格式,可以通过BitmapFactory.Options类的inPreferredConfig属性来指定。代码如下所示:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.pic,options);
总结:如果不需要alpha通道,特别是资源本身为jpg格式的情况下,用RGB_565格式解码更加节省内存。
开发过程中发现一个bug,表现是:在华为mate10 pro等机型上加载超长图片失败,会有一次黑屏闪烁,然后图片出不来。图片规格约为480*10000+。最初是怀疑Glide下载后图片太大导致内存加载不上。但是经过定位发现问题出在华为机子本身上,查看log发现一个openGL错误;Bitmap too large to be uploaded into a texture。提示超过了openGL 8192*8192大小限制。
我们知道,绘图的最终点在于使用openGL将图片进行绘制。而openGL在是有硬件绘制大小限制。这个值在Canvas中,可以通过Canvas.getMaximumBitmapWidth()获取。Andriod系统本身限定值为MAXMIMUM_BITMAP_SIZE = 32766。因为才疏学浅对硬件知识不熟,我理解这值代表的是openGL绘制时可以一次容纳的图片宽高值。
通过测试部分国产手机,发现大部分获取的值为16384。个人理解是在硬件烧录的时候,手机厂商改了这个值,相当于阉割了部分GPU的性能。但是这不是最坑爹的,最坑爹的是华为mate10啊。。。你一个8.0的机型,这个值居然是8192。。。还没有小米魅族2年前的机型大啊。。。搞毛啊。。。
1、关闭硬件加速,Android默认在api14以后,会开启硬件加速。但是在不使用硬件加速的情况下,并不会限制一次载入的图片大小。但是性能影响太大,作罢。
2、在获取到图片时,先与系统的openGL可绘制大小进行比对,一旦发现大于系统大小,则对图片进行scale裁剪。