在我们的日常开发中,不免经常与Bitmap打交道。在为我们渲染美丽的界面的同时,也经常会带给我们许多的苦恼。比如你会发现,Bitmap占用的内存通常很大,很多OOM的原因都是由于Bitmap占用的内存过大导致的。那么到底为什么Bitmap会占用很大的内存,而我们又该怎么做来避免呢?下面我们来简单的了解下Bitmap。
如何计算Bitmap的内存
我们先来看上面这张图。这张图是我用模拟器跑的一个demo,然后dump的一份hprof文件。这里打了个马赛克,让我们先暂且忽略它,后面再为大家揭晓。可以先告诉大家的是,这张图片的尺寸是700 * 1050。我们看到dump的信息中有一个变量叫做mBuffer,简单介绍一下。mBuffer就是一张bitmap开在java层的的内存的大小,你可以暂时简单理解成一张图片所占用的内存大小,为什么说暂时呢,因为其实应该是大于等于的,这设计到内存复用的逻辑。不过这不是我们这次分享的重点,暂且把它当作一张bitmap所占用的内存。放眼一望,可以看到这张图片占用了很大的内存,6615000字节,算了一下,也就是 700 * 1050 * 9,是我们刚才看到的图片尺寸的九倍!所以这个九倍是怎么来的呢?
我们了解到,每张图片都有自己不同的像素格式。Android支持四种格式,分别是ARGB_8888,ARGB_4444,RGB_565和APLHA_8。每个像素格式的区别如下表:
像素格式 | 单位像素字节数 | 备注 |
---|---|---|
ARGB_8888 | 4 | ARGB每个通道占8位,即每个像素点占32位 |
ARGB_4444 | 2 | 每个通道占4位,即每个像素点占16位 |
RGB_565 | 2 | RGB三个通道分别占5、6、6位,即每个像素点占16位 |
ALPHA_8 | 1 | 仅alpha通道,占8位,即每个像素点占8位 |
每个原色都储存着所表示的颜色值,即位图数越高,所存储的颜色也就越丰富,也就是我们说的画质越高。所以我们认为,一张图片所占用的内存是由所有像素点所占有的字节大小决定的。即:
一张图片的内存 = 宽 * 高 * 单位像素字节数。
其中ARGB_4444已经在API 19及以上的版本废弃了,默认使用32位位图,而ALPHA_8最为特殊,适用场景不多,所以对于我们来说,最常用的就是ARGB_8888和RGB_565两种格式。按照我们的推算公式,ARGB_8888格式的图片应该会比RGB_565格式的图片内存大两倍。下面我们来实验下。
图中我们使用上述两种图片格式去加载了同一张图片(默认采用ARGB_8888格式),我们从log信息中可以清楚的看到,确实是程两倍关系。但是一定是两倍吗?
显然答案是否定的。不然我也就不会问这种问题了= =。我们来看下图:
先从结果上看,这次虽然采用的还是两种格式,但是加载出来的图片大小却是一样的。不知道有没有细心的小伙伴可以一眼看出这张图和上一张图的区别(不是image1变成image2了。。。),我们发现,这次加载的一张图片是png格式的。我们都知道,png格式是有透明度的图片,那么为什么使用一张png格式的图片。我们给图片设定的像素格式就没用了呢?
再看下我们刚才给图片设定像素格式的参数,“inPreferredConfig”。再结合下这个参数的注释,不难理解,这只是个“建议”。但是如果系统发现这张图片不匹配,比如我们上面的例子,他会自己选择一个最合适的像素格式来加载图片,也就说明了,并不是我们设定了像素格式就会有用的。
回到我们一开始的问题,我们得到的答案是9倍的关系,但是即使我们使用默认的ARGB_8888的像素格式,也只有4倍呀,那剩下的几倍在哪里呢?
回到我们最开始的那张图,我们把马赛克去掉,发现并不是我告诉你们的700 * 1050的尺寸。但是我并没有骗你们!那么为什么宽和高都被放大了呢?其实这个是因为有一个情况我没有告诉你们。上面测试的时候可以看到我把图片放在了xhdpi目录下,其实,我使用的模拟器试xxhdpi的,也就是480dpi,是xhdpi的1.5倍,系统会根据自己的位深去对应的目录去找资源,如果找不到的话会从其他的目录里找。我们放到xhdpi目录下的资源系统会认为那是属于xhdpi位深的,所以使用在xxhdpi下就会对其进行扩大。也就导致了宽和高分别被扩大了 480 / 320 即1.5倍,至此终于凑齐了我们开篇所抛下的问题,为什么是9倍的关系了(1.5 * 1.5 * 4)。
关于踩坑
现在我们了解到了一张bitmap可能会为我们带来如此巨大的内存开销,而在日常开发过程中,经常有人跟我们渗透“使用完bitmap要及时释放”的逻辑。我深深的记在心里,于是写出了下面的代码:
放眼望去可能没什么问题,先加载了一张图片A,然后通过一些变换得到了另外一张图片B,然后我及时的把A释放掉了!(快夸我!)
然而并没有如我们所愿,我在跑demo的时候崩溃了,于是我们看了下源码,找到了下面这一段。。。
我们可以看到,这里写着如果返回的bitmap与原来的一样的话,就会把我们传进去的A对象返回出来!所以其实我们上面的做法是不保险的,也就是存在A和B是同一个对象的可能!
再来看下面一段类似的代码:
因为踩过上一个坑,这次我机智的改变了他的高度,也就是不会返回给我同一个对象了。但是这次我换了取bitmap的方法,这样写有问题吗?当然有问题了,不然我拿出来干嘛。不过奇怪的是,我们这次第一次跑这段代码的时候是没有异常的,但是在第二次发生了崩溃!老话说得好,有问题,翻源码,机智的我又找到了下面这段代码:
恍然大悟,原来我们获取资源中的bitmap的时候,系统会为我们做一个缓存,也就是第二次开始所取出来的图片都是我们之前缓存的,而经过了我们第一次的释放后,缓存中的bitmap被释放掉了,也就不难解释为什么我们第二次取出来的bitmap再使用的时候会崩溃了。既然做了缓存,那么其实如果我们反复取这张图,内存是不会增长的,而使用BitmapFactory的方法内存是会一直增长的,有兴趣的小伙伴可以试一下。
至此这次分享的内容结束了,主要是想带大家来了解一下bitmap的一些知识,避免由于bitmap所带来一些内存上的问题,以后大家在处理内存优化的时候可以更加小心。