Android 图片内存控制重采样加载高分辨率图片,拒绝OOM

在平常的开发中,经常容易遇到的问题便是OOM的内存泄漏,而在泄漏的过程中,图片的问题一般占据榜首位置,即便在当前已经有了诸多优秀开源的图片缓存框架的情况下,有时候依旧不可避免.图片的加载消耗内存,大量的图片进行内存消耗,使用以后不加以回收等等都是导致图片内存泄漏的问题所在.

这时需要我们来理解图片的内存使用情况,如何来解决问题.

图片由一个个的像素点构成,加载过程会创建一个二维数组,在数组中图片分辨率为x,y,每一个像素点由ARGB组成,占据4个字节因此常理来说消耗的内存应该为:

1KB=1024Byte 1MB= 1024Byte*1024= 1048576Byte

消耗内存大小=分辨率x * 分辨率y * 4byte=??Byte

我们来来观察一张1080*1920的图片的在各个文件夹下的内存消耗状况.

  • 首先看看密度,密度值,代表分辨率之间的关系

    密度 密度值 分辨率
    mdpi 120dpi ~ 160dpi 320 * 480
    hdpi 160dpi ~ 240dpi 480 * 720
    xhdpi 240dpi ~ 320dpi 720 * 1280
    xxhdpi 320dpi ~ 480dpi 1080 * 1920
    xxxdhpi 480dpi ~ 640dpi 1440 * 2560
xxhdpi下的显示

直接加载资源图片

        <ImageView
        android:background="@color/colorAccent"
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

  imageView.setImageResource(R.drawable.gyy1080);

内存占用大小:15.12MB

QQ20170907-153400@2x.png

整个imageview控件占据大小位置:

QQ20170907-154745@2x.png

可以看出所占内存为15.12MB.按照之前的公式1920 * 1080 * 4byte约等于8MB,可是我们这里怎么消耗了差点2倍?

这是我们先考虑是否因为自身手机的dpi不属于xxhdpi范围.

检测手机的屏幕密度值

   xdpi = getResources().getDisplayMetrics().xdpi;
        ydpi = getResources().getDisplayMetrics().ydpi;
        Log.e("密度值","xdpi: " +xdpi + "--"+"ydpi: "+ydpi + "");
        

打印结果

E/密度值: xdpi: 640.0--ydpi: 640.0

从打印结果中我们得知图片的密度值属于xxxdhpi。

下面我们将xxhdpi中的图片放置到xxxhdpi中观察结果

xxxhdpi下的显示

xxxhdpi下图片大小:


QQ20170907-155137@2x.png

xxxhdpi下内存占用大小:


QQ20170907-154259@2x.png

从现实结果上可以看出8.99MB和我们预计的8MB的出入大小已经很接近了.多出的0.99MB主要是由于图片的EXIF也还有一定的信息数据,所以实际会比我们预计的大小要大.

并且图片所占据屏幕的大小也有所改变,这是我们猜测是否是图片被系统自动改变了图片控件大小,我们继续测试,跳过xhpdi,将图片放到hdpi下测试

hdpi下的显示

hdpi下图片大小:


QQ20170907-155519@2x.png

hdpi下内存占用大小:


QQ20170907-155509@2x.png

这时候我们发现更恐怖的事情发生了,图片控件充斥满了整个屏幕不说,内存更恐怖的消耗达到了57.14MB,要知道这仅仅只是一张图片,要是有更多的图片这样岂不是爆炸...

部分总结

经过上述3个简单的图片测试,我们可以得出一个简单的结论:

  • UI在设计时也应该尽量以当前市场的主流密度来作为设计(比如当前是1080P,后续可能就是2K了),并且程序猿在图片的放置位置应该尽量放置在高密度的文件夹中,以此来减少内存的开销

有的人说为什么要设计主流的密度?

  • 如果是超过了主流密度的图片本身已经很大了,对于内存消耗是一样的,并且过大在低分辨率的机型显示会最大化的占据识图空间

  • 如果是设计的尺寸低于主流市场的密度过多,又会导致图片在高分辨率机型上缩小,并且在放大后会看见明显的模糊状况.

因此尽量设计主流密度来完成开发.

OK...你以为到这里就结束了...NO NO NO. 有的时候即便我们的图片放在最顶级的文件夹中,但是因为图片本身巨大,根本无法读取加载,也是必然的OOM

大图加载

这里我使用一张3500 * 5250的图片来进行加载,按照常规方式加载

   imageView.setImageResource(R.drawable.biggyy3500);

QQ20170907-160250@2x.png

直接就OOM爆炸了.我们不禁想怎么办?

如何加载大分辨率图片

对于大分辨率图片而言,手机即便成将其加载出来,那么消耗的内存也是巨大的,在移动设备上来说内存是很可贵的,你用了这么多,别的地方要使用内存怎么办呢,所以我们可以将图片进行压缩,来降低他的分辨率,适配当前的手机然后在进行加载.

既保证了内存的开销又保证了图片的分辨率适应当前设备.

