Android中一张100X100大小的图片占用多大内存

1,673 阅读10分钟

在Android中,我们在做图片优化的时候,一般做的只是保证图片占用的空间越小越好,没有考虑过内存中的图片到底有多大,那么,一张100*100的图片在内存中有多大呢?网上一般的说法是:图片高度*图片宽度*每个像素数所占的byte大小,这个说法是没有错的,但是在安卓资源中放入图片,图片就一定不会发生变化吗?

我们还是来自己实验一下,在mipmap-xxhdpi中放入一张100*100的png格式的图片,然后获取图片的一些信息,代码如下:

final BitmapFactory.Options options = new BitmapFactory.Options();
final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.hundred, options);
//Bitmap bitmap = BitmapFactory.decodeFile("mnt/sdcard/weixin.png", options);
imageView.setImageBitmap(bitmap);
imageView.post(new Runnable() {
    @Override
    public void run() {
         Log.i("TAG", "bitmap:ByteCount = " + bitmap.getByteCount() + "--- bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
         Log.i("TAG", "width:" + bitmap.getWidth() + "--- height:" + bitmap.getHeight());
        Log.i("TAG", "inDensity:" + options.inDensity + "--- inTargetDensity:" + options.inTargetDensity);
        Log.i("TAG", "imageview.width:" + imageView.getWidth() + "---  :imageview.height:" +   imageView.getHeight());
    }
});

打印日志如下:

2020-12-07 17:05:02.202 26516-26516/com.hkrt.component I/TAG: bitmap:ByteCount = 28900--- bitmap:AllocationByteCount = 28900
2020-12-07 17:05:02.202 26516-26516/com.hkrt.component I/TAG: width:85--- height:85
2020-12-07 17:05:02.202 26516-26516/com.hkrt.component I/TAG: inDensity:480--- inTargetDensity:408
2020-12-07 17:05:02.202 26516-26516/com.hkrt.component I/TAG: imageview.width:85---  :imageview.height:85

可以看到,加载的图片大小由100*100变成了85*85,这个是怎么改变的,观察可以知道,是由inTargetDensity和inDensity的比值而来。

如果我们把图片转放到mipmap-xhdpi中,又会是什么情况呢?我们改变一下图片目录,再次运行,日志如下:

2020-12-07 17:08:39.067 27189-27189/com.hkrt.component I/TAG: bitmap:ByteCount = 65536--- bitmap:AllocationByteCount = 65536
2020-12-07 17:08:39.068 27189-27189/com.hkrt.component I/TAG: width:128--- height:128
2020-12-07 17:08:39.068 27189-27189/com.hkrt.component I/TAG: inDensity:320--- inTargetDensity:408
2020-12-07 17:08:39.068 27189-27189/com.hkrt.component I/TAG: imageview.width:128---  :imageview.height:128

可以看到,由于放到了mipmap-xhdpi目录,inDensity的值放生了变化,导致了加载的图片大小发生了变化,宽高变为408/320*100  = 127.5约等于128,占用字节大小128*128*4=65536。

以上说的是将图片放入资源文件中,那放入磁盘中,图片大小又是多大呢?我们再来实验一下,执行如下代码:

final BitmapFactory.Options options = new BitmapFactory.Options();
//        final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.hundred, options);
        final Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/hundred.png", options);
        imageView.setImageBitmap(bitmap);
        imageView.post(new Runnable() {
            @Override
            public void run() {
                Log.i("TAG", "bitmap:ByteCount = " + bitmap.getByteCount() + "--- bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
                Log.i("TAG", "width:" + bitmap.getWidth() + "--- height:" + bitmap.getHeight());
                Log.i("TAG", "inDensity:" + options.inDensity + "--- inTargetDensity:" + options.inTargetDensity);
                Log.i("TAG", "imageview.width:" + imageView.getWidth() + "---  :imageview.height:" +   imageView.getHeight());
            }
        });

日志如下:

2020-12-08 10:07:16.082 28080-28080/com.hkrt.component I/TAG: bitmap:ByteCount = 40000--- bitmap:AllocationByteCount = 40000
2020-12-08 10:07:16.082 28080-28080/com.hkrt.component I/TAG: width:100--- height:100
2020-12-08 10:07:16.082 28080-28080/com.hkrt.component I/TAG: inDensity:0--- inTargetDensity:0
2020-12-08 10:07:16.082 28080-28080/com.hkrt.component I/TAG: imageview.width:100---  :imageview.height:100

我们再看下assets目录中的图片大小占用 内存情况:

final Bitmap bitmap =  getImageFromAssetsFile("hundred.png");

/*
 * 从Assets中读取图片
 */
private Bitmap getImageFromAssetsFile(String fileName){
    Bitmap image = null;
    AssetManager am = getResources().getAssets();
    try
    {
        InputStream is = am.open(fileName);
        image = BitmapFactory.decodeStream(is);
        is.close();
    }
    catch (IOException e)
    {
        e.printStackTrace();
    }
    return image;

}

日志如下:

2020-12-08 10:15:58.179 29333-29333/com.hkrt.component I/TAG: bitmap:ByteCount = 40000--- bitmap:AllocationByteCount = 40000
2020-12-08 10:15:58.179 29333-29333/com.hkrt.component I/TAG: width:100--- height:100
2020-12-08 10:15:58.179 29333-29333/com.hkrt.component I/TAG: inDensity:0--- inTargetDensity:0
2020-12-08 10:15:58.179 29333-29333/com.hkrt.component I/TAG: imageview.width:100---  :imageview.height:100

我们分别调用了decodeResource、decodeFile、decodeStream方法,我们查看一下这些方法的代码:

BitmapFactory#decodeResourcepublic 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) {
        /*  do nothing.
            If the exception happened on open, bm will be null.
            If it happened on close, bm is still valid.
        */
    } 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;
}

