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

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

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

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

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

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

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

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

    密度密度值分辨率
    mdpi120dpi ~ 160dpi320 * 480
    hdpi160dpi ~ 240dpi480 * 720
    xhdpi240dpi ~ 320dpi720 * 1280
    xxhdpi320dpi ~ 480dpi1080 * 1920
    xxxdhpi480dpi ~ 640dpi1440 * 2560
xxdhpi下的显示

直接加载资源图片

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

  imageView.setImageResource(R.drawable.gyy1080);

内存占用大小:

QQ20170907-153400@2x.png

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

QQ20170907-154745@2x.png

可以看出所占内存为15.14MB.按照之前的公式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崩溃需要进行图片的重新压缩采样处理即可解决内存问题
ASP组件AspJpeg(加水印)使用方法大全ASPJPEG是Persits出品的共享软件,它是一款功能相当强大的asp图象处理组件,用它可以轻松地做出图片的缩略图和为图片加上水印功能。 水印组件AspJpeg2.4.0.1 下面简单介绍一下使用方法: 您先要执行下载得到的exe文件,安装该组件. 检测是否注册成功的方法: 将以下代码保存为asp,拷贝在服务器上,用浏览器打开,看返回结果。 一、为图片添加水印 <% Dim Jpeg ''''//声明变量 Set Jpeg = Server.CreateObject("Persits.Jpeg") ''''//调用组件 Jpeg.Open Server.MapPath("aaa.JPG") ''''//源图片位置 Jpeg.Canvas.Font.Color = &H000000; ''''//水印字体颜色 Jpeg.Canvas.Font.Family = "宋体" ''''//水印字体 Jpeg.Canvas.Font.Size = 14 ''''//水印字体大小 Jpeg.Canvas.Font.Bold = False ''''//是否粗体,粗体用:True Jpeg.Canvas.Font.BkMode = &HFFFFFF; ''''//字体背景颜色 Jpeg.Canvas.Print 10, 10, "不败顽童工作室" ''''//水印文字,两个数字10为水印的xy座标 Jpeg.Save Server.MapPath("aaa_05.jpg") ''''//生成有水印的新图片及保存位置 Set Jpeg = Nothing ''''//注销组件,释放资源 Response.Write "" ''''//在该页显示生成水印后的图片 %> 二、生成缩略图 <% Dim Jpeg ''''//声明变量 Set Jpeg = Server.CreateObject("Persits.Jpeg") ''''//调用组件 Jpeg.Open Server.MapPath("aaa.JPG") ''''//原图位置 Jpeg.Width = Jpeg.OriginalWidth/4 ''''//设图片宽度为原图的四分之一 Jpeg.Height = Jpeg.OriginalHeight/4 ''''//设图片高度为原图的四分之一 Jpeg.Sharpen 1, 130 ''''//设定锐化效果 Jpeg.Save Server.MapPath("aaa_small.jpg") ''''//生成缩略图位置及名称 Set Jpeg = Nothing ''''//注销组件,释放资源 Response.Write "" ''''//在该页显示生成缩略图 %> aspjpeg组件高级使用方法介绍 aspjpeg是一款非常强大的图片处理组件,纯英文版本。不过早已经有免费版和破解版,但是对其进行详细与深入介绍的文章却是不多,即使有也只牵涉到图片缩略和图片水印。可能是因为纯英文的缘故。 这里我就是针对这些问题谈谈aspjpeg的高级用法。这里的技术主要包括: 图片缩略 图片水印 安全码技术 图片切割 图片合并 数据库支持 更多不常用的方法介绍 以及相关的一些实用技术 aspjpeg唯一点不足的就是输出方式比较单一。在这里,我们主要谈将图片处理保存后再调用的这种输出方法。另外,本人比较懒,所以有些代码仍然引用于原文档,不懂的地方偶会加以解释! 学过vb或者.net的同志肯定一看就明白了。刷子来着。呵呵。 一、图片缩略 <IMG SRC="images/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值