性能优化专题十一--高效使用Bitmaps使用AsyncTask解码图片

 高效使用Bitmaps有什么好处?

我们常常提到的“Android程序优化”,通常指的是性能和内存的优化,即:更快的响应速度,更低的内存占用。Android程序的性能和内存问题,大部分都和图片紧密相关,而图片的加载在很多情况下很用到Bitmap(位图)这个类。而由于Bitmap自身的特性(将每个像素的属性全部保存在内存中),导致稍有不慎就会创建出一个占用内存非常大的Bitmap对象,从而导致加载过慢,还会有内存溢出的风险。所以,Android程序要做优化,Bitmap的优化是必不可少的一步。

需要对Bitmap进行优化的情形

首先请看一行代码:

mImageView.setImageResource(R.drawable.my_image);

这是一行从资源文件中加载图片到ImageView的代码。通常这段代码没什么问题,但有些情况下,你需要对这段代码进行优化。例如当图片的尺寸远远大于ImageView的尺寸时,或者当你要在一个ListView或GridView中批量加载一些大小未知的图片时。实际上,以上这行代码会在运行时使用BitmapFactory.decodeStream()方法将资源图片生成一个Bitmap,然后由这个Bitmap生成一个Drawable,最后再将这个Drawable设置到ImageView。由于在过程中生成了Bitmap,因此如果你使用的图片过大,就会导致性能和内存占用的问题。另外,需要优化的情形不止这一种,这里就不再列举。

下面分步说明使用代码来减小Bitmap的尺寸从而达到减小内存占用的方法:

1. 获取原图片尺寸

通常,我们使用BitmapFactory.decodeResource()方法来从资源文件中读取一张图片并生成一个Bitmap。但如果使用一个BitmapFactory.Options对象,并把该对象的inJustDecodeBounds属性设置为true,decodeResource()方法就不会生成Bitmap对象,而仅仅是读取该图片的尺寸和类型信息:

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;

2. 根据原图尺寸和目标区域的尺寸计算出合适的Bitmap尺寸

BitmapFactory.Options类有一个参数inSampleSize,该参数为int型,他的值指示了在解析图片为Bitmap时在长宽两个方向上像素缩小的倍数。inSampleSize的默认值和最小值为1(当小于1时,解码器将该值当做1来处理),且在大于1时,该值只能为2的幂(当不为2的幂时,解码器会取与该值最接近的2的幂)。例如,当inSampleSize为2时,一个2000*1000的图片,将被缩小为1000*500,相应地,它的像素数和内存占用都被缩小为了原来的1/4:

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // 原始图片的宽高
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // 在保证解析出的bitmap宽高分别大于目标尺寸宽高的前提下,取可能的inSampleSize的最大值
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

3. 根据计算出的inSampleSize生成Bitmap 

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // 首先设置 inJustDecodeBounds=true 来获取图片尺寸
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

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

    // 根据计算出的 inSampleSize 来解码图片生成Bitmap
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

这里有一点要注意,就是要在第二遍decode之前把inJustDecodeBounds设置回false。

4. 调用以上的decodeSampledBitmapFromResource方法,使用自定尺寸的Bitmap

如果你要将一张大图设置为一个100*100的缩略图,执行以下代码:

mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

到此,使用decodeResource()方法将一个大图解析为小尺寸bitmap的应用就完成了。同理,还可以使用decodeStream(),decodeFile()等方法做相同的事,原理是一样的。

延伸:一个Bitmap到底占用多大内存?系统给每个应用程序分配多大内存?

· Bitmap占用的内存为:像素总数 * 每个像素占用的内存。在Android中,Bitmap有四种像素类型:ARGB_8888、ARGB_4444、ARGB_565、ALPHA_8,他们每个像素占用的字节数分别为4、2、2、1。因此,一个2000*1000的ARGB_8888类型的Bitmap占用的内存为2000*1000*4=8000000B=8MB。