BitmapFactory#decodeResourceStream

@Nullable
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);
}

可以看到decodeResourceStream方法中针对option中的inDensity和inTargetDensity做了处理,如果放在资源文件中,inDensity不同的density有不同的值,具体值如下边的表,inTargetDensity取决于具体的Android设备.如果放在磁盘或者assets目录中那么这两个值都未0.

BitmapFactory#decodeFile:

public static Bitmap decodeFile(String pathName, Options opts) {
    validate(opts);
    Bitmap bm = null;
    InputStream stream = null;
    try {
        stream = new FileInputStream(pathName);
        bm = decodeStream(stream, null, opts);
    } catch (Exception e) {
        /*  do nothing.
            If the exception happened on open, bm will be null.
        */
        Log.e("BitmapFactory", "Unable to decode stream: " + e);
    } finally {
        if (stream != null) {
            try {
                stream.close();
            } catch (IOException e) {
                // do nothing here
            }
        }
    }
    return bm;
}

可以看到,我们最终都调用了decodeStream 方法,继续查看其代码:

@Nullable
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
        @Nullable Options opts) {
    // we don't throw in this case, thus allowing the caller to only check
    // the cache, and not force the image to be decoded.
    if (is == null) {
        return null;
    }
    validate(opts);

    Bitmap bm = null;

    Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
    try {
        if (is instanceof AssetManager.AssetInputStream) {
            final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
            bm = nativeDecodeAsset(asset, outPadding, opts, Options.nativeInBitmap(opts),
                Options.nativeColorSpace(opts));
        } else {
            bm = decodeStreamInternal(is, outPadding, opts);
        }

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

        setDensityFromOptions(bm, opts);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
    }

    return bm;
}

调用了setDensityFromOptions方法,这个方法很重要:

/**
 * Set the newly decoded bitmap's density based on the Options.
 */
private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
    if (outputBitmap == null || opts == null) return;

    final int density = opts.inDensity;
    if (density != 0) {
        outputBitmap.setDensity(density);
        final int targetDensity = opts.inTargetDensity;
        if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
            return;
        }

        byte[] np = outputBitmap.getNinePatchChunk();
        final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
        if (opts.inScaled || isNinePatch) {
            outputBitmap.setDensity(targetDensity);
        }
    } else if (opts.inBitmap != null) {
        // bitmap was reused, ensure density is reset
        outputBitmap.setDensity(Bitmap.getDefaultDensity());
    }
}

这里主要就是把刚刚赋值过的两个属性inDensity和inTargetDensity给Bitmap进行赋值,不过并不是直接赋给Bitmap就完了,中间有个判断,当inDensity的值与inTargetDensity或与设备的屏幕Density不相等时,则将应用inTargetDensity的值,如果相等则应用inDensity的值。

所以总结来说, setDensityFromOptions 方法就是把 inTargetDensity 的值赋给Bitmap,不过前提是opts.inScaled = true;

由此我们可以得出以下结论:

1、res目录中图片被加载进内存的时候,分辨率会经过一层转换,显示的图片会进行缩放,宽高不是图片本身的宽高;

2、res目录分辨率和设备分辨率一致时,图片尺寸不会缩放;

3、不同res目录下的图片的inDensity的值不同,Android设备的density也各不相同,最终图片显示的宽高取决于图片在哪个res目录和屏幕密度;

