Bitmap的高效加载

/ 0评 / 0

在我们的程序运行的时候,Android系统分配给我们程序的内存是有限的,当程序使用的内存过多,就会出现OOM(Out Of Memory),最常见出现OOM的地方就是加载图片,下面我就来介绍下如何高效的加载图片到我们的程序中。

图片的大小怎么计算?

在Android中,图片一共有4中配置,分别为

A:透明度    R:红色    G:绿    B:蓝

Bitmap.Config ARGB_4444:每个像素占四位,即A=4,R=4,G=4,B=4,那么一个像素点占4+4+4+4=16位 

Bitmap.Config ARGB_8888:每个像素占四位,即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位

Bitmap.Config RGB_565:每个像素占四位,即R=5,G=6,B=5,没有透明度,那么一个像素点占5+6+5=16位

Bitmap.Config ALPHA_8:每个像素占四位,只有透明度,没有颜色。

默认为Bitmap.Config ARGB_8888,也就是一个像素占用4个字节(4k)。一张1200*800像素的图片占用内存为 1200*800*4/1028/1024 = 3.6M。

1M = 1024 KB = 1024*1024 K = 1024*1024*8 位

处理的方法

对图片处理的方法有两种,第一种是降低图片质量。第二种是缩放图片尺寸,也可以把两种方法结合起来使用,避免OOM。

降低图片质量

一般情况下,用户不需要那么高精度的图片,比如显示的是一张略缩图,如果我们也是加载原图的话,而原图很大,那就很可能OOM了。

//加载低质量图片
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.img1, options);

在上面的代码中,我门使用了BitmapFactory.Options去设置加载的属性,让程序加载的图片格式为Bitmap.Config.RGB_565,从上面的分析可以看出来,这样图片占用的内存为默认配置的一半,而在测试过程中,发现和默认效果没有很大的区别。

减少图片尺寸

如果把一个大小为1024x768像素的图片显示到大小为128x96像素的ImageView上,显然没有必要把整张原图都加载到内存中,而是将原图缩放为128x96即可。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

设置 inJustDecodeBounds 属性为true可以在解码的时候避免内存的分配,它会返回一个null的Bitmap,但是可以获取到 outWidth, outHeight 与 outMimeType。该技术可以允许你在构造Bitmap之前优先读图片的尺寸与类型。(在加载图片之前获取图片的大小信息)

图片的缩放主要用到了BitmapFactory.Options的inSampleSize属性,默认为1,当inSampleSize大于1的时候,比如为2,那么缩放后的图片宽高均为原图的1/2,占用内存为原图的1/4,inSampleSize必须大于1才有效果,另外,官方文档指出,inSampleSize的值应该总为2的指数,比如1、2、4、8等,如果外界传递给系统的值不为2的指数,那么系统会自己向下取整,并选择一个接近2的指数的数来代替。

/**
 * 计算InSampleSize
 * @param options 需要缩放图片的配置信息
 * @param reqWidth 缩放后的宽
 * @param reqHeight 缩放后的高
 * @return 计算出的InSampleSize
 */
public int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;
    if (height > reqWidth || width > reqHeight) {
        float ratioWidth = (float) width / reqWidth;
        float ratioHeight = (float) height / reqHeight;
        inSampleSize = (int) Math.min(ratioHeight, ratioWidth);
    }
    //因为系统会自己向下取整为最接近的2的指数,所以我们不处理
    inSampleSize = Math.max(1, inSampleSize);
    return inSampleSize;
}

封装为一个函数就是:

/**
 * 缩放从资源文件中加载的图片
 * @param res 资源文件对象
 * @param resId 文件id
 * @param reqWidth 需要设置的宽
 * @param reqHeight 需要设置的高
 * @return 缩放后的Bitmap对象
 */
public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {

    // 计算出图片的尺寸
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // 计算出inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

测试代码

布局文件为3个ListView,用于显示原图,低质量图片,小尺寸图片。查看显示效果。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/img1"
        android:layout_width="280dp"
        android:layout_height="140dp" />

    <ImageView
        android:id="@+id/img2"
        android:layout_width="280dp"
        android:layout_height="140dp" />

    <ImageView
        android:id="@+id/img3"
        android:layout_width="280dp"
        android:layout_height="140dp" />
</LinearLayout>

主Activity代码

public class MainActivity extends AppCompatActivity {
    private Bitmap mBitmap;
    private static final String TAG = "MainActivity";
    private ImageView mImg1;
    private ImageView mImg2;
    private ImageView mImg3;

    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mImg1 = (ImageView) findViewById(R.id.img1);
        mImg2 = (ImageView) findViewById(R.id.img2);
        mImg3 = (ImageView) findViewById(R.id.img3);

        //直接加载原图
        Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.img1);
        mImg1.setImageBitmap(bitmap1);
        //加载低质量图片
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.img1, options);
        mImg2.setImageBitmap(bitmap2);
        //加载小尺寸图片
        //以为在onCreate里面无法获取ImageView的宽高,所以这里先强行测量大小
        //布局文件中大小为280dp * 140dp
        mImg3.measure(View.MeasureSpec.makeMeasureSpec(dp2px(280), View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(dp2px(140), View.MeasureSpec.EXACTLY));
        Bitmap bitmap3 = decodeSampledBitmapFromResource(getResources(),
                R.drawable.img1, mImg3.getMeasuredWidth(), mImg3.getMeasuredHeight());
        mImg3.setImageBitmap(bitmap3);
        Log.i(TAG, "原图" + bitmap1.getByteCount());
        Log.i(TAG, "低质量" + bitmap2.getByteCount());
        Log.i(TAG, "小尺寸" + bitmap3.getByteCount());
    }

    /**
     * dp转px
     *
     * @param dp 待转换的dp
     * @return 打牌转px的结果
     */
    int dp2px(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }

    /**
     * 计算InSampleSize
     *
     * @param options   需要缩放图片的配置信息
     * @param reqWidth  缩放后的宽
     * @param reqHeight 缩放后的高
     * @return 计算出的InSampleSize
     */
    public int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqWidth || width > reqHeight) {
            float ratioWidth = (float) width / reqWidth;
            float ratioHeight = (float) height / reqHeight;
            inSampleSize = (int) Math.min(ratioHeight, ratioWidth);
        }
        //因为系统会自己向下取整为最接近的2的指数,所以我们不处理
        inSampleSize = Math.max(1, inSampleSize);
        return inSampleSize;
    }

    /**
     * 缩放从资源文件中加载的图片
     *
     * @param res       资源文件对象
     * @param resId     文件id
     * @param reqWidth  需要设置的宽
     * @param reqHeight 需要设置的高
     * @return 缩放后的Bitmap对象
     */
    public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {

        // 计算出图片的尺寸
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // 计算出inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
}

结果对比

Log输出为

05-11 08:10:02.906 20605-20605/com.jay.testloadimage I/MainActivity: 原图16785408

05-11 08:10:02.906 20605-20605/com.jay.testloadimage I/MainActivity: 低质量8392704

05-11 08:10:02.906 20605-20605/com.jay.testloadimage I/MainActivity: 小尺寸4196352

效果图如下,可以看到,原图加载占用内存约为16M,低质量为8M,小尺寸为4M,如果两种方式结合起来大小应该为2M,可是显示效果区别不大,所以,加载大图时,在图片要求不大的情况下应该进行处理,不然容易OOM。

device-2016-05-11-201436

延伸阅读

Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?

发表评论

您的电子邮箱地址不会被公开。