Bitmap 小结

在Android开发中离不开Bitmap的使用,而Bitmap又是一个吃内存的大胖子(最怕这个胖子和你的应用来个OOM亲密接触)所以在应用中使用Bitmap的时候你得十分小心,不然很有可能就会造成OOM了。本文针对Bitmap做一个简单认识以及在加载bitmap的时候应该怎么防止出现OOM。
一.为什么会出现OOM?
1.每个机型在编译ROM时都设置了一个应用堆内存VM值上限dalvik.vm.heapgrowthlimit,用来限定每个应用可用的最大内存,超出这个最大值将会报OOM。这个阀值,一般根据手机屏幕dpi大小递增,dpi越小的手机,每个应用可用最大内存就越低。所以当加载图片的数量很多时,就很容易超过这个阀值,造成OOM。关于这个阈值我们可以通过以下代码来获取
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        Log.e(TAG, "Max memory is " + maxMemory + "KB");
        ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        Log.e(TAG,"手机堆内存size: " + activityManager.getMemoryClass());
这里我自己用的是夜神模拟器 配置为 720 * 1280 DPI 为240 先看结果吧
  01-26 13:43:12.253 4235-4235/com.example.gzw.androidstudydemo E/BitmapTestActivity: Max memory is 196608KB
  01-26 13:43:12.263 4235-4235/com.example.gzw.androidstudydemo E/BitmapTestActivity: 手机堆内存size: 192
其实大小是一样的上面 196608 KB /1024 = 192M,当然了你也可以申请更大的内存空间,需要在application节点下配置android:largeHeap="true"我配置完以后内存大小变成512M了。但是申请更大的内存空间只能说是暂时压住了OOM的风险(比如说你项目要上线了可以先这么做),作为开发人员的我们当然是需要从代码的角度去优化这个问题啊,谁让咱是做这个的啊o(╥﹏╥)o
2.图片分辨率越高,消耗的内存越大,当加载高分辨率图片的时候,将会非常占用内存,一旦处理不当就会OOM。例如,一张分辨率为:1920x1080的图片。如果Bitmap使用 ARGB_8888 32位来平铺显示的话,占用的内存是1920x1080x4个字节,占用将近8M内存,可想而知,如果不对图片进行处理的话,就会OOM。
3.在使用ListView, GridView等这些大量加载view的组件时,如果没有合理的处理缓存,大量加载Bitmap的时候,也将容易引发OOM
二.一个Bitmap对象到底占了多少内存?
关于一个bitmap到底占用了多少内存通常情况下我们可以用以下的公式进行一个简单的计算
bitmap占用内存 = 图片宽度x图片高度x单位像素占用字节数(Byte)
从公式里可以看出解码一个图片占用内存与图片本身的大小(10M,20M)并没有直接关系的,只与宽,高单位像素占用字节(Byte)有关。(这就好比人的血型一样,不管你胖瘦,年龄,性别等 你的血型只会是ABO模型中的一种))图片宽度和高度很好理解,单位像素占用字节数是个什么鬼呢?难道还会不一样?额,这个真的会不一样。你想想同样是100*100的图片高清和普通的肯定不一样吧。在Android中这个参数是解码图片的格式决定的,Android 系统支持四种像素解码格式,如下图所示

在这里插入图片描述

我们常用的只有ARGB_8888和RGB_565这两种而Android系统在解码图片的时候默认采用的是ARGB_8888来解码的。我们可以先验证一下刚才说的公式是否正确首先我们在工程目录下建立assets目录步骤如图所示在这里插入图片描述
然后我准备了3张图片分别是large.jpg(2000x1000)large1440(2560x1440)large1600(2560x1600)我们可以先用上面的公式来预算一下,默认都用AGRB_8888模式解码则我们先根据上面的公式来计算一下 2000x1000x4 = 8000000byte 2560x1440x4 = 14745600byte 256016004 = 16384000byte 再看代码里面的处理
private Bitmap loadBitmap() {

        Bitmap bm = null;
        try {
           bm = BitmapFactory.decodeStream(getAssets().open("large.jpg"));
           bm = BitmapFactory.decodeStream(getAssets().open("large1440.jpg"));
           //可以用getByteCount()方法来获得bitmap占用的内存
           Log.e(TAG, "loadBitmap: bm=large=2560*1440===========" + bm.getByteCount() + "byte" );
           bm = BitmapFactory.decodeStream(getAssets().open("large1600.png"));
           Log.e(TAG, "loadBitmap: bm=large=2560*1600===========" + bm.getByteCount() + "byte" );
        } catch (OutOfMemoryError e) {
            Log.e(TAG, "loadBitmap: 发生OOM了兄弟");
        }
        return bm;
    }
看结果
E/BitmapTestActivity: loadBitmap: bm=large=2000*1000==========8000000byte
 E/BitmapTestActivity: loadBitmap: bm=large=2560*1440===========14745600byte
 E/BitmapTestActivity: loadBitmap: bm=large=2560*1600===========16384000byte
跟我们之前的计算结果是一样的没问题对吧!但是我们刚才是将图片放在assets目录下面的,如果放到drawable下结果会怎么样呢?首先明确一点Android中drawable系列目录的存在,如:drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi等,是为了兼容适配不同手机屏幕的屏幕密度。

在这里插入图片描述

