Android图片内存计算方法

Android图片内存计算方法

前言

我们在做项目的时候,内存是一个绕不过去的话题。有时候同样一个app在不同的手机上运行内存差距好大,其中一个原因就是同一张图片在不同的手机中占用内存是不一样的。
如果app中本地图片较多的情况下。你将图片放到drawable目录中,然后再将其盖到mipmap-xxhdpi目录中,你惊奇的发现,内存差距比你想象的打。那么这个时候我们就会有疑问

疑问

  • 一张100kb的图片到底占用多少内存。
  • 同一个图片,放到不同的目录在同一个设备上所占内存是否不同。
  • 同一个目录的图片,在不同设备上,是否所占内存不同。
  • 怎样优化图片的内存。尽量不让他产生oom。

一张图片占多少内存

我们都知道图片在渲染的时候,也是按照一个个像素点去渲染的, 因此大家都会说:
图片内存 = 分辨率 * 每个像素点的大小

这个算法不能说不对,当脱离了具体场景,这个算法是正确的,我们先看下面一段代码:

override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)

      var options :Options = Options();
      options.inPreferredConfig = Bitmap.Config.ARGB_8888;
      var bitmap2: Bitmap = BitmapFactory.decodeResource(resources, R.mipmap.water, options)
      Log.e(TAG,"the water bitmap width="+bitmap2.width+", hieght="+bitmap2.height+", memory size="+bitmap2.byteCount)

      var bitmap: Bitmap = BitmapFactory.decodeResource(resources, R.mipmap.earth,options);
      Log.e(TAG, "the earth bitmap width=" + bitmap.width+",  height="+bitmap.height+", memory size="+bitmap.byteCount)

      val metrics: DisplayMetrics =  getResources().getDisplayMetrics()

      Log.e(TAG,"the density =="+metrics.density)
  }

其中water这张图片的原始尺寸是:5801031,earth图片的原始尺寸是:279180
按照常规理解,inPreferredConfig表示像素中颜色和透明度的信息,ARGB每一位最多可以采用8位表示,所以采用ARGB_8888的情况下,每个像素点
占用的内存是4B, 所以常规情况下,我们可以认为:

图片water内存: 4B5801031 = 2391920 = 2.2811 M
图片earth内存: 4B279180 = 200880 = 0.1915 M

当把图片放到不同目录下在真机的测试结果如下:

名称内存图片位置设备density
water116020629567680hdpi3.0
water87015475383560xhdpi3.0
water58010312391920xxhdpi3.0
earth558360803520hdpi3.0
earth419270452520xhdpi3.0
earth279180200880xxhdpi3.0

我们知道资源文件夹表示的密度值如下:

密度mdpihdpixhdpixxhdpixxxhdpi
密度值160240320480640
density11.5234

通过比较我们发现:
新图的高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi )
新图的宽度 = 原图宽度 * (设备的 dpi / 目录对应的 dpi )

所以单个图片在app中占用的内存是:

图片实际内存 = 新图的高度 * 新图的宽度 * 单个像素内存

我们是靠猜得出的结论吗?

也许有兄弟说,是不是根据结果反推的条件。当然不是啦, 这都是可在在源码中找到的。

首先看我们生成bitmap的方式:

public static Bitmap decodeResource(Resources res, int id, Options opts) {
       validate(opts);
       Bitmap bm = null;
       InputStream is = null;

       try {
           final TypedValue value = new TypedValue();
           is = res.openRawResource(id, value);

           bm = decodeResourceStream(res, value, is, null, opts);
       } catch (Exception e) {
       } finally {
           try {
               if (is != null) is.close();
           } catch (IOException e) {
               // Ignore
           }
       }

       if (bm == null && opts != null && opts.inBitmap != null) {
           throw new IllegalArgumentException("Problem decoding into existing bitmap");
       }

       return bm;
   }

可见图片解析分为2个阶段:

  • res.openRawResource(id, value) 读取原始图片流
  • decodeResourceStream 对数据流进行解码适配。

记下来我们看解码适配过程:

public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
        validate(opts);
        if (opts == null) {
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }

        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }

        return decodeStream(is, pad, opts);
    }

我们可以看到传入的option影响最终内存占比的是indensity和intargetDensity,

  • inDensity 表示图片存放的 Drawable 文件夹代表的 densityDpi
  • inTargetDensity : 表示当前设备的 densityDpi 。

接着一路跟下去,会执行到nativeDecodeStream这个方法,我们接下来只能去找BitmapFactory.cpp文件中的
decodeStream方法了,一直会跟到doDecode方法。其中代码如下:

//只截取了最终有关的一段代码

if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
          const int density = env->GetIntField(options, gOptions_densityFieldID);
          const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
          const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
          if (density != 0 && targetDensity != 0 && density != screenDensity) {
              scale = (float) targetDensity / density;
          }
      }

在这里我们可以看到最终图片的实际拉伸效果是scale = (float) targetDensity / density,即设备的dpi/文件目录dpi

接下来我们看计算内存大小的代码

//getByteCount源码,
public final int getByteCount() {
       if (mRecycled) {
           Log.w(TAG, "Called getByteCount() on a recycle()'d bitmap! "
                   + "This is undefined behavior!");
           return 0;
       }

       return getRowBytes() * getHeight();
   }

根据源码,我们可以看出,图片资源大小,是getRowBytes()乘以图片高度, 我们只需要查找getRowBytes就可以了。我们追下去,最后到了
nativeRowBytes()这个方法,看来只能找底层的源码了。最终的结论就是:

图片实际内存 = 新图的高度 * 新图的宽度 * 单个像素内存

而单个像素内存,与我们选择的ARGB有关。而在Android中通常用一个32位的整数来表示一个像素的颜色和透明度。
32位的4个字节从高到低分别表示Alpha,R,G,B四个通道,每个通道用8位表示,每个通道的值范围是[0, 0xFF),
对Alpha通道0表示完全透明,0xFF表示完全不透明。对R,G,B三个通道,0表示没有该通道的颜色分量,0xFF表示该通道颜色分量达到最大。
每个通过最多是8位,所以8888为4B, 565位2B。 这个就不详细讲了。

内存优化

平时我们看到最常见的内存优化有2中:

  • 对于不需要透明度的设置为RGB565 (降低像素点大小)
  • 通过设置 inSampleSize来降低图片分辨率。
    对于第二点不说什么了。这个肯定没问题, 但是对于第一点,一定记住,不是所有的情况都是用。

public Bitmap.Config inPreferredConfig
If this is non-null, the decoder will try to decode into this internal configuration. If it is null, or the request cannot be met, the decoder will try to pick the best matching config based on the system’s screen depth, and characteristics of the original image such as if it has per-pixel alpha (requiring a config that also does). Image are loaded with the Bitmap.Config#ARGB_8888 config by default.


官方文档说的很清楚,并不是所有情况一定按照我们设置的计算。比如一张图片本身是含有透明度的,你即使设置565,仍然不会按照你设置的机型计算。这点需要特别注意。

总结

  • 这种图片的计算方式,要根据不同的场景来讨论。 如果不是放到资源文件中,通过网络下载那么就遵循 长 * 宽 * 单个像素大小 的原则。
  • 内存优化通过设置rgb565不一定生效。降低分辨率肯定会节省内存。
  • 热门的图片加载类库,有自己的处理方式。要根据情景单独使用。

写在最后

本篇博客中如有内容不符,请留言告知,我会尽快休息。每一位程序员都很苦,为了尽量给后来者留下正确的资料,需要每个人的力量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值