4、decodeFile、decodeStream在解析时不会对Bitmap进行一系列的屏幕适配,Android系统不会进行处理,直接按照图片原有大小进行处理;

屏幕密度和密度值的对应关系如下:

我们继续以在mipmap-xxhdpi中的日志为例,图片所占用的内存大小为28900byte,这个是怎么算出来的呢,上面我们已经提到了,这里在重复一下,加强记忆?

计算方式是这样的:

图片宽度*图片高度*没个像素占的像素数

Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (inTargetDensity / inDensity )的平方× 每个像素的字节大小

100  * (408 / 480) *   1 00   *  (408 / 480 )* 4=28900

那个这个4,每个像素占用的字节数是4字节是怎么来的呢?

我们在一开始就声明了一个BitmapFactory.Option类的对象,Option是BitmapFactory的内部类,其主要作用是用于解码Bitmap时的各种参数控制,我们看下其中的各种参数的定义:

inJustDecodeBounds:

如果将这个值置为true,那么在解码的时候将不会返回bitmap,只会返回这个bitmap的尺寸。这个属性的目的是,如果你只想知道一个bitmap的尺寸,但又不想将其加载到内存时。这是一个非常有用的属性。

inSampleSize:

这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4。

inPreferredConfig:

这个值是设置色彩模式,默认值是ARGB_8888,在这个模式下,一个像素点占用4bytes空间,一般对透明度不做要求的话,一般采用RGB_565模式,这个模式下一个像素点占用2bytes。

inPremultiplied:

这个值和透明度通道有关,默认值是true,如果设置为true,则返回的bitmap的颜色通道上会预先附加上透明度通道。

inDither:

这个值和抖动解码有关,默认值为false,表示不采用抖动解码。

inDensity:

表示这个bitmap的像素密度(对应的是DisplayMetrics中的densityDpi,不是density)。

inTargetDensity:

表示要被画出来时的目标像素密度(对应的是DisplayMetrics中的densityDpi,不是density)。

inScreenDensity:

表示实际设备的像素密度(对应的是DisplayMetrics中的densityDpi,不是density)。

inScaled:

设置这个Bitmap是否可以被缩放,默认值是true,表示可以被缩放。

inPurgeable和inInputShareable:

这两个值一般是一起使用,设置为true时,前者表示空间不够是否可以被释放,后者表示是否可以共享引用。这两个值在Android5.0后被弃用。

inPreferQualityOverSpeed:

这个值表示是否在解码时图片有更高的品质,仅用于JPEG格式。如果设置为true,则图片会有更高的品质,但是会解码速度会很慢。

outWidth和outHeight:

表示这个Bitmap的宽和高,一般和inJustDecodeBounds一起使用来获得Bitmap的宽高,但是不加载到内存。

我们也可以查看此处的源码,可以看到这个值时由inPrefferedConfig这个值来决定的,其类型为Bitmap.Config类型,其默认值是ARGB_8888,具体的枚举如下:

ALPHA_8

此时图片只有alpha值,没有RGB值,一个像素占用1个字节

ARGB_4444

一个像素占用2个字节,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各占4个bites,共16bites,即2个字节

ARGB_8888

一个像素占用4个字节,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各占8个bites,共32bites,即4个字节

这是一种高质量的图片格式,电脑上普通采用的格式。它也是Android手机上一个BitMap的默认格式。

RGB_565

一个像素占用2个字节,没有alpha(A)值,即不支持透明和半透明,Red(R)值占5个bites ,Green(G)值占6个bites ,Blue(B)值占5个bites,共16bites,即2个字节.对于没有透明和半透明颜色的图片来说,该格式的图片能够达到比较的呈现效果,相对于ARGB_8888来说也能减少一半的内存开销。因此它是一个不错的选择。另外我们通过android.content.res.Resources来取得一个张图片时,它也是以该格式来构建BitMap的.

从Android4.0开始,该选项无效。即使设置为该值,系统任然会采用 ARGB_8888来构造图片

由以上可以看到,我们在加载Bitmap的时候,也可以通过指定inPrefferedConfig的值来改变所占用的内存的大小。如果我们想节省图片加载内存,可以指定inPrefferedConfig类型为RGB_565。

结论:

综上所述,当我们在做Bitmap图片的内存占用优化时,只有两个方向可以入手;

  • 降低图片分辨率;
  • 减少每个像素点所占内存大小,例如使用低色彩的解析模式,如RGB565;
  • 资源文件合理放置,高分辨率图片可以放到高分辨率目录下;