· Android根据设备屏幕尺寸和dpi的不同,给系统分配的单应用程序内存大小也不同,具体如下表(表格取自Android 4.4 Compatibility Definition Document (CDD)):

 屏幕尺寸  DPI  应用内存
 small / normal / large  ldpi / mdpi  16MB
 small / normal / large  tvdpi / hdpi  32MB
 small / normal / large  xhdpi  64MB
 small / normal / large  400dpi  96MB
 small / normal / large  xxhdpi  128MB
 xlarge  mdpi  32MB
 xlarge  tvdpi / hdpi  64MB
 xlarge  xhdpi  128MB
 xlarge  400dpi  192MB
 xlarge  xxhdpi  256MB

为什么要在后台加载Bitmap?

有没有过这种体验:你在Android手机上打开了一个带有含图片的ListView的页面,用手猛地一划,就见那ListView嘎嘎地卡,仿佛每一个新的Item都是顶着阻力蹦出来的一样?看完这篇文章,你将学会怎样避免这种情况的发生。

在Android中,使用BitmapFactory.decodeResource(), BitmapFactory.decodeStream() 等方法可以把图片加载到Bitmap中。但由于这些方法是耗时的,所以多数情况下,这些方法应该放在非UI线程中,否则将有可能导致界面的卡顿,甚至是触发ANR。

一般情况下,网络图片的加载必须放在后台线程中;而本地图片就可以根据实际情况自行决定了,如果图片不多不大的话,也可以在UI线程中操作来图个方便。至于谷歌官方的说法,是只要是从硬盘或者从网络加载Bitmap,统统不应该在主线程中进行。

基础操作:使用AsyncTask

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(imageView);
    }

    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

以上代码摘自Android官方文档,是一个后台加载Bitmap并在加载完成后自动将Bitmap设置到ImageView的AsyncTask的实现。有了这个AsyncTask之后,异步加载Bitmap只需要下面的简单代码:

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

然后,一句loadBitmap(R.id.my_image, mImageView) 就能实现本地图片的异步加载了。

并发操作:在ListView和GridView中进行后台加载

在实际中,影响性能的往往是ListView和GridView这种包含大量图片的控件。在滑动过程中,大量的新图片在短时间内一起被加载,对于没有进行任何优化的程序,卡顿现象必然会随之而来。通过使用后台加载Bitmap的方式,这种问题将被有效解决。具体怎么做,我们来看看谷歌推荐的方法。

首先创建一个实现了Drawable接口的类,用来存储AsyncTask的引用。在本例中,选择了继承BitmapDrawable,用来给ImageView设置一个预留的占位图,这个占位图用于在AsyncTask执行完毕之前的显示。

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

接下来,和上面类似,依然是使用一个loadBitmap()方法来实现对图片的异步加载。不同的是,要在启动AsyncTask之前,把AsyncTask传给AsyncDrawable,并且使用AsyncDrawable为ImageView设置占位图:

public void loadBitmap(int resId, ImageView imageView) {
    if (cancelPotentialWork(resId, imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}

然后在Adapter的getView()方法中调用loadBitmap()方法,就可以为每个Item中的ImageView进行图片的动态加载了。

loadBitmap()方法的代码中有两个地方需要注意:第一,cancelPotentialWork()这个方法,它的作用是进行两项检查:首先检查当前是否已经有一个AsyncTask正在为这个ImageView加载图片,如果没有就直接返回true。如果有,再检查这个Task正在加载的资源是否与自己正要进行加载的资源相同,如果相同,那就没有必要再进行多一次的加载了,直接返回false;而如果不同(为什么会不同?文章最后会有解释),就取消掉这个正在进行的任务,并返回true。第二个需要注意的是,本例中的 BitmapWorkerTask 实际上和上例是有所不同的。这两点我们分开说,首先我们看cancelPotentialWork()方法的代码:

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (bitmapData != data) {
            // 取消之前的任务
            bitmapWorkerTask.cancel(true);
        } else {
            // 相同任务已经存在,直接返回false,不再进行重复的加载
            return false;
        }
    }
    // 没有Task和ImageView进行绑定,或者Task由于加载资源不同而被取消,返回true
    return true;
}

