在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;
- 资源文件合理放置,高分辨率图片可以放到高分辨率目录下;