Bitmap的加载简单优化
Bitmap如何加载
Bitmap可以认为是Android系统将图片加载GPU的一个映射,Android可以读取png格式的,也可以读取jpg格式的。那么Android是如何加载一张图片的呢?有个类叫做BitmapFactory,它提供了四个方法:decodeFile(从文件系统中加载),decodeResource(从资源中加载),decodeStream(从输入流中加载),decodeByteArray(从字节数组中加载);其中decodeFile和decodeResource又间接调用了decodeStream方法;
下面是decodeFile的代码:
public static Bitmap decodeFile(String pathName, Options 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;
}
下面是decodeResource的代码:
public static Bitmap decodeResource(Resources res, int id, Options 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;
}
其中的decodeResourceStream方法是这样的:
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options 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);
}
最后依然调用的是decodeStream方法。
这几个方法是在底层实现的,对应着几个native方法,这里不说了。
BitmapFactory.Option的几个参数简介
我们都知道在Android里内存溢出的最大元凶就是图片,如何避免这种情况呢?其实也挺简单,就是采用BitmapFactory.Option这个对象设置图片显示的尺寸。
我们一般显示图片会使用ImageView等控件,但是它们需要的尺寸一般都会比我们提供的图片的尺寸下,所以我们要对图片压缩,下面我们就看一下BitmapFactory.Option提供的方法。
属性 | 说明 |
---|---|
int inSampleSize | 字面意思为取样的尺寸,为int 型;虽然它只设定一个值,但是它会影响到解析图片为Bitmap时长宽两个方向上的像素大小。它设定值会使Bitmap按倍数缩小。默认为1,最小值也为1,表示不缩小;当大于1时,它才会按照比例缩小;比如我们设置为2,Bitmap的长宽会变为原大小的一半,那么相应的它的像素所占有的内存就会缩小为原来的1/4。需要注意的是它的值必须为2的n次幂,例如:1,2,4,8,16 |
boolean inJustDecodeBounds | 字面意思为只解析Bitmap边界,也就是长宽;为Boolean型,当我们设置为true时,我们使用BitmapFactory的decode方式解析图片时就不会返回Bitmap,而只会读取该图片的尺寸和类型信息。我们一般通过这个方法来获取尺寸,然后再按照其尺寸缩小。当然用完记得重新置为false。 |
Bitmap.Config inPreferredConfig | 字面为Bitmap优先的设置,它是一个enum,它设置的是Bitmap的像素类型;默认为ARGB_8888,这个后面再具体介绍。 |
int outHeight | Bitmap 的高 |
int outWidth | Bitmap 的宽 |
String outMimeType | 图片的MIME类型,比如“image/jpeg” |
上面的只是其中的一部分,但是已经够用了。下面我们通过实例来看一下它的使用。
如何利用inSampleSize压缩图片
我们上面说过,我们不想加载图片的原尺寸,我们需要对它进行尺寸的压缩。那么我们一般的步骤是什么样的呢?
- 获取图片尺寸
- 计算Bitmap合适的尺寸,即计算inSampleSize的值
- 设置inSampleSize,压缩Bitmap,并显示在ImageView上
下面我们根据这个步骤来实现一下,这里我随便在网上找了了一张图片,放到了Android的外部存储设备中,我们直接读取这个图片,它的像素为1024 × 683,格式为”jpg“。
首先我们不压缩,看看什么情况:
Bitmap b = BitmapFactory.decodeFile(getBeautyPath());
ivBeauty.setImageBitmap(b);
上面的ImageView宽高都为wrap_content
,然后我们开始压缩之旅。
1. 获取尺寸
BitmapFactory.Options options = new BitmapFactory.Options();
//这里我获取图片的尺寸和类型信息
options.inJustDecodeBounds = true;
//我们这里获取到的Bitmap是空的,图片没有真正的加载到内存,只有尺寸和类型信息
Bitmap bitmap = BitmapFactory.decodeFile(getBeautyPath(), options);
//为了确定为空,我们做一下判断
if (bitmap == null) {
loge("图片的MIME类型 = " + options.outMimeType);
loge("图片的高 = " + options.outHeight);
loge("图片的宽 = " + options.outWidth);
}
上面的loge
方法为我自己封装的,调用的是Log.e()
。运行之后我们得到了下面的log,证明我们的是没有问题的。
09-03 11:16:41.426 13194-13194/com.liteng.mytest E/BitmapFactoryTest: 图片的MIME类型 = image/jpeg
09-03 11:16:41.426 13194-13194/com.liteng.mytest E/BitmapFactoryTest: 图片的高 = 683
09-03 11:16:41.426 13194-13194/com.liteng.mytest E/BitmapFactoryTest: 图片的宽 = 1024
2.计算Bitmap合适的尺寸
我们获取了原图的尺寸,我们想把它显示到一个宽高分别为200,100的ImageView上,那我们怎么计算呢?
这里我们封装一个方法:
private int computeInSampleSize(BitmapFactory.Options options, int targetW, int targetH) {
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
//判断一下原图的宽高与我们的目标宽高大小,如果原图的长或者宽大于目标宽高才计算
if (width > targetW || height > targetH) {
int halfH = height / 2;
int halfW = width / 2;
//inSimpleSize的必须是2的指数次幂,所以我们取最可能的inSampleSize的最大值
while ((halfH / inSampleSize) > targetH && (halfW / inSampleSize) > targetW) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
3.设置inSampleSize,压缩Bitmap,并显示在ImageView上
BitmapFactory.Options options = new BitmapFactory.Options();
//这里我获取图片的尺寸和类型信息
options.inJustDecodeBounds = true;
//我们这里获取到的Bitmap是空的,图片没有真正的加载到内存,只有尺寸和类型信息
Bitmap bitmap = BitmapFactory.decodeFile(getBeautyPath(), options);
//为了确定为空,我们做一下判断
if (bitmap == null) {
loge("图片的MIME类型 = " + options.outMimeType);
loge("图片的高 = " + options.outHeight);
loge("图片的宽 = " + options.outWidth);
}
//获取计算到的inSampleSize
options.inSampleSize = computeInSampleSize(options,200,100);
//我们不再只获取图片的尺寸和类型了,下一步我们需要加载Bitmap了
options.inJustDecodeBounds = false;
Bitmap targetBitmap = BitmapFactory.decodeFile(getBeautyPath(),options);
ivBeauty.setImageBitmap(targetBitmap);
那么经过我们压缩过的图片是什么样的呢?看图
那么我们在看一下log呢?
09-03 12:03:19.957 8642-8642/com.liteng.mytest E/BitmapFactoryTest: 图片的MIME类型 = image/jpeg
09-03 12:03:19.957 8642-8642/com.liteng.mytest E/BitmapFactoryTest: 图片的高 = 683
09-03 12:03:19.957 8642-8642/com.liteng.mytest E/BitmapFactoryTest: 图片的宽 = 1024
09-03 12:03:19.957 8642-8642/com.liteng.mytest E/BitmapFactoryTest: inSampleSize = 4
我们计算出来的inSampleSize为4,之前我们说过,in SampleSize只能是2的指数次幂,我们这里只能尽可能接近我们需要值,而不能完全精确。
上面我们说到Bitmap.Config 这个枚举,它的代码如下(去掉了注释和空行):
public enum Config {
ALPHA_8 (1),
RGB_565 (3),
@Deprecated
ARGB_4444 (4),
ARGB_8888 (5);
final int nativeInt;
private static Config sConfigs[] = {
null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888
};
Config(int ni) {
this.nativeInt = ni;
}
static Config nativeToConfig(int ni) {
return sConfigs[ni];
}
}
我们都知道Bitmap占有内存为:
我们在上面代码中看到的 ALPHA_8,RGB_565,ARGB_4444 ,ARGB_8888就是Bitmap的四种像素形式,它们每个像素占用的字节数分别为4、2、2、1;即
其中ARGB4444在API13被废弃了。
下面是整个Activity的代码,感兴趣的可以再封装一下,做一个工具类:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "BitmapFactoryTest";
private ImageView ivBeauty;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ivBeauty = (ImageView) this.findViewById(R.id.ivBeauty);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_4444;
//这里我获取图片的尺寸和类型信息
options.inJustDecodeBounds = true;
//我们这里获取到的Bitmap是空的,图片没有真正的加载到内存,只有尺寸和类型信息
Bitmap bitmap = BitmapFactory.decodeFile(getBeautyPath(), options);
//为了确定为空,我们做一下判断
if (bitmap == null) {
loge("图片的MIME类型 = " + options.outMimeType);
loge("图片的高 = " + options.outHeight);
loge("图片的宽 = " + options.outWidth);
}
//获取计算到的inSampleSize
options.inSampleSize = computeInSampleSize(options,200,100);
loge("inSampleSize = " + options.inSampleSize);
//我们不再只获取图片的尺寸和类型了,下一步我们需要加载Bitmap了
options.inJustDecodeBounds = false;
Bitmap targetBitmap = BitmapFactory.decodeFile(getBeautyPath(),options);
ivBeauty.setImageBitmap(targetBitmap);
}
private int computeInSampleSize(BitmapFactory.Options options, int targetW, int targetH) {
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
//判断一下原图的宽高与我们的目标宽高大小,如果原图的长或者宽大于目标宽高才计算
if (width > targetW || height > targetH) {
int halfH = height / 2;
int halfW = width / 2;
//inSimpleSize的必须是2的指数次幂,所以我们取最可能的inSampleSize的最大值
while ((halfH / inSampleSize) > targetH && (halfW / inSampleSize) > targetW) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
private String getBeautyPath() {
String path = Environment.getExternalStorageDirectory() + "/Download/beauty.jpg";
return path;
}
private void loge(String msg) {
Log.e(TAG, msg);
}
}