在cancelPotentialWork()的代码中,首先使用getBitmapWorkerTask()方法获取到与ImageView相关联的Task,然后进行上面所说的判断。好,我们接着来看这个getBitmapWorkerTask()是怎么写的:

private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   if (imageView != null) {
       final Drawable drawable = imageView.getDrawable();
       if (drawable instanceof AsyncDrawable) {
           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           return asyncDrawable.getBitmapWorkerTask();
       }
    }
    return null;
}

从代码中可以看出,该方法通过imageView获取到它内部的Drawable对象,如果获取到了并且该对象为AsyncDrawable的实例,就调用这个AsyncDrawable的getBitmapWorkerTask()方法来获取到它对应的Task,也就是通过一个ImageView->Drawable->AsyncTask的链来获取到ImageView所对应的AsyncTask。

好的,cancelPotentialWork()方法分析完了,我们回到刚才提到的第二个点:BitmapWorkerTask类的不同。这个类的改动在于onPostExecute()方法,具体请看下面代码:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (isCancelled()) {
            bitmap = null;
        }

        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

从代码中可以看出,在后台加载完Bitmap之后,它 并不是直接把Bitmap设置给ImageView,而是先判断这个ImageView对应的Task是不是自己 (为什么会不同?文章最后会有解释)。如果是自己,才会执行ImageView的setImageBitmap()方法。到此,一个并发的异步加载ListView(或GridView)中图片的实现全部完成。

延伸:文中两个“为什么会不同”的解答

首先,简单说一下ListView中Item和Item对应的View的关系(GridView中同理)。假设一个ListView含有100项,那么它的100个Item应该分别对应一个View用于显示,这样一共是100个View。但Android实际上并没有这样做。出于内存考虑,Android只会为屏幕上可见的每个Item分配一个View。用户滑动ListView,当第一个Item移动到可视范围外后,他所对应的View将会被系统分配给下一个即将出现的Item。

回到问题。

我们不妨假设屏幕上显示了一个ListView,并且它最多能显示10个Item,而用户在最顶部的Item(不妨称他为第1个Item)使用Task加载Bitmap的时候进行了滑动,并且直到第1个Item消失而第11个Item已经在屏幕底部出现的时候,这个Task还没有加载完成。那么此时,原先与第1个Item绑定的ImageView已经被重新绑定到了第11个Item上,并且第11个Item触发了getItem()方法。在getItem()方法中,ImageView第二次使用Task为自己加载Bitmap,但这时它需要加载的图片资源已经变了(由第1个Item对应的资源变成了第11个Item对应的资源),因此在cancelPotentialWork()方法执行时会判断两个资源不一致。这就是为什么相同ImageView却对应了不同的资源。

同理,一个Task持有了一个ImageView,但由于这个Task有可能已经过时,因此这个ImageView所对应的Task未必就是这个Task本身,也有可能是另一个更年轻的Task。

完整图片工具类:

package com.gala.video.app.opr.live.epg.news;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;

import com.gala.video.lib.framework.core.env.AppRuntimeEnv;

import java.lang.ref.WeakReference;

public class BitmapWorkTask extends AsyncTask<Integer, Void, Bitmap> {

    private final WeakReference<ImageView> imageViewWeakReference;
    private int data = 0;

    public BitmapWorkTask(ImageView imageView) {
        imageViewWeakReference = new WeakReference<ImageView>(imageView);
    }

    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(AppRuntimeEnv.get().getApplicationContext().getResources(), data, 100, 100);
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewWeakReference != null && bitmap != null) {
            final ImageView imageView = imageViewWeakReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }

    private Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
                                                         int reqWidth, int reqHeight) {

        // 首先设置 inJustDecodeBounds=true 来获取图片尺寸
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

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

        // 根据计算出的 inSampleSize 来解码图片生成Bitmap
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    private int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // 原始图片的宽高
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // 在保证解析出的bitmap宽高分别大于目标尺寸宽高的前提下,取可能的inSampleSize的最大值
            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }


}

猜你喜欢

转载自blog.csdn.net/cpcpcp123/article/details/106962692