当我们使用decodeResource方法读取drawable目录下面的图片时,会根据手机的屏幕密度,到对应的文件夹中查找该图片。若该图片存在于其他目录下,系统先对该图片进行缩放处理,再显示。我也把large2000x1000分别放到了这里我将2000*1000的图分别放在了drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi以及drawable下面,直接看结果吧
E/BitmapTestActivity: loadBitmap: bm=hdpi=large=2000*1000==========8000000byte
 E/BitmapTestActivity: loadBitmap: bm=mdpi=large=2000*1000==========18000000byte
  E/BitmapTestActivity: loadBitmap: bm=xhdpi=large=2000*1000==========4500000byte
  E/BitmapTestActivity: loadBitmap: bm=xxhdpi=large=2000*1000==========2000000byte
  E/BitmapTestActivity: loadBitmap: bm=drawable=large=2000*1000==========18000000byte
什么鬼,跟之前的结果完全不一样了好吧!嗯 对的,这个时候就出现了另外一个计算方式了
scale = 设备的屏幕密度 / drawable目录设定的屏幕密度
图片占用内存 = int(图片长度 * scale + 0.5) * int(图片宽度 * scale + 0.5) * 单位像素字节数
我自己测试用的是夜神模拟器 720 *1280 密度是 240
因此在我们的例子中放入mdpi的内存计算为
scale = 240/160
bitmap占用内存 = int(2000*1.5 + 0.5)*int(1000*1.5 + 0.5)*4 = 18000000byte
这个如果你直接把图片放到drawable下面的话会默认使用160(文献上看到的源码我还没去看过)

但是只有在解析本地资源图片的时候采用这个公式其余的还是一开始那个公式

三.怎么减小bitmap占用的内存?
根据我们前面的计算公式,要想减少内存从两个方面入手,1.大小2.单位像素占用字节数。大小这个很好理解,就像裁剪一样嘛,本来是200200,我裁剪成100100显然会减小内存的开销啊。大小这一块我们可以这样解决
BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;//先设置为true就不会加载到内存里面了
            BitmapFactory.decodeResource(getResources(),R.drawable.large,options);
            options.inSampleSize = 2;
            options.inJustDecodeBounds = false;//一定要设置为false否则返回的对象仍然是null
            bm = BitmapFactory.decodeResource(getResources(),R.drawable.large,options);
            Log.e(TAG, "loadBitmap: bm=xxhdpi=large=2000*1000=压缩后=========" + bm.getByteCount() + "byte");
			未压缩,在xxhdpi下E/BitmapTestActivity: loadBitmap: bm=xxhdpi=large=2000*1000==========2000000byte
           压缩后E/BitmapTestActivity: loadBitmap: bm=xxhdpi=large=2000*1000=压缩后=========500000byte
            inSampleSize = 5,6,7
			 E/BitmapTestActivity: loadBitmap: bm=xxhdpi=large=2000*1000=压缩后=========125000byte
简单解释下,首先每个BitmapFactory方法都可以传递一个options参数,我们先把inJustDecodeBounds设置为ture这时候是不会把Bitmap加载到内存中的所以返回的bitmap为null 但是会包含图片宽高,我们可以得到这个宽高,然后我们设置了inSampleSize = 2 ;注意在实际应用中你应该根据你的需求来动态计算这个inSampleSize这里我只是为了显示效果写成了2 那这样的话宽高都变成原来的1/2了所以内存应该变为原来的1/4没问题对吧。(注意insampleSize的值必须大于1且尽量是2的指数,如果不是2的指数会被向下拉成最贴近的那个。我尝试将inSampleSize改成了567 但是结果都是inSmapleSize = 4一样)当然了实际项目中你肯定要根据你自己的需求来动态计算inSampleSize的大小的这里给出一种计算方法
reqWidth,reqHeight代表你的ImageView的宽高
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            //Calculate the largest inSampleSize value that is a power of 2 and
            //keep both
            //height nd width larger than the requested height and width
            while ((halfHeight / inSampleSize) >= reqHeight &&
                    (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;

            }
        }
        return inSampleSize;
    }
    (如果你的图片不允许压缩,比如说世界地图这种,那这个要用到另外的处理方式了本文先不讨论)
对于单位像素所占字节数这个就很明确了,改变解码格式就好
BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            bm = BitmapFactory.decodeResource(getResources(),R.drawable.large,options);
            Log.e(TAG, "loadBitmap: bm=xxhdpi=large=2000*1000=RGB_565=========" + bm.getByteCount() + "byte");
            结果如下:
E/BitmapTestActivity: loadBitmap: bm=xxhdpi=large=2000*1000=RGB_565=========1000000byte
因为RGB_565每个像素占2个字节  ARGB_8888每个像素占4个字节所以改了以后占用的内存会小一半。
***************************************************************************************************
请注意!请注意!请注意!
即使你把解码的格式设置了RGB_565,占用的内存也不一定会变小,因为这个属性只是建议系统这么做,
如果你的图片本身不支持这样的解码格式,那仍然会使用默认的AGRB_8888来解码。
正常情况下从这两方面处理了之后图片占用的内存都不会太大了,如果这样处理了还占用太大的内存的这个我表示我也没法了 这个时候就该用加载框架了,像Glide,Picasso,Fresco这些知名的框架。这些框架用起来贼方便还贼简单,感谢开源的框架O(∩_∩)O哈哈~
总结:本文只是简单的梳理bitmap的一小部分知识,鉴于我是一只小小小小鸟,若有错误之处请各位看官不吝赐教,万分感激!
参考文章

Bitmap详解与Bitmap的内存优化 https://www.jianshu.com/p/8206dd8b6d8b
Android性能优化系列之Bitmap图片优化 https://blog.csdn.net/u012124438/article/details/66087785
android 高效加载大图片,避免OOM https://blog.csdn.net/coderinchina/article/details/40964205
聊聊Bitmap的那些事儿 https://www.jianshu.com/p/0fbcadfd4213?winzoom=1

最后的最后,怎么说呢?我知道我还差的很远很远,但是我相信只要坚持,终将到达理想的彼岸。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值