要改变图片的分辨率,我们需要用到BitmapFactory.Options,使用它获取图片的信息并且根据当前的设备进行压缩采样生成新的Drawable来进行使用.

  BitmapFactory.Options options = new BitmapFactory.Options();
        // 不读取像素数组到内存中,仅读取图片的信息
        options.inJustDecodeBounds = true;

        // 获取图片大小
        BitmapFactory.decodeResource(resource, resId, options);
        
          // 从Options中获取图片的分辨率
        int srcWidth = options.outWidth;
        int srcHeight = options.outHeight;
 boolean densityFaking = false;

        if (options.inDensity < resource.getDisplayMetrics().densityDpi) {
            // 相同的density不会scale放大
            options.inDensity = resource.getDisplayMetrics().densityDpi;
            densityFaking = true;

            if (DEBUG_SCALE) {
                Log.d(TAG, "set inDensity=" + resource.getDisplayMetrics().densityDpi);
            }
        } else {
            // 根据density计算scale缩小之后宽高
            srcWidth = scaleFromDensity(srcWidth, options.inDensity, options.inTargetDensity);
            srcHeight = scaleFromDensity(srcHeight, options.inDensity, options.inTargetDensity);

            if (DEBUG_SCALE) {
                Log.d(TAG, "scaleFromDensity srcWidth=" + srcWidth + " srcHeight=" + srcHeight);
            }
        }
         ImageSize srcSize = new ImageSize(srcWidth, srcHeight);
        ImageSize tarSize = new ImageSize(Constants.DISPLAY_WIDTH, Constants.DISPLAY_HEIGHT);

        // 根据density计算scale之后的宽高才是准确的采样源大小
        // 计算采样率,缩小图片
        int inSampleSize = ImageSizeUtils.computeImageSampleSize(srcSize, tarSize, ViewScaleType.FIT_INSIDE, true);

        if (useRgb565) {
            if (DEBUG_SCALE) {
                Log.d(TAG, "PreferredConfig use RGB565");
            }
            // 通常机型能根据图片是否有Alpha通道来决定是否真正使用RGB_565,但有的机型是强制应用,所以RGB_565还是得慎重使用
            options.inPreferredConfig = Bitmap.Config.RGB_565;

        } else if (!densityFaking && inSampleSize == 1) {
            // 不需要压缩,也不需要采样,直接返回null,由外部处理
            if (DEBUG_SCALE) {
                Log.d(TAG, "No scaling and no sampling, just return");
            }
            return null;
        }

        options.inSampleSize = inSampleSize;
        // 读取图片像素数组到内存中,设定的采样率
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeResource(resource, resId, options);
        
        return bitmap;

代码的核心在于使用BitmapFactory.Options获取到了图片一系列的信息,根据图片的信息和设备的分辨率作比较,判断是否进行缩放,以及如何缩放.

在缩放的处理上可以自行实现或者借鉴ImageLoader的核心计算缩放的方法.

自行简单计算采样率:

                // 计算采样率
                int scaleX = 图片宽分辨率 / 设备宽分辨率;
                int scaleY = 图片高分辨率 / 设备高分辨率;
                int inSampleSize = 1;
                
                if (scaleX > scaleY && scaleY >= 1) {
                    inSampleSize = scaleX;
                }
                if (scaleX < scaleY && scaleX >= 1) {
                    inSampleSize = scaleY;
                }

在这里我使用ImageLoder的计算采样方法(有现成的干吗不用)

  • computeImageSampleSize
    通过对源图片的宽高和目标图片的宽高(设备的分辨率)进行循环压缩判断,直到获取到一个适合当前屏幕比例的采样率。并且在ImagView中因为有图片的样式风格还加入了ScaleType的区别处理,简直业界良心

获得采样率之后就可以将图片重新设置采样率输出Bitmap。

获取压缩后的Drawanle
   BitmapDrawable drawable = new BitmapDrawable(bitmap);
    drawable.setTargetDensity(resource.getDisplayMetrics().densityDpi);

压缩后的Drawable和设备的分辨率保持一致性.

这里我们只是获取了Drawable,如果是一些常用的甚至可以使用弱引用将其缓存下来,注意缓存的时候需要缓存的是Bitmap,而不是Drawable

  • 缓存Bitmap
  private static HashMap<String, WeakReference<Bitmap>> stringWeakReferenceBitmap =
            new HashMap<String, WeakReference<Bitmap>>();
            
               // 缓存Bitmap对象
            stringWeakReferenceBitmap.put(key, new WeakReference<Bitmap>(bitmap));
  • 获取缓存的BitMap
      // 从弱引用缓存中获取
        WeakReference<Bitmap> ref = stringWeakReferenceBitmap.get(key);

使用重新采样后的drawable

直接加载图片而不缓存

imageView.setImageDrawable( ResourceUtils.getScaledDrawable(getResources(),R.drawable.biggyy3500));

QQ20170907-165600@2x.png

可以看出我即便这张图片的分辨率达到3500 * 5250,在经过压缩重新采样适配当前设备后,依然将其加载出来了,并且内存消耗仅有5MB.

总结

  • 设计师设计图片要根据主流分辨率设计
  • 攻城狮在放置图片时要根据设计师设计的图片分辨率来选择正确的文件夹并且尽量选择高分辨率的文件夹
  • 如果有低分辨率的图片而运行在高分辨率机型OOM崩溃需要进行图片的重新压缩采样处理即可解决内存问题
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,198评论 6 514
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,334评论 3 398
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,643评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,495评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,502评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,156评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,743评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,659评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,200评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,282评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,424评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,107评论 5 349
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,789评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,264评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,390评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,798评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,435评论 2 359

推荐阅读更多精彩内容