在日常开发中不可避免的都会遇到图片展示的功能,而通常都会使用到Bitmap,且每个程序都有相应的最大运行内存;由于Bitmap所消耗的内存相当的大(超出程序最大运行内存)在加载多、大图的时候非常容易引起OOM。。这篇文章简单介绍Bitmap的优化以及内存的计算方式。
如何获取我们当前程序的最大运行内存?
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");
1.Bitmap使用及其优化
常见的加载方法
:
public static Bitmap decodeFile(String pathName) {} //从文件中加载
public static Bitmap decodeResource(Resources res, int id) { } //从资源文件中加载
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {} //从byte[]中加载
public static Bitmap decodeStream(@Nullable InputStream is) {} //从输入流中加载
public static Bitmap decodeFileDescriptor(FileDescriptor fd) {} //从FileDescriptor 中加载
之前的文章中有说明相同图片在不同drawable文件夹下对用的宽高也不同,那是因为内部加载时进行了缩放,源码:
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;
}
}
...
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
...
if (willScale) {
const float sx = scaledWidth / float(decoded->width());
const float sy = scaledHeight / float(decoded->height());
bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
bitmap->allocPixels(&javaAllocator, NULL);
bitmap->eraseColor(0);
SkPaint paint;
paint.setFilterBitmap(true);
SkCanvas canvas(*bitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}
可以看出Bitmap 最终绘制出来后会经过一个scale操作,scale比例=设备分辨率/资源目录分辨率
Bitmap所占用内存的加载公式
:
- memorySize=横像素x竖像素*每个像素字节数
- memorySize=宽x高x(设备分辨率/资源目录分辨率)平方x每个像素字节数
获取设备分辨率
:
int densityDpi = getResources().getDisplayMetrics().densityDpi;
资源目录分辨率相关见:https://blog.csdn.net/qq_34341338/article/details/86706751
其中每个像素所代表的字节数根据BitmapFactory.Option.inPreferredConfig
参数决定:
参数 | 字节数 |
---|---|
ALPHA_8 | 1 |
RGB_565 | 2 |
ARGB_8888(默认) | 4 |
RGBA_F16 | 8 |
根据上面的公式,我们可以有以下优化方式:
- 调整
inPreferredConfig
参数,改变像素对应的字节数 - 调整图片所在的资源目录,选择合适的资源目录
- 通过缩放图片的宽高
接下来展示如何优化Bitmap内存大小:
- 第二种方式参照自己手机的dpi与对应的drawable文件夹的dpi即可
- 方式一和三通过一个小案例来进行实现(自己封装成一个工具类)
BitmapUtil,java工具类
package com.example.administrator.drawabletest;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
/**
* 创建时间: 2019/2/15 16:00
* 描述: TODO
*/
@SuppressWarnings("unused")
public class BitmapUtil {
/**
* 计算缩放比率
*
* @param options BitmapFactory.Options
* @param reqW 压缩后的宽
* @param reqH 压缩后的高
* @return 缩放比例
*/
public static int calculateInSampleSize(BitmapFactory.Options options, int reqW, int reqH) {
if (options == null) return 1;
//源图片的宽高
final int sourceW = options.outWidth;
final int sourceH = options.outHeight;
//缩放比例
int sampleSize = 1;
//原图宽高大于需要的宽高
if (sourceW > reqW || sourceH > reqH) {
//计算缩放比率
final int wRatio = Math.round((float) sourceW / (float) reqW);
final int hRatio = Math.round((float) sourceH / (float) reqH);
//选择小的缩放比率,保证最终图片宽高大于等于目标宽高
sampleSize = wRatio > hRatio ? hRatio : wRatio;
}
return sampleSize;
}
/**
* 获取缩放后的图片
*
* @param options BitmapFactory.Options
* @param resources Resources
* @param resId 资源ID
* @param reqW 目标宽
* @param reqH 目标高
* @return 所需Bitmap
*/
public static Bitmap decodeBitmapFromResource(BitmapFactory.Options options, Resources resources, int resId, int reqW, int reqH) {
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = true; //禁止为Bitmap分配内存
//获取图片大小等信息,不生成像素数据
BitmapFactory.decodeResource(resources, resId, options);
//计算缩放比率
options.inSampleSize = calculateInSampleSize(options, reqW, reqH);
//再次解析图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(resources, resId, options);
}
}
使用:
package com.example.administrator.drawabletest;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;
public class BitmapActivity extends AppCompatActivity {
private ImageView ivImage;
private static final String TAG = "wdl";
private TextView tvOption;
int on = 4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bitmap);
ivImage = findViewById(R.id.iv_image);
tvOption = findViewById(R.id.tv_option);
// 优化方法一 改变config
// options.inPreferredConfig = Bitmap.Config.RGB_565;
//
// //优化方式二 选择合适的资源文件目录
//
// 优化方式三 缩放图片
BitmapFactory.Options options = new BitmapFactory.Options();
// options.inJustDecodeBounds = true; //禁止为Bitmap分配内存
// BitmapFactory.decodeResource(getResources(), R.drawable.bit, options);
// options.inJustDecodeBounds = false;
//高效加载大图避免OOM
Bitmap bit = BitmapUtil.decodeBitmapFromResource(options, getResources(), R.drawable.bit, 100, 100);
ivImage.setImageBitmap(bit);
int w = bit.getWidth();
int width = options.outWidth;
int h = bit.getHeight();
int height = options.outHeight;
int densityDpi = getResources().getDisplayMetrics().densityDpi;
int inDensity = options.inDensity;
if (options.inPreferredConfig == Bitmap.Config.RGB_565)
on = 2;
else if (options.inPreferredConfig == Bitmap.Config.ARGB_8888)
on = 4;
//像素*4 ARGB_8888
int memorySizeMethod1 = w * h * on;
int memorySizeMethod2 = (int) (width * height * Math.pow(densityDpi / inDensity, 2) * on);
StringBuilder builder = new StringBuilder();
builder.append("w:").append(w).append("\n")
.append("h:").append(h).append("\n")
.append("memorySizeMethod1:").append(memorySizeMethod1).append("\n")
.append("memorySizeMethod2:").append(memorySizeMethod2).append("\n")
.append("config:").append(options.inPreferredConfig).append("\n")
.append("资源目录分辨率-inDensity:").append(inDensity).append("\n")
.append("inScreenDensity:").append(options.inScreenDensity).append("\n")
.append("inTargetDensity:").append(options.inTargetDensity).append("\n")
.append("设备分辨率-densityDpi:").append(densityDpi).append("\n")
.append("dpiX:").append(getResources().getDisplayMetrics().xdpi).append("\n")
.append("dpiY:").append(getResources().getDisplayMetrics().ydpi).append("\n");
tvOption.setText(builder.toString());
//Log.e(TAG, "w: " + w + " h: " + h + " " + + " memorySize:" + w * h * 4);
}
}
其中的注释都比较完整,不一一进行说明。
大致流程:
1.在加载图片到内存之前先计算图片的一系列值赋值给BitmapFactory.Options
.
2.在改变inPreferredConfig
值
3.计算缩放比例并赋值给options.inSampleSize
4.重新加载进内存并返回Bitmap
其中BitmapFactory.Options options
起到很重要的作用,我们来看一下它的一些参数:
参数 | 含义 |
---|---|
inPreferredConfig | 设置解码时图片压缩的色彩质量参数 |
inSampleSize | 尺寸压缩的比例 |
inJustDecodeBounds | true:只加载图片相关信息,不加载bitmap到内存中,false:重新加载返回bitmap |
outHeight | 图片高 |
outWidth | 图片宽 |
inPreferredConfig | 显示精度 |
参考自:
https://blog.csdn.net/guolin_blog/article/details/9316683
https://www.jianshu.com/p/3f6f6e4f1c88
https://blog.csdn.net/wxl_344600977/article/details/69398823