Android_Bitmap的高效加载

时间:2023-02-01 21:03:42

在讲bitmap的高效加载时我们有必要先来了解认识一下这两个重要的类BitmapFactory和Options。它们之前的关系,从源码中我们可以看到Options是BitmapFactory的一个内部类。

public class BitmapFactory {
private static final int DECODE_BUFFER_SIZE = 16 * 1024;

public static class Options {
/**
* Create a default Options object, which if left unchanged will give
* the same result from the decoder as if null were passed.
*/

public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}
}

BitmapFactory
它提供了一下几个不同的静态方法,利用不同的原料加工出bitmap对象。

//将一个byte数组中的数据从offset位置开始解析length字节作为一个bitmap对象
decodeByteArray(byte[] data, int offset, int lenght);

//把文件pathName解析成bitmap对象
decodeFile(String pathName);

//从给定的流中解析出一个Bitmap对象,加载这个对象到内存中时应用options指定的选项
decodeStream(InputStream is, Rect outPadding, Options opts)

//根据id从给定的资源中解析出一个Bitmap对象,加载这个对象到内存中时应用options指定的选项
decodeResource(Resource res, int id, Options opts);

//从给定的流中解析出一个Bitmap对象
decodeStream(InputStream is);

这里我们把decodeFile()和decodeResource()函数作为对比。
decodeFile()用于读取SD卡上的图,得到的是图片的原始尺寸。
decodeResource()用于读取Res、Raw等资源,得到的是图片的原始尺寸 * 缩放系数。

Options
我们主要根据decodeResource()函数来介绍Options的一些特性,当然它在其他函数中的用法原理是一样的。所谓的Options的指定选项无非就是对它的一些变量的设置。

public static class Options { 

public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}

public Bitmap inBitmap; //用于实现Bitmap的复用,下面会具体介绍
public int inSampleSize; //采样率
public boolean inPremultiplied;
public boolean inDither; //是否开启抖动
public int inDensity; //上篇文章中的原始资源density
public int inTargetDensity; //目标屏幕密度
public boolean inScaled; //是否支持缩放
public int outWidth; //图片的原始宽度
public int outHeight; //图片的原始高度
...
}

· 缩放系数(inScaled)
在上面的源码中我们能看到inScaled变量来表示是否缩放,很明显默认是支持缩放。它是如何进行缩放的我们在上一篇Bitmap你有多大时已经讲过了,这个缩放系数除了图片分辨率是我们不可控的以外这两个变量(inDensity、inTargetDensity)我们都可以设置,当然我们在调用decodeResource()函数加载图片时即使我们不对上述两个变量进行设置,我们在上一篇源码中依然可以看到这两个变量还是会按照一定的规则被设值。我们来看看缩放系数对内存的影响。依旧是华为xx手机(inTargetDensity=320)加载xxhdpi文件的图片(480)。图片的分辨率850*1203
Android_Bitmap的高效加载

private void test(){
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.girl);
Log.i("华为手机的显示密度:", getResources().getDisplayMetrics().densityDpi+"");
Log.i("图片加载内存大小:", bitmap.getByteCount()+"b");
Log.i("图片加载内存大小:", bitmap.getByteCount()/1024+"k");
}

它的内存显示为我们知道应该这样计算:
850/480*320*1203/480*320*4=1817866。
Android_Bitmap的高效加载
我们手动设值它的缩放系数:

private void test(){
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inDensity = 160;
opts.inTargetDensity = 320;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.girl, opts);
Log.i("华为手机的显示密度:", getResources().getDisplayMetrics().densityDpi+"");
Log.i("图片加载内存大小:", bitmap.getByteCount()+"b");
Log.i("图片加载内存大小:", bitmap.getByteCount()/1024+"k");
}

这里320/160大于320/480,那么它的内存消耗要变大。缩放系数是原来的3倍则消耗内存为原来的9倍。
Android_Bitmap的高效加载

