问题
1.一张图在手机内存中占有多大?
2.如何优化图片大小?
3.大图如何展示,比如世界地图?
4.Drawable存放位置有什么区别?
为什么要优化Bitmap?
Bitmap对内存影响很大,比如说我们要加载一张4048x3036像素的照片,如果按照ARGB_8888来显示的话,那么就需要将近47M的内存大小(4048x3036x4bytes),这么大的消耗很容易引起OutOfMemoryError(OOM)异常,因此必须要对Bitmap进行优化。
一张图在手机内存中占有多大?
在上一个问题中,有写到不进行压缩的情况下一张图所占用的内存:width * height * 一个像素所占用的字节
,这种计算方式在绝大部分情况下是正确的但是又不是完全正确,因为我们遗漏了Density
要素。BitmapFactory解码图片资源的时候会从BitmapFactory.Options
读取配置信息,而如果加载本地资源文件
,则会在方法链过程中写入Density相关的配置:
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) {
//inDensity默认文件夹密度
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//inTargetDensity为屏幕实际密度
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
上述方法会将inDensity默认设置为图片所在文件夹密度值,将inTargetDensity
设置为屏幕实际密度值。后续方法会走到BitmapFactory.cpp的nativeDecodeStream
:
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options) {
...
bitmap = doDecode(env, bufferedStream.release(), padding, options);
}
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
...
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 is necessary due to density differences.
if (scale != 1.0f) {
//获取缩放尺寸
willScale = true;
scaledWidth = static_cast(scaledWidth * scale + 0.5f);
scaledHeight = static_cast(scaledHeight * scale + 0.5f);
}
...
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
...
canvas.scale(sx, sy);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
...
}
上述块中摘取了相关联的代码,可以看到nativeDecodeStream方法调用了doDecode方法,在其中先确认通过Options的属性确认缩放系数scale,然后获取到缩放后的尺寸,最后完成缩放。因此本地资源文件内存大小的计算方式为size = width * height * (targetDensity / density) * (targetDensity / density) * 像素所占用的字节
。
加载其余图片资源和本地资源文件流程大体一致,只是没有考虑Density元素,因此计算方式为size = widthheight像素所占用的字节。
像素的存储方式
Bitmap.Config | 占位 | 描述 |
---|---|---|
Bitmap.Config.ALPHA_8 | 1bytes | 只存储alpha信息 |
Bitmap.Config.RGB_565 | 2bytes | 只存储RGB信息 R占用5位 G占用6位 B占用5位 |
Bitmap.Config.ARGB_4444 | 2bytes | 占用 4位 R占用4位 G占用4位 B占用4位 |
Bitmap.Config.ARGB_8888 | 4bytes | 占用 8位 R占用8位 G占用8位 B占用8位 |
默认情况下像素是以ARGB_8888方式存储,如果需要缩略图等质量不高的图片,可以通过降低像素存储方式来实现。
知道了图片内存确切的计算方式,那么该如何优化图片呢?
1.Bitmap.compress质量压缩
2.inJustDecodeBounds和inSampleSize结合
第一种方式是质量压缩
bitmap.compress(Bitmap.CompressFormat.JPEG,100,ous);
第二个参数是质量压缩的百分比,100为不压缩。该方法并不能改变图片的尺寸和内存大小,但是可以改变ous的大小,使用场景是在一些对图片长度有要求的场景,比如微信分享这些的。
第二张方式则是常用的内存压缩方式
设置Options的inJustDecodeBounds
字段为true,可以在不将图片加载到内存的情况下读取图片信息,然后通过图片的尺寸和目标尺寸对比,计算出inSampleSize
的值,然后将inJustDecodeBounds
设置为false,加载经过压缩的图片到内存中。
//内存压缩
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.download,options);
options.inSampleSize = getSampleSize(options,50,50);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.download,options);
img.setImageBitmap(bitmap);
private int getSampleSize(BitmapFactory.Options options,float realWidth,float realHeight) {
int bitmapWidth = options.outWidth;
int bitmapHeight = options.outHeight;
int sampleSize = 1;
if (bitmapWidth > realWidth && bitmapHeight > realHeight) {
int halfWidth = bitmapWidth/2;
int halfHeight = bitmapHeight/2;
while (halfWidth/sampleSize > realWidth && halfHeight/sampleSize>realHeight){
sampleSize *= 2;
}
}
return sampleSize;
}
//BitmapFactory.app中对sampleSize的支持
if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
willScale = true;
scaledWidth = codec->getInfo().width() / sampleSize;
scaledHeight = codec->getInfo().height() / sampleSize;
}
//BitmapFactory.app中如果inJustDecodeBounds为true,则会返回nullptr,不会走下面的createBitmap方法。
gOptions_justBoundsFieldID = GetFieldIDOrDie(env, options_class, "inJustDecodeBounds", "Z");
if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
onlyDecodeSize = true;
}
if (onlyDecodeSize) {
return nullptr;
}
注:sampleSize
会取最接近的2的幂次方的值。
如何加载大图
通过上面的学习我们已经知道了如何去优化图片内存了,那么现在有一张超大尺寸的图(世界地图),我们该如果清晰的加载到手机中呢?如果用之前的策略去加载的话,的确可以将图加载到手机中,但是图片就看不清了,这不符合我们的预期,所以面对这种情况,我们考虑有局部加载策略。
局部加载
Bitmap布局加载的核心是BitmapRegionDecoder
类,此类通过decodeRegion方法获取图片局部区域的Bitmap实例,从而绘制在View上。
Bitmap bitmap = mDecoder.decodeRegion(mRect,options);
其中mRect
是绘制的矩形区域,options
是图片的配置项。
相关细节代码如下:
供外部传入,获取图片宽高并初始化BitmapRegionDecoder
public void setInputStream(InputStream stream){
try {
BitmapFactory.Options tempOption =new BitmapFactory.Options();
tempOption.inJustDecodeBounds = true;
BitmapFactory.decodeStream(stream, null, tempOption);
mImageWidth = tempOption.outWidth;
mImageHeight = tempOption.outHeight;
mDecoder = BitmapRegionDecoder.newInstance(stream, false);
requestLayout();
invalidate();
} catch (IOException e) {
e.printStackTrace();
}
}
//测量方法中获取确切的矩形区域
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
//默认直接显示图片的中心区域,可以自己去调节
mRect.left = imageWidth / 2 - width / 2;
mRect.top = imageHeight / 2 - height / 2;
mRect.right = mRect.left + width;
mRect.bottom = mRect.top + height;
}
//onDraw方法绘制
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (null != mDecoder) {
Bitmap bitmap = mDecoder.decodeRegion(mRect,options);
canvas.drawBitmap(bitmap,0,0,paint);
}
}
//onTouchEvent刷新矩形区域,达到移动图片的效果
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if (mImageWidth > getWidth())
{
float moveX = event.getX() - mDownX;
mRect.offset((int) -moveX, 0);
mDownX = event.getX();
checkWidth();
invalidate();
}
if (mImageHeight > getHeight())
{
float moveY = event.getY() - mDownY;
mRect.offset(0, (int) -moveY);
mDownY = event.getY();
checkHeight();
invalidate();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
}
return true;
}
Drawable存放位置有什么区别?
上面我们学习了Bitmap相关的知识,现在我们来看看drawable的一些注意点:
在Android开发过程中,我们我们可以看到以下层级:
其中mipmap一般用来存放不同分辨率的App图标,引用方式和drawable一样R.mipmap.xxx。drawable存放我们开发过程中的不同分辨率的图片资源,其中各文件夹对应分辨率如下:
密度 | 建议尺寸 |
---|---|
mipmap-mdpi | 48 * 48 |
mipmap-hdpi | 72 * 72 |
mipmap-xhdpi | 96 * 96 |
mipmap-xxhdpi | 144 * 144 |
mipmap-xxxhdpi | 192 * 192 |
dpi范围 | 密度 |
---|---|
120dpi | ldpi |
120dpi ~ 160dpi | mdpi |
160dpi ~ 240dpi | hdpi |
240dpi ~ 320dpi | xhdpi |
320dpi ~ 480dpi | xxhdpi |
480dpi ~ 640dpi | xxxhdpi |
注意同一张图片放到不同的文件夹有不同的展现形式,实际效果就是放到密度越低的文件夹中,展现到手机的尺寸越大,低密度的尺寸在高密度的手机上系统会默认放大,会导致占用内存增加,因此图片优先放到高密度的文件夹中。手机优先从更高的密度中获取资源,如果获取不到则会从低密度中获取,获取顺序为:drawable-xxxhdpi->drawable-nodpi ->drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。
drawable-nodpi这个文件夹是一个密度无关的文件夹,放在这里的图片系统就不会对它进行自动缩放,原图片是多大就会实际展示多大。但是要注意一个加载的顺序,drawable-nodpi文件夹是在匹配密度文件夹和更高密度文件夹都找不到的情况下才会去这里查找图片的,因此放在drawable-nodpi文件夹里的图片通常情况下不建议再放到别的文件夹里面。