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崩溃需要进行图片的重新压缩采样处理即可解决内存问题

推荐阅读更多精彩内容