Android开发经验谈Android开发Android知识

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

2017-09-07  本文已影响912人  Ch3r1sh

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

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

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

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

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

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

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个简单的图片测试,我们可以得出一个简单的结论:

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

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

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的计算采样方法(有现成的干吗不用)

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

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

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

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

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

使用重新采样后的drawable

直接加载图片而不缓存

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

QQ20170907-165600@2x.png

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

总结

上一篇下一篇

猜你喜欢

热点阅读