· 采样率(inSampleSize)
下面我们来介绍inSampleSize这个参数,当这个参数为1时,采样后的图片大小和原来一样;当这个参数为2时,采样后的图片宽高均为原来的1/2,大小也就成了原来的1/4。也就是说,采样后的大小等于原始大小除以采样率的平方。官方文档规定,inSampleSize的值应为2的非负整数次幂(1,2,4,… ),否则会被系统向下取整并找到一个最接近的值。通过设置inSampleSize我们就能够将图片缩放到一个合理的大小,那么该如何设置inSampleSize的值呢?在讲解这个之前,我们先来考虑以下情况:我们的ImageView的大小为100 100,要显示的图片大小为300 400,此时我们应该将inSampleSize设为多少呢。首先我们通过计算可以得到图片宽是ImageView的3倍,而图片高是ImageView的4倍。那么我们应该将图片宽高缩小为原来的4倍吗?假如我们把图片宽高都变为原来的1/4,那么现在图片大小为75 100,ImageView大小为100100,图片要显示在ImageView中需要进行拉伸,而拉伸的话可能会导致图片失真。所以我们应该把图片宽高变为原来的1/3,以保证它不小于ImageView的大小,这样尽管多占用一些内存,但不会造成图片质量的下降,这还是很有必要的。通过以上分析,我们知道了在设置inSampleSize时应该注意使得缩放后的图片大小不小于相应的ImageView大小。
第一步,获取图片的原始宽高,为了节省内存,需要先设置BitmapFactory.Options的inJustDecodeBounds为true,这样的Bitmap可以借助decodeFile方法把高和宽存放到Bitmap.Options中,但是内存占用为空(不会真正的加载图片)。所以在这期间取出的bitmap为null我们只是需要图片的尺寸罢了。

private void test(){
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.girl, opts);
opts.inJustDecodeBounds = false;
Log.i("bitmap", (bitmap==null) ? "bitmap==null" : "bitmap!=null");
Log.i("opts.outWidth=", opts.outWidth+"");
Log.i("opts.outHeight=", opts.outHeight+"");
//1.根据宽、高等参数获取options的采样率
//2.设置采样率
//3.执行BitmapFactory.decodeResource(getResources(), R.mipmap.girl, opts),获取bitmap实例
}

查看Log
Android_Bitmap的高效加载

第二步,根据原始宽高计算出inSampleSize,代码如下:

//dstWidth和dstHeight分别为目标ImageView的宽高
public static int calSampleSize(BitmapFactory.Options options,
int dstWidth, int dstHeight) {
int rawWidth = options.outWidth;
int rawHeight = options.outHeight;
int inSampleSize = 1;
if (rawWidth > dstWidth || rawHeight > dstHeight) {
float ratioHeight = (float) rawHeight / dstHeight;
float ratioWidth = (float) rawWidth / dstHeight;
inSampleSize = (int) Math.min(ratioWidth, ratioHeight);
}
return inSampleSize;
}

以上代码的逻辑很直接,唯一需要注意的就是要记得使采样后的图片能够“覆盖”ImageView,以防止图片质量下降。

一、缩略图

有了具备高宽信息的Options,结合上面的inSampleSize算法算出缩小的倍数,我们就能加载本地大图的某个合适大小的缩略图了。

/**
* 获取缩略图
* 支持自动旋转
* 某些型号的手机相机图片是反的,可以根据exif信息实现自动纠正
* @return
*/

public static Bitmap $thumbnail(String path,
int maxWidth, int maxHeight, boolean autoRotate) {
int angle = 0;
if (autoRotate) {
angle = ImageLess.$exifRotateAngle(path);
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
// 获取这个图片的宽和高信息到options中, 此时返回bm为空
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
options.inJustDecodeBounds = false;
// 计算缩放比
int sampleSize = $sampleSize(options, maxWidth, maxHeight);
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inPurgeable = true;
options.inInputShareable = true;
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
bitmap = BitmapFactory.decodeFile(path, options);
if (autoRotate && angle != 0) {
bitmap = $rotate(bitmap, angle);
}
return bitmap;
}

系统内置了一个ThumbnailUtils也能生成缩略图,细节上不一样但原理是相同的。感兴趣可以直接查看源码。

二、大图加载

BitmapRegionDecoder类,鉴于有点高端且应用场景不是太多(高清大图且不允许压缩),有机会好好单独的研究一下。